1use crate::error::Error;
4use crate::problem::error_to_problem;
5use crate::request_id::current_request_id;
6use crate::response::Response;
7use std::cell::RefCell;
8
9tokio::task_local! {
10 static ACCEPT: RefCell<String>;
11}
12
13pub async fn with_accept<F, T>(accept: impl Into<String>, fut: F) -> T
15where
16 F: std::future::Future<Output = T>,
17{
18 ACCEPT.scope(RefCell::new(accept.into()), fut).await
19}
20
21pub fn current_accept() -> Option<String> {
23 ACCEPT.try_with(|c| c.borrow().clone()).ok()
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ErrorFormat {
29 Html,
30 ProblemJson,
31 Text,
32}
33
34pub fn negotiate_error_format(accept: Option<&str>) -> ErrorFormat {
37 let accept = accept.unwrap_or("*/*").to_ascii_lowercase();
38 let wants_html = accept.contains("text/html");
39 let wants_problem = accept.contains("application/problem+json");
40 let wants_json = accept.contains("application/json");
41
42 if wants_html && !accept.contains("application/json") && !wants_problem {
43 return ErrorFormat::Html;
44 }
45 if wants_html {
47 if let Some(html_i) = accept.find("text/html") {
48 let json_i = accept
49 .find("application/json")
50 .or_else(|| accept.find("application/problem+json"))
51 .unwrap_or(usize::MAX);
52 if html_i <= json_i {
53 return ErrorFormat::Html;
54 }
55 }
56 }
57 if wants_problem || wants_json {
58 return ErrorFormat::ProblemJson;
59 }
60 if accept.trim() == "*/*" || accept.is_empty() {
61 return ErrorFormat::Text;
62 }
63 ErrorFormat::Text
64}
65
66fn error_title_detail(err: &Error) -> (u16, &'static str, String) {
67 match err {
68 Error::NotFound => (404, "Not Found", "Not Found".into()),
69 Error::Unauthorized => (401, "Unauthorized", "Unauthorized".into()),
70 Error::Forbidden => (403, "Forbidden", "Forbidden".into()),
71 Error::BadRequest(msg) => (400, "Bad Request", msg.clone()),
72 Error::PayloadTooLarge => (413, "Payload Too Large", "Payload Too Large".into()),
73 Error::MethodNotAllowed => (405, "Method Not Allowed", "Method Not Allowed".into()),
74 Error::Internal(msg) => (500, "Internal Server Error", msg.clone()),
75 Error::Json(e) => (400, "Bad Request", format!("JSON error: {e}")),
76 Error::Io(e) => (500, "Internal Server Error", format!("IO error: {e}")),
77 Error::Response(_) => (500, "Error", "Error".into()),
78 }
79}
80
81pub fn html_error_page(status: u16, title: &str, detail: &str) -> Response {
83 let rid = current_request_id().unwrap_or_default();
84 let rid_row = if rid.is_empty() {
85 String::new()
86 } else {
87 format!("<p class=\"rid\">request_id: {rid}</p>")
88 };
89 let body = format!(
90 "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">\
91 <title>{status} {title}</title>\
92 <style>body{{font-family:system-ui,sans-serif;margin:2rem;color:#111}}\
93 h1{{font-size:1.5rem}} .rid{{color:#666;font-size:.875rem}}</style></head>\
94 <body><h1>{status} {title}</h1><p>{detail}</p>{rid_row}</body></html>",
95 detail = html_escape(detail),
96 title = html_escape(title),
97 );
98 Response::html(body).status(status)
99}
100
101fn html_escape(s: &str) -> String {
102 s.replace('&', "&")
103 .replace('<', "<")
104 .replace('>', ">")
105 .replace('"', """)
106}
107
108pub fn error_response_for_accept(accept: Option<&str>, err: Error) -> Response {
110 if let Error::Response(res) = err {
111 return *res;
112 }
113 let accept = accept
114 .map(|s| s.to_string())
115 .or_else(current_accept);
116 match negotiate_error_format(accept.as_deref()) {
117 ErrorFormat::Html => {
118 let (status, title, detail) = error_title_detail(&err);
119 html_error_page(status, title, &detail)
120 }
121 ErrorFormat::ProblemJson => error_to_problem(err),
122 ErrorFormat::Text => err.into_response(),
123 }
124}
125
126pub fn status_response_for_accept(accept: Option<&str>, status: u16, detail: &str) -> Response {
128 let title = match status {
129 404 => "Not Found",
130 405 => "Method Not Allowed",
131 _ => "Error",
132 };
133 match negotiate_error_format(accept) {
134 ErrorFormat::Html => html_error_page(status, title, detail),
135 ErrorFormat::ProblemJson => crate::problem::problem_response(status, title, detail, []),
136 ErrorFormat::Text => Response::text(detail.to_string()).status(status),
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn browser_accept_prefers_html() {
146 assert_eq!(
147 negotiate_error_format(Some(
148 "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
149 )),
150 ErrorFormat::Html
151 );
152 }
153
154 #[test]
155 fn api_client_prefers_json() {
156 assert_eq!(
157 negotiate_error_format(Some("application/json")),
158 ErrorFormat::ProblemJson
159 );
160 assert_eq!(
161 negotiate_error_format(Some("application/problem+json")),
162 ErrorFormat::ProblemJson
163 );
164 }
165}