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#[derive(Clone, Debug, PartialEq, Eq)]
170pub struct Problem {
171 pub subject: String,
173 pub message: String,
175}
176
177#[derive(Clone, Debug, PartialEq, Eq)]
180pub struct BuildError {
181 problems: Vec<Problem>,
182}
183
184impl BuildError {
185 pub(crate) fn new(problems: Vec<Problem>) -> Self {
186 Self { problems }
187 }
188
189 #[must_use]
191 pub fn problems(&self) -> &[Problem] {
192 &self.problems
193 }
194}
195
196impl fmt::Display for BuildError {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 write!(f, "the application has {} problem(s):", self.problems.len())?;
199 for p in &self.problems {
200 write!(f, "\n {}: {}", p.subject, p.message)?;
201 }
202 Ok(())
203 }
204}
205
206impl std::error::Error for BuildError {}
207
208pub(crate) fn title(status: u16) -> &'static str {
210 match status {
211 400 => "Bad Request",
212 401 => "Unauthorized",
213 403 => "Forbidden",
214 404 => "Not Found",
215 405 => "Method Not Allowed",
216 409 => "Conflict",
217 410 => "Gone",
218 413 => "Content Too Large",
219 415 => "Unsupported Media Type",
220 421 => "Misdirected Request",
221 422 => "Unprocessable Content",
222 429 => "Too Many Requests",
223 500 => "Internal Server Error",
224 501 => "Not Implemented",
225 503 => "Service Unavailable",
226 s if s < 500 => "Request Refused",
227 _ => "Server Error",
228 }
229}
230
231pub(crate) fn message(status: u16) -> &'static str {
233 match status {
234 400 => "The request could not be understood.",
235 401 => "Please sign in to continue.",
236 403 => "You do not have access to this page.",
237 404 => "The page was not found.",
238 405 => "This page does not accept that kind of request.",
239 421 => "This server does not serve that address.",
240 422 => "The request could not be processed.",
241 429 => "Too many requests. Please wait a moment and try again.",
242 503 => "The service is unavailable right now. Please try again shortly.",
243 s if s < 500 => "The request was refused.",
244 _ => "Something went wrong on our side.",
245 }
246}
247
248pub(crate) fn builtin_page(status: u16, reference: &str, out: &mut Vec<u8>) {
252 let t = title(status);
253 out.extend_from_slice(
254 b"<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><title>",
255 );
256 push_status(status, out);
257 out.push(b' ');
258 out.extend_from_slice(t.as_bytes());
259 out.extend_from_slice(
260 b"</title><meta name=\"viewport\" content=\"width=device-width\"></head>\n<body><h1>",
261 );
262 out.extend_from_slice(t.as_bytes());
263 out.extend_from_slice(b"</h1><p>");
264 out.extend_from_slice(message(status).as_bytes());
265 out.extend_from_slice(b"</p><p><small>Reference: ");
266 out.extend_from_slice(reference.as_bytes());
267 out.extend_from_slice(b"</small></p></body></html>\n");
268}
269
270pub(crate) fn json_body(status: u16, reference: &str, out: &mut Vec<u8>) {
273 out.extend_from_slice(b"{\"status\":");
274 push_status(status, out);
275 out.extend_from_slice(b",\"title\":\"");
276 out.extend_from_slice(title(status).as_bytes());
277 out.extend_from_slice(b"\",\"message\":\"");
278 out.extend_from_slice(message(status).as_bytes());
279 out.extend_from_slice(b"\",\"reference\":\"");
280 out.extend_from_slice(reference.as_bytes());
282 out.extend_from_slice(b"\"}");
283}
284
285fn push_status(status: u16, out: &mut Vec<u8>) {
286 let s = status.min(999);
287 out.push(b'0' + (s / 100) as u8);
288 out.push(b'0' + (s / 10 % 10) as u8);
289 out.push(b'0' + (s % 10) as u8);
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_status_clamped() {
298 assert_eq!(Error::new(200, "x").status(), 500);
299 assert_eq!(Error::new(302, "x").status(), 500);
300 assert_eq!(Error::new(418, "x").status(), 418);
301 }
302
303 #[test]
304 fn test_builtin_page_has_no_detail() {
305 let mut out = Vec::new();
306 builtin_page(404, "c7-r2", &mut out);
307 let s = String::from_utf8(out).unwrap();
308 assert!(s.contains("<title>404 Not Found</title>"));
309 assert!(s.contains("Reference: c7-r2"));
310 }
311
312 #[test]
313 fn test_build_error_lists_all() {
314 let e = BuildError::new(vec![
315 Problem {
316 subject: "GET /".into(),
317 message: "a".into(),
318 },
319 Problem {
320 subject: "GET /b".into(),
321 message: "b".into(),
322 },
323 ]);
324 assert_eq!(e.problems().len(), 2);
325 assert!(e.to_string().contains("2 problem(s)"));
326 }
327}