1use std::fmt;
9
10type Source = Box<dyn std::error::Error + Send + Sync>;
11
12pub struct Error {
28 status: u16,
29 code: &'static str,
30 source: Option<Source>,
31}
32
33impl Error {
34 #[must_use]
36 pub fn new(status: u16, code: &'static str) -> Self {
37 let status = if (400..=599).contains(&status) {
38 status
39 } else {
40 500
41 };
42 Self {
43 status,
44 code,
45 source: None,
46 }
47 }
48
49 #[must_use]
51 pub fn bad_request(code: &'static str) -> Self {
52 Self::new(400, code)
53 }
54
55 #[must_use]
57 pub fn forbidden(code: &'static str) -> Self {
58 Self::new(403, code)
59 }
60
61 #[must_use]
63 pub fn not_found(code: &'static str) -> Self {
64 Self::new(404, code)
65 }
66
67 #[must_use]
69 pub fn conflict(code: &'static str) -> Self {
70 Self::new(409, code)
71 }
72
73 #[must_use]
76 pub fn unprocessable(code: &'static str) -> Self {
77 Self::new(422, code)
78 }
79
80 #[must_use]
82 pub fn internal(code: &'static str) -> Self {
83 Self::new(500, code)
84 }
85
86 #[must_use]
88 pub fn unavailable(code: &'static str) -> Self {
89 Self::new(503, code)
90 }
91
92 #[must_use]
94 pub fn with_source(mut self, source: impl Into<Source>) -> Self {
95 self.source = Some(source.into());
96 self
97 }
98
99 #[must_use]
101 pub const fn status(&self) -> u16 {
102 self.status
103 }
104
105 #[must_use]
107 pub const fn code(&self) -> &'static str {
108 self.code
109 }
110
111 #[must_use]
113 pub fn source_ref(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)> {
114 self.source.as_deref()
115 }
116}
117
118impl fmt::Debug for Error {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 f.debug_struct("Error")
121 .field("status", &self.status)
122 .field("code", &self.code)
123 .field("source", &self.source.as_ref().map(|s| s.to_string()))
124 .finish()
125 }
126}
127
128impl fmt::Display for Error {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 write!(f, "{} {}", self.status, self.code)?;
131 if let Some(s) = &self.source {
132 write!(f, ": {s}")?;
133 }
134 Ok(())
135 }
136}
137
138impl std::error::Error for Error {
139 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
140 self.source
141 .as_deref()
142 .map(|s| s as &(dyn std::error::Error + 'static))
143 }
144}
145
146impl From<crate::Denial> for Error {
147 fn from(d: crate::Denial) -> Self {
148 Self::new(d.status_code(), d.code())
149 }
150}
151
152impl From<form_net::FormError> for Error {
153 fn from(e: form_net::FormError) -> Self {
155 Self::new(e.status_hint(), e.code()).with_source(e)
156 }
157}
158
159impl From<crate::RenderError> for Error {
160 fn from(e: crate::RenderError) -> Self {
161 Self::internal(e.code()).with_source(e)
162 }
163}
164
165pub type Result<T = (), E = Error> = std::result::Result<T, E>;
167
168#[non_exhaustive]
170#[derive(Clone, Debug, PartialEq, Eq)]
171pub struct Problem {
172 pub subject: String,
174 pub message: String,
176}
177
178#[derive(Clone, Debug, PartialEq, Eq)]
181pub struct BuildError {
182 problems: Vec<Problem>,
183}
184
185impl BuildError {
186 pub(crate) fn new(problems: Vec<Problem>) -> Self {
187 Self { problems }
188 }
189
190 #[must_use]
192 pub fn problems(&self) -> &[Problem] {
193 &self.problems
194 }
195}
196
197impl fmt::Display for BuildError {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 write!(f, "the application has {} problem(s):", self.problems.len())?;
200 for p in &self.problems {
201 write!(f, "\n {}: {}", p.subject, p.message)?;
202 }
203 Ok(())
204 }
205}
206
207impl std::error::Error for BuildError {}
208
209pub(crate) fn title(status: u16) -> &'static str {
211 match status {
212 400 => "Bad Request",
213 401 => "Unauthorized",
214 403 => "Forbidden",
215 404 => "Not Found",
216 405 => "Method Not Allowed",
217 409 => "Conflict",
218 410 => "Gone",
219 413 => "Content Too Large",
220 415 => "Unsupported Media Type",
221 421 => "Misdirected Request",
222 422 => "Unprocessable Content",
223 429 => "Too Many Requests",
224 500 => "Internal Server Error",
225 501 => "Not Implemented",
226 503 => "Service Unavailable",
227 s if s < 500 => "Request Refused",
228 _ => "Server Error",
229 }
230}
231
232pub(crate) fn message(status: u16) -> &'static str {
234 match status {
235 400 => "The request could not be understood.",
236 401 => "Please sign in to continue.",
237 403 => "You do not have access to this page.",
238 404 => "The page was not found.",
239 405 => "This page does not accept that kind of request.",
240 421 => "This server does not serve that address.",
241 422 => "The request could not be processed.",
242 429 => "Too many requests. Please wait a moment and try again.",
243 503 => "The service is unavailable right now. Please try again shortly.",
244 s if s < 500 => "The request was refused.",
245 _ => "Something went wrong on our side.",
246 }
247}
248
249pub(crate) fn builtin_page(status: u16, reference: &str, out: &mut Vec<u8>) {
253 let t = title(status);
254 out.extend_from_slice(
255 b"<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><title>",
256 );
257 push_status(status, out);
258 out.push(b' ');
259 out.extend_from_slice(t.as_bytes());
260 out.extend_from_slice(
261 b"</title><meta name=\"viewport\" content=\"width=device-width\"></head>\n<body><h1>",
262 );
263 out.extend_from_slice(t.as_bytes());
264 out.extend_from_slice(b"</h1><p>");
265 out.extend_from_slice(message(status).as_bytes());
266 out.extend_from_slice(b"</p><p><small>Reference: ");
267 out.extend_from_slice(reference.as_bytes());
268 out.extend_from_slice(b"</small></p></body></html>\n");
269}
270
271pub(crate) fn json_body(status: u16, reference: &str, out: &mut Vec<u8>) {
274 out.extend_from_slice(b"{\"status\":");
275 push_status(status, out);
276 out.extend_from_slice(b",\"title\":\"");
277 out.extend_from_slice(title(status).as_bytes());
278 out.extend_from_slice(b"\",\"message\":\"");
279 out.extend_from_slice(message(status).as_bytes());
280 out.extend_from_slice(b"\",\"reference\":\"");
281 out.extend_from_slice(reference.as_bytes());
283 out.extend_from_slice(b"\"}");
284}
285
286fn push_status(status: u16, out: &mut Vec<u8>) {
287 let s = status.min(999);
288 out.push(b'0' + (s / 100) as u8);
289 out.push(b'0' + (s / 10 % 10) as u8);
290 out.push(b'0' + (s % 10) as u8);
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn test_status_clamped() {
299 assert_eq!(Error::new(200, "x").status(), 500);
300 assert_eq!(Error::new(302, "x").status(), 500);
301 assert_eq!(Error::new(418, "x").status(), 418);
302 }
303
304 #[test]
305 fn test_builtin_page_has_no_detail() {
306 let mut out = Vec::new();
307 builtin_page(404, "c7-r2", &mut out);
308 let s = String::from_utf8(out).unwrap();
309 assert!(s.contains("<title>404 Not Found</title>"));
310 assert!(s.contains("Reference: c7-r2"));
311 }
312
313 #[test]
314 fn test_build_error_lists_all() {
315 let e = BuildError::new(vec![
316 Problem {
317 subject: "GET /".into(),
318 message: "a".into(),
319 },
320 Problem {
321 subject: "GET /b".into(),
322 message: "b".into(),
323 },
324 ]);
325 assert_eq!(e.problems().len(), 2);
326 assert!(e.to_string().contains("2 problem(s)"));
327 }
328}