Parent [>]
1//! Application error types and HTTP response rendering.
2//!
3//! Defines the unified [`Error`] enum used across all handlers and the
4//! [`Result`] type alias.  `Error` implements [`IntoResponse`] via askama's
5//! `error.html` template.
6
7use std::sync::{Arc, OnceLock};
8
9use askama::Template;
10use axum::http::{StatusCode, header};
11use axum::response::{IntoResponse, Response};
12use thiserror::Error;
13
14use crate::config::SiteConfig;
15
16/// Unified error type for the application.
17///
18/// Each variant maps to an HTTP status code and a rendered error page.
19#[derive(Debug, Error)]
20pub enum Error {
21    #[error(transparent)]
22    Io(#[from] std::io::Error),
23    #[error("repository not found: {0}")]
24    RepoNotFound(String),
25    #[error("not found: {0}")]
26    NotFound(String),
27    #[error("bad request: {0}")]
28    BadRequest(String),
29    #[error("corrupt object: {0}")]
30    GitCorrupt(String),
31}
32
33/// Convenience alias for `std::result::Result<T, Error>`.
34pub type Result<T> = std::result::Result<T, Error>;
35
36/// Module-level [`SiteConfig`] used only by the error template renderer.
37///
38/// Set once during startup in [`init_site`].  This exists because
39/// [`Error::into_response`] does not have access to [`axum::extract::State`]
40/// — it runs after the handler has returned.  All other code paths read from
41/// [`AppState`](crate::routes::AppState).
42static SITE: OnceLock<Arc<SiteConfig>> = OnceLock::new();
43
44/// Stores the [`SiteConfig`] for use by the error template.
45///
46/// Called once from `main` before the server starts.
47pub fn init_site(site: Arc<SiteConfig>) {
48    let _ = SITE.set(site);
49}
50
51fn site() -> Arc<SiteConfig> {
52    SITE.get()
53        .cloned()
54        .unwrap_or_else(|| {
55            Arc::new(SiteConfig {
56                site_name: crate::PKG_NAME.to_owned(),
57                hostname: String::new(),
58                ssh_prefix: String::new(),
59                base_url: String::new(),
60                favicon: Vec::new(),
61                favicon_mime: "image/svg+xml",
62            })
63        })
64}
65
66#[derive(Template)]
67#[template(path = "error.html")]
68struct ErrorTemplate {
69    status: u16,
70    kind: &'static str,
71    message: String,
72    site: Arc<SiteConfig>,
73}
74
75impl IntoResponse for Error {
76    fn into_response(self) -> Response {
77        let is_5xx: bool;
78        let (status, kind) = match &self {
79            Self::RepoNotFound(_) | Self::NotFound(_) => {
80                is_5xx = false;
81                (StatusCode::NOT_FOUND, "Not Found")
82            }
83            Self::BadRequest(_) => {
84                is_5xx = false;
85                (StatusCode::BAD_REQUEST, "Bad Request")
86            }
87            Self::Io(_) | Self::GitCorrupt(_) => {
88                is_5xx = true;
89                tracing::error!(error = %self, "internal error");
90                (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error")
91            }
92        };
93
94        let message = if is_5xx {
95            "An internal error occurred.".into()
96        } else {
97            self.to_string()
98        };
99
100        let tpl = ErrorTemplate {
101            status: status.as_u16(),
102            kind,
103            message,
104            site: site(),
105        };
106
107        tpl.render().map_or_else(
108            |_| {
109                let fallback = if is_5xx {
110                    "Internal Server Error".into()
111                } else {
112                    self.to_string()
113                };
114                (status, fallback).into_response()
115            },
116            |html| {
117                (
118                    status,
119                    [(header::CONTENT_TYPE, "text/html; charset=utf-8")],
120                    html,
121                )
122                    .into_response()
123            },
124        )
125    }
126}