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#[derive(Clone, Debug, PartialEq, Eq)]
170pub struct Problem {
171    /// The route or setting it is about (`"GET /admin"`, `"host"`).
172    pub subject: String,
173    /// What is wrong and what to do.
174    pub message: String,
175}
176
177/// Why [`Builder::build`](crate::Builder::build) refused: every problem
178/// found, not only the first.
179#[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    /// The problems, in declaration order.
190    #[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
208/// The generic reason phrase shown for a status.
209pub(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
231/// The generic message shown for a status.
232pub(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
248/// The built-in error page: fixed markup, the status, its title and message,
249/// and the reference. Nothing from the request appears in it except the
250/// reference, which web-net makes itself.
251pub(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
270fn push_status(status: u16, out: &mut Vec<u8>) {
271    let s = status.min(999);
272    out.push(b'0' + (s / 100) as u8);
273    out.push(b'0' + (s / 10 % 10) as u8);
274    out.push(b'0' + (s % 10) as u8);
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_status_clamped() {
283        assert_eq!(Error::new(200, "x").status(), 500);
284        assert_eq!(Error::new(302, "x").status(), 500);
285        assert_eq!(Error::new(418, "x").status(), 418);
286    }
287
288    #[test]
289    fn test_builtin_page_has_no_detail() {
290        let mut out = Vec::new();
291        builtin_page(404, "c7-r2", &mut out);
292        let s = String::from_utf8(out).unwrap();
293        assert!(s.contains("<title>404 Not Found</title>"));
294        assert!(s.contains("Reference: c7-r2"));
295    }
296
297    #[test]
298    fn test_build_error_lists_all() {
299        let e = BuildError::new(vec![
300            Problem {
301                subject: "GET /".into(),
302                message: "a".into(),
303            },
304            Problem {
305                subject: "GET /b".into(),
306                message: "b".into(),
307            },
308        ]);
309        assert_eq!(e.problems().len(), 2);
310        assert!(e.to_string().contains("2 problem(s)"));
311    }
312}