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<crate::RenderError> for Error {
153    fn from(e: crate::RenderError) -> Self {
154        Self::internal(e.code()).with_source(e)
155    }
156}
157
158/// The handler result.
159pub type Result<T = (), E = Error> = std::result::Result<T, E>;
160
161/// One thing wrong with an application's declaration.
162#[derive(Clone, Debug, PartialEq, Eq)]
163pub struct Problem {
164    /// The route or setting it is about (`"GET /admin"`, `"host"`).
165    pub subject: String,
166    /// What is wrong and what to do.
167    pub message: String,
168}
169
170/// Why [`Builder::build`](crate::Builder::build) refused: every problem
171/// found, not only the first.
172#[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    /// The problems, in declaration order.
183    #[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
201/// The generic reason phrase shown for a status.
202pub(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
224/// The generic message shown for a status.
225pub(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
241/// The built-in error page: fixed markup, the status, its title and message,
242/// and the reference. Nothing from the request appears in it except the
243/// reference, which web-net makes itself.
244pub(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}