webserver-base 0.2.4

A Rust library which contains shared logic for all of my webserver projects.
Documentation
//! The web server's error type.

use std::net::SocketAddr;

use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use tracing::error;

use crate::env::EnvError;

/// Every way the web server can fail.
///
/// Composed from the per-kit errors rather than replacing them: a crate on only
/// `telegram` sees [`TelegramError`](crate::telegram::TelegramError) and never
/// this type. Deliberately not `#[non_exhaustive]`.
#[derive(thiserror::Error)]
pub enum WebServerError {
    /// A required environment variable was missing or malformed.
    #[error(transparent)]
    Env(#[from] EnvError),

    /// The listener could not bind.
    #[error("failed to bind to {addr}")]
    Bind {
        addr: SocketAddr,
        #[source]
        source: std::io::Error,
    },

    /// The browser Sentry DSN could not be parsed.
    #[cfg(feature = "analytics")]
    #[error("the browser Sentry DSN is malformed")]
    SentryDsn(#[from] crate::analytics::SentryDsnParseError),

    /// The server stopped with an error rather than a shutdown.
    #[error("the server stopped unexpectedly")]
    Serve(#[source] std::io::Error),

    /// A template could not be loaded or rendered.
    #[cfg(feature = "templates")]
    #[error(transparent)]
    Template(#[from] crate::templates::TemplateError),

    /// A render was attempted without `.templates(..)` having been called.
    #[cfg(feature = "templates")]
    #[error("cannot render: the server was built without `.templates(..)`")]
    TemplatesNotConfigured,

    /// The asset cache could not be built.
    #[cfg(feature = "webserver")]
    #[error(transparent)]
    CacheBuster(#[from] crate::assets::CacheBusterError),

    /// The sitemap could not be written.
    #[cfg(feature = "sitemap")]
    #[error(transparent)]
    Sitemap(#[from] crate::sitemap::SitemapError),

    /// A feed could not be built.
    #[cfg(feature = "feed")]
    #[error(transparent)]
    Feed(#[from] crate::feed::FeedError),

    /// `.feed(..)` was called on a server that is not a frontend.
    ///
    /// A boot failure rather than a shrug: the feed would simply not be served,
    /// the autodiscovery links would not be emitted, and nothing at runtime
    /// would ever say so.
    #[cfg(feature = "feed")]
    #[error("cannot serve a feed: the server was built without `.frontend(..)`")]
    FeedWithoutFrontend,

    /// Observability could not be configured.
    #[cfg(feature = "observability")]
    #[error(transparent)]
    Observability(#[from] crate::observability::ObservabilityError),

    /// Pages were declared without the template data needed to render or list
    /// them.
    #[cfg(feature = "pages")]
    #[error("`.pages(..)` requires `.templates(..)`: the sitemap needs the site's base url")]
    PagesRequireTemplates,

    /// A parameterised path was declared as a single page. `/blog/{slug}`
    /// matches many URLs, so it cannot be one sitemap entry.
    #[cfg(feature = "pages")]
    #[error(
        "page path `{path}` contains a route parameter; \
         use `dynamic_page_group` with concrete urls, or mark it unlisted"
    )]
    DynamicPagePathHasParameters { path: String },
}

impl IntoResponse for WebServerError {
    /// The body says nothing beyond "internal error"; the detail goes to the log.
    fn into_response(self) -> Response {
        error!("request failed: {self}");
        (StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response()
    }
}

/// Renders the whole error chain, not the enum's shape.
///
/// `main` returning a `Result` prints its error with `Debug`, so a derived one
/// would surface `CacheBuster(AmbiguousFavicon)` — the variant name — and throw
/// away the sentence that says what to do about it. Every message in this crate
/// is written for a human who has just had a boot fail; this is what makes them
/// visible.
impl std::fmt::Debug for WebServerError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "{self}")?;

        let mut source: Option<&(dyn std::error::Error + 'static)> =
            std::error::Error::source(self);
        while let Some(error) = source {
            writeln!(f, "  caused by: {error}")?;
            source = error.source();
        }
        Ok(())
    }
}