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 askama::Template;
8use axum::http::{StatusCode, header};
9use axum::response::{IntoResponse, Response};
10use thiserror::Error;
11
12/// Unified error type for the application.
13///
14/// Each variant maps to an HTTP status code and a rendered error page.
15#[derive(Debug, Error)]
16pub enum Error {
17    #[error(transparent)]
18    Io(#[from] std::io::Error),
19    #[error("repository not found: {0}")]
20    RepoNotFound(String),
21    #[error("not found: {0}")]
22    NotFound(String),
23    #[error("bad request: {0}")]
24    BadRequest(String),
25    #[error("corrupt object: {0}")]
26    GitCorrupt(String),
27}
28
29/// Convenience alias for `std::result::Result<T, Error>`.
30pub type Result<T> = std::result::Result<T, Error>;
31
32#[derive(Template)]
33#[template(path = "error.html")]
34struct ErrorTemplate {
35    status: u16,
36    kind: &'static str,
37    message: String,
38    base_url: String,
39}
40
41impl IntoResponse for Error {
42    fn into_response(self) -> Response {
43        let is_5xx: bool;
44        let (status, kind) = match &self {
45            Self::RepoNotFound(_) | Self::NotFound(_) => {
46                is_5xx = false;
47                (StatusCode::NOT_FOUND, "Not Found")
48            }
49            Self::BadRequest(_) => {
50                is_5xx = false;
51                (StatusCode::BAD_REQUEST, "Bad Request")
52            }
53            Self::Io(_) | Self::GitCorrupt(_) => {
54                is_5xx = true;
55                tracing::error!(error = %self, "internal error");
56                (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error")
57            }
58        };
59
60        let message = if is_5xx {
61            "An internal error occurred.".into()
62        } else {
63            self.to_string()
64        };
65
66        let tpl = ErrorTemplate {
67            status: status.as_u16(),
68            kind,
69            message,
70            base_url: crate::base_url().to_string(),
71        };
72
73        tpl.render().map_or_else(
74            |_| {
75                let fallback = if is_5xx {
76                    "Internal Server Error".into()
77                } else {
78                    self.to_string()
79                };
80                (status, fallback).into_response()
81            },
82            |html| (status, [(header::CONTENT_TYPE, "text/html; charset=utf-8")], html).into_response(),
83        )
84    }
85}