use http::StatusCode;
use crate::http::{HttpResponse, request::HttpRequest};
#[derive(Debug, Clone)]
pub struct TechnicalErrorResponse {
pub status: StatusCode,
pub exception_type: String,
pub exception_value: String,
pub request_path: String,
pub request_method: String,
pub traceback: Vec<String>,
pub settings: Vec<(String, String)>,
}
impl TechnicalErrorResponse {
#[must_use]
pub fn new(status: StatusCode, error: &str) -> Self {
let (exception_type, exception_value) = split_error(error);
Self {
status,
exception_type,
exception_value,
request_path: String::new(),
request_method: String::new(),
traceback: Vec::new(),
settings: Vec::new(),
}
}
#[must_use]
pub fn render(&self) -> HttpResponse {
let title = format!(
"{} {}",
self.status.as_u16(),
self.status.canonical_reason().unwrap_or("Error")
);
let traceback_items = if self.traceback.is_empty() {
"<li>No traceback available.</li>".to_string()
} else {
self.traceback
.iter()
.map(|line| format!("<li>{}</li>", escape_html(line)))
.collect::<Vec<_>>()
.join("")
};
let settings_rows = if self.settings.is_empty() {
"<tr><th>Details</th><td>None</td></tr>".to_string()
} else {
self.settings
.iter()
.map(|(key, value)| {
format!(
"<tr><th>{}</th><td><pre>{}</pre></td></tr>",
escape_html(key),
escape_html(value)
)
})
.collect::<Vec<_>>()
.join("")
};
let body = format!(
concat!(
"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">",
"<title>{title}</title>",
"<style>",
"body{{margin:0;font-family:Arial,sans-serif;background:#f6f6cf;color:#111;}}",
".page{{max-width:1100px;margin:0 auto;padding:24px;}}",
"h1{{margin:0 0 8px;font-size:32px;}}",
".subtitle{{font-size:18px;margin-bottom:20px;}}",
".panel{{background:#fff;border:1px solid #d6d0a5;margin-bottom:16px;}}",
".panel h2{{margin:0;padding:10px 14px;background:#efe8a1;font-size:18px;border-bottom:1px solid #d6d0a5;}}",
".panel .content{{padding:14px;}}",
"table{{width:100%;border-collapse:collapse;}}",
"th,td{{padding:10px;vertical-align:top;border-top:1px solid #ece5b6;text-align:left;}}",
"th{{width:220px;background:#faf7d9;}}",
"ul{{margin:0;padding-left:24px;}}pre{{margin:0;white-space:pre-wrap;word-break:break-word;}}",
"code{{background:#faf7d9;padding:2px 4px;}}",
"</style></head><body><div class=\"page\">",
"<h1>{title}</h1><div class=\"subtitle\">{exception_type}: {exception_value}</div>",
"<section class=\"panel\"><h2>Request information</h2><div class=\"content\"><table>",
"<tr><th>Method</th><td><code>{request_method}</code></td></tr>",
"<tr><th>Path</th><td><code>{request_path}</code></td></tr>",
"</table></div></section>",
"<section class=\"panel\"><h2>Traceback</h2><div class=\"content\"><ul>{traceback_items}</ul></div></section>",
"<section class=\"panel\"><h2>Environment</h2><div class=\"content\"><table>{settings_rows}</table></div></section>",
"</div></body></html>"
),
title = escape_html(&title),
exception_type = escape_html(&self.exception_type),
exception_value = escape_html(&self.exception_value),
request_method = escape_html(&self.request_method),
request_path = escape_html(&self.request_path),
traceback_items = traceback_items,
settings_rows = settings_rows,
);
HttpResponse::with_status(self.status, body)
}
}
#[must_use]
pub fn technical_404_response(request: &HttpRequest, patterns: &[String]) -> HttpResponse {
let mut error =
TechnicalErrorResponse::new(StatusCode::NOT_FOUND, "Resolver404: Page not found");
error.request_method = request.method.to_string();
error.request_path = request.get_full_path();
error.exception_value = format!(
"No route matched {}. {} pattern(s) were tried.",
request.path,
patterns.len()
);
error.traceback = patterns
.iter()
.map(|pattern| format!("Tried pattern: {pattern}"))
.collect();
error.settings = vec![("Headers".to_string(), headers_summary(request))];
error.render()
}
#[must_use]
pub fn technical_500_response(request: &HttpRequest, error: &str) -> HttpResponse {
let mut technical = TechnicalErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, error);
technical.request_method = request.method.to_string();
technical.request_path = request.get_full_path();
technical.traceback = error.lines().map(str::to_owned).collect();
technical.settings = vec![("Headers".to_string(), headers_summary(request))];
technical.render()
}
#[must_use]
fn split_error(error: &str) -> (String, String) {
if let Some((kind, value)) = error.split_once(':') {
(kind.trim().to_string(), value.trim().to_string())
} else {
("Error".to_string(), error.to_string())
}
}
#[must_use]
fn headers_summary(request: &HttpRequest) -> String {
if request.headers.is_empty() {
return "No headers".to_string();
}
request
.headers
.iter()
.map(|(name, value)| {
format!(
"{}: {}",
name.as_str(),
String::from_utf8_lossy(value.as_bytes())
)
})
.collect::<Vec<_>>()
.join("\n")
}
#[must_use]
fn escape_html(value: &str) -> String {
value
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('\"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use http::{HeaderMap, HeaderValue, Method, StatusCode, Uri, header::HOST};
use super::{technical_404_response, technical_500_response};
use crate::http::request::HttpRequest;
fn build_request() -> HttpRequest {
let mut headers = HeaderMap::new();
headers.insert(HOST, HeaderValue::from_static("example.com"));
headers.insert("x-request-id", HeaderValue::from_static("req-123"));
HttpRequest::from_axum(
Method::GET,
Uri::from_static("/missing/?page=2"),
headers,
Bytes::new(),
)
}
#[test]
fn test_technical_404_includes_patterns() {
let response = technical_404_response(
&build_request(),
&["admin/".to_string(), "articles/<slug>/".to_string()],
);
let body = std::str::from_utf8(&response.content).expect("utf8 body");
assert_eq!(response.status_code, StatusCode::NOT_FOUND);
assert!(body.contains("admin/"));
assert!(body.contains("articles/<slug>/"));
assert!(body.contains("/missing/?page=2"));
}
#[test]
fn test_technical_500_includes_error() {
let response =
technical_500_response(&build_request(), "ValueError: broken\nframe one\nframe two");
let body = std::str::from_utf8(&response.content).expect("utf8 body");
assert_eq!(response.status_code, StatusCode::INTERNAL_SERVER_ERROR);
assert!(body.contains("ValueError"));
assert!(body.contains("broken"));
assert!(body.contains("frame one"));
assert!(body.contains("x-request-id: req-123"));
}
}