1#[cfg(all(test, feature = "serde"))]
32mod tests;
33
34use std::panic::{catch_unwind, AssertUnwindSafe};
35use std::sync::atomic::{AtomicBool, Ordering};
36use std::sync::{Mutex, Once};
37
38use crate::application::Application;
39use crate::entry_point::RwsEnv;
40use crate::header::Header;
41use crate::middleware::Middleware;
42use crate::request::Request;
43use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
44use crate::server::ConnectionInfo;
45
46struct Captured {
49 location: String,
50 backtrace: String,
51}
52
53static CAPTURING: AtomicBool = AtomicBool::new(false);
54static LAST: Mutex<Vec<(std::thread::ThreadId, Captured)>> = Mutex::new(Vec::new());
58const KEPT: usize = 64;
59
60fn install_hook() {
63 static ONCE: Once = Once::new();
64 ONCE.call_once(|| {
65 let previous = std::panic::take_hook();
66 std::panic::set_hook(Box::new(move |info| {
67 if CAPTURING.load(Ordering::Relaxed) {
68 let location = info.location().map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column())).unwrap_or_default();
69 let backtrace = std::backtrace::Backtrace::force_capture().to_string();
70 if let Ok(mut last) = LAST.lock() {
71 let me = std::thread::current().id();
72 last.retain(|(id, _)| *id != me);
73 if last.len() >= KEPT {
74 last.remove(0);
75 }
76 last.push((me, Captured { location, backtrace }));
77 }
78 }
79 previous(info);
80 }));
81 });
82}
83
84fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
85 if let Some(s) = payload.downcast_ref::<&str>() {
86 s.to_string()
87 } else if let Some(s) = payload.downcast_ref::<String>() {
88 s.clone()
89 } else {
90 "a panic with a non-string payload".to_string()
91 }
92}
93
94#[derive(Clone, Copy, Debug)]
96pub struct ErrorPages {
97 details: bool,
98}
99
100impl ErrorPages {
101 pub fn from_env() -> Self {
104 Self::detailed(RwsEnv::current().shows_error_details())
105 }
106
107 pub fn detailed(details: bool) -> Self {
109 if details {
110 install_hook();
111 CAPTURING.store(true, Ordering::Relaxed);
112 }
113 ErrorPages { details }
114 }
115
116 pub fn shows_details(&self) -> bool {
118 self.details
119 }
120
121 fn answer(&self, request: &Request, message: &str, captured: Option<Captured>) -> Response {
122 let (location, backtrace) = captured.map(|c| (c.location, c.backtrace)).unwrap_or_default();
123 eprintln!("[rws] {} {} failed: {message}{}{}", request.method, request.request_uri, if location.is_empty() { String::new() } else { format!(" at {location}") }, if backtrace.is_empty() { String::new() } else { format!("\n{backtrace}") });
124 if !self.details {
125 return crate::error::problem::problem(STATUS_CODE_REASON_PHRASE.n500_internal_server_error, "");
126 }
127 let wants_html = request.header("accept").is_some_and(|h| h.value.contains("text/html"));
128 if wants_html {
129 return page(request, message, &location, &backtrace);
130 }
131 let frames: Vec<&str> = backtrace.lines().map(str::trim_end).filter(|l| !l.is_empty()).collect();
132 let body = serde_free_json(message, &location, &frames);
133 let content = crate::range::Range::get_content_range(body.into_bytes(), crate::error::problem::PROBLEM_JSON.to_string());
134 Response::get_response(STATUS_CODE_REASON_PHRASE.n500_internal_server_error, None, Some(vec![content]))
135 }
136}
137
138fn serde_free_json(message: &str, location: &str, frames: &[&str]) -> String {
141 fn s(text: &str) -> String {
142 let mut out = String::from("\"");
143 for c in text.chars() {
144 match c {
145 '"' => out.push_str("\\\""),
146 '\\' => out.push_str("\\\\"),
147 '\n' => out.push_str("\\n"),
148 '\r' => out.push_str("\\r"),
149 '\t' => out.push_str("\\t"),
150 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
151 c => out.push(c),
152 }
153 }
154 out.push('"');
155 out
156 }
157 let frames: Vec<String> = frames.iter().map(|f| s(f)).collect();
158 format!(
159 "{{\"type\":\"about:blank\",\"title\":\"Internal Server Error\",\"status\":500,\"detail\":{},\"location\":{},\"backtrace\":[{}]}}",
160 s(message),
161 s(location),
162 frames.join(",")
163 )
164}
165
166fn escape(text: &str) -> String {
167 text.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """)
168}
169
170fn page(request: &Request, message: &str, location: &str, backtrace: &str) -> Response {
171 let headers: String = request
172 .headers
173 .iter()
174 .map(|h| {
175 let secret = matches!(h.name.to_ascii_lowercase().as_str(), "cookie" | "authorization" | "proxy-authorization" | "x-api-key");
176 let value = if secret { "(hidden)" } else { h.value.as_ref() };
177 format!("<tr><th>{}</th><td>{}</td></tr>", escape(&h.name), escape(value))
178 })
179 .collect();
180 let body = format!(
181 "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><title>500: {title}</title>\
182 <style>body{{font:15px system-ui,sans-serif;margin:2rem;color:#222}}h1{{color:#b00020;font-size:1.4rem}}code,pre{{font:13px ui-monospace,monospace}}pre{{background:#f6f6f6;padding:1rem;overflow:auto}}th{{text-align:left;padding-right:1rem;vertical-align:top}}</style></head>\
183 <body><h1>{title}</h1><p><code>{location}</code></p><p><code>{method} {uri}</code></p>\
184 <h2>Backtrace</h2><pre>{backtrace}</pre><h2>Request headers</h2><table>{headers}</table>\
185 <p><small>Shown because RWS_ENV is dev or test; production answers a bare 500.</small></p></body></html>",
186 title = escape(message),
187 location = escape(location),
188 method = escape(&request.method),
189 uri = escape(&request.request_uri),
190 backtrace = if backtrace.is_empty() { "(none: the handler ran on another thread, or the app returned an error)".to_string() } else { escape(backtrace) },
191 );
192 let content = crate::range::Range::get_content_range(body.into_bytes(), crate::mime_type::MimeType::TEXT_HTML.to_string());
193 let mut response = Response::get_response(STATUS_CODE_REASON_PHRASE.n500_internal_server_error, None, Some(vec![content]));
194 response.headers.push(Header::new(Header::_CACHE_CONTROL, "no-store"));
195 response
196}
197
198impl Middleware for ErrorPages {
199 fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
200 match catch_unwind(AssertUnwindSafe(|| next.execute(request, connection))) {
201 Ok(Ok(response)) => Ok(response),
202 Ok(Err(message)) => Ok(self.answer(request, &message, None)),
203 Err(payload) => {
204 let me = std::thread::current().id();
205 let captured = LAST.lock().ok().and_then(|mut last| {
206 let at = last.iter().position(|(id, _)| *id == me)?;
207 Some(last.remove(at).1)
208 });
209 Ok(self.answer(request, &panic_message(&*payload), captured))
210 }
211 }
212 }
213
214 fn is_passthrough(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
215 !self.details
216 }
217
218 fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
219 true
220 }
221
222 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
223 true
224 }
225
226 fn layer_name(&self) -> &'static str {
227 "ErrorPages"
228 }
229}