Skip to main content

webserver_base/webserver/
error.rs

1//! The web server's error type.
2
3use std::net::SocketAddr;
4
5use axum::http::StatusCode;
6use axum::response::{IntoResponse, Response};
7use tracing::error;
8
9use crate::env::EnvError;
10
11/// Every way the web server can fail.
12///
13/// Composed from the per-kit errors rather than replacing them: a crate on only
14/// `telegram` sees [`TelegramError`](crate::telegram::TelegramError) and never
15/// this type. Deliberately not `#[non_exhaustive]`.
16#[derive(thiserror::Error)]
17pub enum WebServerError {
18    /// A required environment variable was missing or malformed.
19    #[error(transparent)]
20    Env(#[from] EnvError),
21
22    /// The listener could not bind.
23    #[error("failed to bind to {addr}")]
24    Bind {
25        addr: SocketAddr,
26        #[source]
27        source: std::io::Error,
28    },
29
30    /// The browser Sentry DSN could not be parsed.
31    #[cfg(feature = "analytics")]
32    #[error("the browser Sentry DSN is malformed")]
33    SentryDsn(#[from] crate::analytics::SentryDsnParseError),
34
35    /// The server stopped with an error rather than a shutdown.
36    #[error("the server stopped unexpectedly")]
37    Serve(#[source] std::io::Error),
38
39    /// A template could not be loaded or rendered.
40    #[cfg(feature = "templates")]
41    #[error(transparent)]
42    Template(#[from] crate::templates::TemplateError),
43
44    /// A render was attempted without `.templates(..)` having been called.
45    #[cfg(feature = "templates")]
46    #[error("cannot render: the server was built without `.templates(..)`")]
47    TemplatesNotConfigured,
48
49    /// The asset cache could not be built.
50    #[cfg(feature = "webserver")]
51    #[error(transparent)]
52    CacheBuster(#[from] crate::assets::CacheBusterError),
53
54    /// The sitemap could not be written.
55    #[cfg(feature = "sitemap")]
56    #[error(transparent)]
57    Sitemap(#[from] crate::sitemap::SitemapError),
58
59    /// A feed could not be built.
60    #[cfg(feature = "feed")]
61    #[error(transparent)]
62    Feed(#[from] crate::feed::FeedError),
63
64    /// `.feed(..)` was called on a server that is not a frontend.
65    ///
66    /// A boot failure rather than a shrug: the feed would simply not be served,
67    /// the autodiscovery links would not be emitted, and nothing at runtime
68    /// would ever say so.
69    #[cfg(feature = "feed")]
70    #[error("cannot serve a feed: the server was built without `.frontend(..)`")]
71    FeedWithoutFrontend,
72
73    /// Observability could not be configured.
74    #[cfg(feature = "observability")]
75    #[error(transparent)]
76    Observability(#[from] crate::observability::ObservabilityError),
77
78    /// Pages were declared without the template data needed to render or list
79    /// them.
80    #[cfg(feature = "pages")]
81    #[error("`.pages(..)` requires `.templates(..)`: the sitemap needs the site's base url")]
82    PagesRequireTemplates,
83
84    /// A parameterised path was declared as a single page. `/blog/{slug}`
85    /// matches many URLs, so it cannot be one sitemap entry.
86    #[cfg(feature = "pages")]
87    #[error(
88        "page path `{path}` contains a route parameter; \
89         use `dynamic_page_group` with concrete urls, or mark it unlisted"
90    )]
91    DynamicPagePathHasParameters { path: String },
92}
93
94impl IntoResponse for WebServerError {
95    /// The body says nothing beyond "internal error"; the detail goes to the log.
96    fn into_response(self) -> Response {
97        error!("request failed: {self}");
98        (StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response()
99    }
100}
101
102/// Renders the whole error chain, not the enum's shape.
103///
104/// `main` returning a `Result` prints its error with `Debug`, so a derived one
105/// would surface `CacheBuster(AmbiguousFavicon)` — the variant name — and throw
106/// away the sentence that says what to do about it. Every message in this crate
107/// is written for a human who has just had a boot fail; this is what makes them
108/// visible.
109impl std::fmt::Debug for WebServerError {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        writeln!(f, "{self}")?;
112
113        let mut source: Option<&(dyn std::error::Error + 'static)> =
114            std::error::Error::source(self);
115        while let Some(error) = source {
116            writeln!(f, "  caused by: {error}")?;
117            source = error.source();
118        }
119        Ok(())
120    }
121}