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