Skip to main content

web_net/
error.rs

1//! Errors: what a handler returns, and what a build refuses.
2//!
3//! Every refusal and error becomes a response through one function: the
4//! status, the site's error template for it (or a built-in page that cannot
5//! fail), a generic message, and the request's reference. The code and the
6//! detail go to the events only.
7
8use std::fmt;
9
10type Source = Box<dyn std::error::Error + Send + Sync>;
11
12/// A handler error: a status, a stable code, and an optional source kept for
13/// the logs. The visitor sees the status, a generic message, and the
14/// request's reference; never the code or the source.
15///
16/// ```
17/// use web_net::Error;
18///
19/// let e = Error::not_found("blog.no_post");
20/// assert_eq!((e.status(), e.code()), (404, "blog.no_post"));
21///
22/// let io = std::io::Error::other("disk gone");
23/// let e = Error::internal("blog.read").with_source(io);
24/// assert_eq!(e.status(), 500);
25/// assert!(e.source_ref().is_some());
26/// ```
27pub struct Error {
28    status: u16,
29    code: &'static str,
30    source: Option<Source>,
31}
32
33impl Error {
34    /// An error with `status` (400 to 599; anything else is treated as 500).
35    #[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    /// 400 Bad Request.
50    #[must_use]
51    pub fn bad_request(code: &'static str) -> Self {
52        Self::new(400, code)
53    }
54
55    /// 403 Forbidden.
56    #[must_use]
57    pub fn forbidden(code: &'static str) -> Self {
58        Self::new(403, code)
59    }
60
61    /// 404 Not Found.
62    #[must_use]
63    pub fn not_found(code: &'static str) -> Self {
64        Self::new(404, code)
65    }
66
67    /// 409 Conflict.
68    #[must_use]
69    pub fn conflict(code: &'static str) -> Self {
70        Self::new(409, code)
71    }
72
73    /// 422 Unprocessable Content: the request was understood and refused
74    /// (a form with errors).
75    #[must_use]
76    pub fn unprocessable(code: &'static str) -> Self {
77        Self::new(422, code)
78    }
79
80    /// 500 Internal Server Error: our fault, not the visitor's.
81    #[must_use]
82    pub fn internal(code: &'static str) -> Self {
83        Self::new(500, code)
84    }
85
86    /// 503 Service Unavailable.
87    #[must_use]
88    pub fn unavailable(code: &'static str) -> Self {
89        Self::new(503, code)
90    }
91
92    /// Keeps `source` for the logs.
93    #[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    /// The status.
100    #[must_use]
101    pub const fn status(&self) -> u16 {
102        self.status
103    }
104
105    /// The code.
106    #[must_use]
107    pub const fn code(&self) -> &'static str {
108        self.code
109    }
110
111    /// The source, if one was kept.
112    #[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    /// The form's status (400, 413, 415, 422, or 403 for a token) and code.
154    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
165/// The handler result.
166pub type Result<T = (), E = Error> = std::result::Result<T, E>;
167
168/// One thing wrong with an application's declaration.
169#[non_exhaustive]
170#[derive(Clone, Debug, PartialEq, Eq)]
171pub struct Problem {
172    /// The route or setting it is about (`"GET /admin"`, `"host"`).
173    pub subject: String,
174    /// What is wrong and what to do.
175    pub message: String,
176}
177
178/// Why [`Builder::build`](crate::Builder::build) refused: every problem
179/// found, not only the first.
180#[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    /// The problems, in declaration order.
191    #[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
209/// The generic reason phrase shown for a status.
210pub(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
232/// The generic message shown for a status.
233pub(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
249/// The built-in error page: fixed markup, the status, its title and message,
250/// and the reference. Nothing from the request appears in it except the
251/// reference, which web-net makes itself.
252pub(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
271/// The error reply for a client that prefers JSON: the same facts as the
272/// page, no detail.
273pub(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    // The reference is hex digits, a dash, and digits: nothing to escape.
282    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}