wayver's git archive


a simple self-hosted git server
git clone https://git.wayver.dev/bile

Commit: 3aec1a1bb245ce255f59532eba2bdfe1d2879510 (tree)
Parent: 7e0330303eec590c92eb6194f83f04d0e8fb2ea0 (tree)
Author: wayverd
Date: 2026 M02 20, Fri 13:35:08 -0500
15 files changed; 108 insertions 49 deletions
custom path extractor for better error handling

diff --git a/src/handlers/git.rs b/src/handlers/git.rs
index c593a78..1b31eb4 100644
--- a/src/handlers/git.rs
+++ b/src/handlers/git.rs
@@ -1,5 +1,5 @@
 use axum::{
-    extract::{Path, State},
+    extract::State,
     http::{HeaderValue, StatusCode, Uri, header},
     response::{IntoResponse as _, Response},
 };
@@ -10,6 +10,7 @@ use crate::{
     git::Repository,
     http::{
         extractor::RepoName,
+        path::Path,
         response::{ErrorPage, Result},
     },
 };
@@ -39,7 +40,7 @@ pub(crate) async fn get_2(
 fn inner(state: &BileState, uri: &Uri, repo_name: &RepoName) -> Result<Response> {
     let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
     else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
@@ -53,7 +54,7 @@ fn inner(state: &BileState, uri: &Uri, repo_name: &RepoName) -> Result<Response>
 
     // cant canonicalize if it doesnt exist
     if !path.exists() {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     }
@@ -63,7 +64,7 @@ fn inner(state: &BileState, uri: &Uri, repo_name: &RepoName) -> Result<Response>
     // that path got us outside of the repository structure somehow
     if !path.starts_with(repo.path()) {
         tracing::warn!("Attempt to acces file outside of repo dir: {:?}", path);
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::FORBIDDEN)
             .into_response());
     }
@@ -71,7 +72,7 @@ fn inner(state: &BileState, uri: &Uri, repo_name: &RepoName) -> Result<Response>
     // Either the requested resource does not exist or it is not
     // a file, i.e. a directory.
     if !path.is_file() {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     }
diff --git a/src/handlers/repo_commit.rs b/src/handlers/repo_commit.rs
index 823424d..f32701d 100644
--- a/src/handlers/repo_commit.rs
+++ b/src/handlers/repo_commit.rs
@@ -1,7 +1,7 @@
 use std::fmt::Write as _;
 
 use axum::{
-    extract::{Path, State},
+    extract::State,
     http::StatusCode,
     response::{IntoResponse as _, Response},
 };
@@ -19,6 +19,7 @@ use crate::{
     git::Repository,
     http::{
         extractor::{Commit, RepoName},
+        path::Path,
         response::{ErrorPage, Html, Result},
     },
     utils::filters,
@@ -141,13 +142,13 @@ pub(crate) async fn get(
 fn inner(state: &BileState, repo_name: &RepoName, commit: &Commit) -> Result<Response> {
     let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
     else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
 
     let Some(commit) = repo.commit(&commit.0).context("failed to get commit")? else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
diff --git a/src/handlers/repo_file.rs b/src/handlers/repo_file.rs
index e83fe92..e103430 100644
--- a/src/handlers/repo_file.rs
+++ b/src/handlers/repo_file.rs
@@ -1,7 +1,7 @@
 use std::{fmt::Write as _, path};
 
 use axum::{
-    extract::{Path, State},
+    extract::State,
     http::StatusCode,
     response::{IntoResponse as _, Response},
 };
@@ -18,6 +18,7 @@ use crate::{
     git::Repository,
     http::{
         extractor::{ObjectName, Ref, RepoName},
+        path::Path,
         response::{ErrorPage, Html, Redirect, Result},
     },
     utils::{blob_mime, filters},
@@ -80,7 +81,7 @@ fn inner(
 ) -> Result<Response> {
     let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
     else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
@@ -96,7 +97,7 @@ fn inner(
         .commit_tree(&spec)
         .context("failed to get commit tree")?
     else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
@@ -110,13 +111,13 @@ fn inner(
     };
 
     let Some(tree_obj) = tree_obj else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
 
     let Some(last_commit) = repo.file_last_commit(&spec, path)? else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
@@ -139,7 +140,7 @@ fn inner(
     };
 
     let Some(blob) = tree_obj.as_blob() else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
diff --git a/src/handlers/repo_file_raw.rs b/src/handlers/repo_file_raw.rs
index 9072050..c173e5e 100644
--- a/src/handlers/repo_file_raw.rs
+++ b/src/handlers/repo_file_raw.rs
@@ -1,7 +1,7 @@
 use std::path;
 
 use axum::{
-    extract::{Path, State},
+    extract::State,
     http::{StatusCode, header},
     response::{IntoResponse as _, Response},
 };
@@ -12,6 +12,7 @@ use crate::{
     git::Repository,
     http::{
         extractor::{ObjectName, Ref, RepoName},
+        path::Path,
         response::{ErrorPage, Result},
     },
     utils::blob_mime,
@@ -37,14 +38,14 @@ fn inner(
     let repo = match Repository::open(&state.config, repo_name).context("opening repository") {
         Ok(Some(repo)) => repo,
         Ok(None) => {
-            return Ok(ErrorPage::new(&state.config)
+            return Ok(ErrorPage::from(state)
                 .with_status(StatusCode::NOT_FOUND)
                 .into_response());
         }
         Err(err) => {
             tracing::error!(err=?err, "failed to open repository");
 
-            return Ok(ErrorPage::new(&state.config)
+            return Ok(ErrorPage::from(state)
                 .with_status(StatusCode::NOT_FOUND)
                 .into_response());
         }
@@ -56,13 +57,13 @@ fn inner(
         .commit_tree(&r#ref.0)
         .context("failed to get commit tree")?
     else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
 
     let Some(blob) = repo.tree_blob(&tree, path)? else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
diff --git a/src/handlers/repo_home.rs b/src/handlers/repo_home.rs
index 503b851..5ccb446 100644
--- a/src/handlers/repo_home.rs
+++ b/src/handlers/repo_home.rs
@@ -1,5 +1,5 @@
 use axum::{
-    extract::{Path, State},
+    extract::State,
     http::StatusCode,
     response::{IntoResponse as _, Response},
 };
@@ -11,6 +11,7 @@ use crate::{
     git::Repository,
     http::{
         extractor::RepoName,
+        path::Path,
         response::{ErrorPage, Html, Result},
     },
     utils::filters,
@@ -34,7 +35,7 @@ pub(crate) async fn get(state: State<BileState>, Path(repo_name): Path<RepoName>
 fn inner(state: &BileState, repo_name: &RepoName) -> Result<Response> {
     let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
     else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
@@ -44,7 +45,7 @@ fn inner(state: &BileState, repo_name: &RepoName) -> Result<Response> {
     // TODO: let r = req.param("ref").unwrap_or("HEAD");
     let r = "HEAD";
     let Some(commits) = repo.commits(r, 3)? else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
diff --git a/src/handlers/repo_log.rs b/src/handlers/repo_log.rs
index d41fb39..b90cbb6 100644
--- a/src/handlers/repo_log.rs
+++ b/src/handlers/repo_log.rs
@@ -1,5 +1,5 @@
 use axum::{
-    extract::{Path, State},
+    extract::State,
     http::StatusCode,
     response::{IntoResponse as _, Response},
 };
@@ -11,6 +11,7 @@ use crate::{
     git::Repository,
     http::{
         extractor::{ObjectName, Ref, RepoName},
+        path::Path,
         response::{ErrorPage, Html, Redirect, Result},
     },
     utils::filters,
@@ -62,7 +63,7 @@ fn inner(
 ) -> Result<Response> {
     let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
     else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
@@ -90,7 +91,7 @@ fn inner(
         .commits_for_obj(r, state.config.log_per_page + 1, object_name)
         .context("failed to get commits for object")?
     else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
diff --git a/src/handlers/repo_log_feed.rs b/src/handlers/repo_log_feed.rs
index cd306d8..86f67bf 100644
--- a/src/handlers/repo_log_feed.rs
+++ b/src/handlers/repo_log_feed.rs
@@ -1,5 +1,5 @@
 use axum::{
-    extract::{Path, State},
+    extract::State,
     http::StatusCode,
     response::{IntoResponse as _, Response},
 };
@@ -10,6 +10,7 @@ use crate::{
     git::Repository,
     http::{
         extractor::{Ref, RepoName},
+        path::Path,
         response::{ErrorPage, Result, Xml},
     },
     utils::filters,
@@ -44,14 +45,14 @@ pub(crate) async fn get_2(
 fn inner(state: &BileState, repo_name: &RepoName, r#ref: Option<&Ref>) -> Result<Response> {
     let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
     else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
 
     if repo.is_empty()? {
         // show a server error
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::SERVICE_UNAVAILABLE)
             .into_response());
     }
@@ -59,7 +60,7 @@ fn inner(state: &BileState, repo_name: &RepoName, r#ref: Option<&Ref>) -> Result
     let r = r#ref.map_or("HEAD", |r| r.0.as_str());
 
     let Some(commits) = repo.commits(r, state.config.log_per_page)? else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
diff --git a/src/handlers/repo_refs.rs b/src/handlers/repo_refs.rs
index efff748..55f823f 100644
--- a/src/handlers/repo_refs.rs
+++ b/src/handlers/repo_refs.rs
@@ -1,5 +1,5 @@
 use axum::{
-    extract::{Path, State},
+    extract::State,
     http::StatusCode,
     response::{IntoResponse as _, Response},
 };
@@ -11,6 +11,7 @@ use crate::{
     git::{Repository, TagEntry},
     http::{
         extractor::RepoName,
+        path::Path,
         response::{ErrorPage, Html, Redirect, Result},
     },
     utils::filters,
@@ -34,7 +35,7 @@ pub(crate) async fn get(state: State<BileState>, Path(repo_name): Path<RepoName>
 fn inner(state: &BileState, repo_name: &RepoName) -> Result<Response> {
     let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
     else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
diff --git a/src/handlers/repo_refs_feed.rs b/src/handlers/repo_refs_feed.rs
index 203658a..3f072a7 100644
--- a/src/handlers/repo_refs_feed.rs
+++ b/src/handlers/repo_refs_feed.rs
@@ -1,5 +1,5 @@
 use axum::{
-    extract::{Path, State},
+    extract::State,
     http::StatusCode,
     response::{IntoResponse as _, Response},
 };
@@ -10,6 +10,7 @@ use crate::{
     git::{Repository, TagEntry},
     http::{
         extractor::RepoName,
+        path::Path,
         response::{ErrorPage, Result, Xml},
     },
     utils::filters,
@@ -32,14 +33,14 @@ pub(crate) async fn get(state: State<BileState>, Path(repo_name): Path<RepoName>
 fn inner(state: &BileState, repo_name: &RepoName) -> Result<Response> {
     let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
     else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
 
     if repo.is_empty()? {
         // show a server error
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::SERVICE_UNAVAILABLE)
             .into_response());
     }
diff --git a/src/handlers/repo_tag.rs b/src/handlers/repo_tag.rs
index d78a4ca..ef8643d 100644
--- a/src/handlers/repo_tag.rs
+++ b/src/handlers/repo_tag.rs
@@ -1,5 +1,5 @@
 use axum::{
-    extract::{Path, State},
+    extract::State,
     http::StatusCode,
     response::{IntoResponse, Response},
 };
@@ -11,6 +11,7 @@ use crate::{
     git::Repository,
     http::{
         extractor::{RepoName, Tag},
+        path::Path,
         response::{ErrorPage, Html, Redirect, Result},
     },
     utils::filters,
@@ -38,7 +39,7 @@ pub(crate) async fn get(
 fn inner(state: &BileState, repo_name: &RepoName, tag: &Tag) -> Result<Response> {
     let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
     else {
-        return Ok(ErrorPage::new(&state.config)
+        return Ok(ErrorPage::from(state)
             .with_status(StatusCode::NOT_FOUND)
             .into_response());
     };
diff --git a/src/http/mod.rs b/src/http/mod.rs
index 83e27db..19d0b35 100644
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -1,4 +1,5 @@
 pub(crate) mod extractor;
+pub(crate) mod path;
 pub(crate) mod response;
 
 use std::sync::Arc;
@@ -31,7 +32,7 @@ impl BileState {
 
         let this = self.clone();
 
-        spawn_blocking(move || span.in_scope(|| wrap_err(&this.config.clone(), f(this)))).await
+        spawn_blocking(move || span.in_scope(|| wrap_err(this.clone(), f(this)))).await
     }
 }
 
@@ -47,13 +48,13 @@ where
         .expect("failed to join spawn_blocking call, this should only happen due to a panic")
 }
 
-fn wrap_err(config: &Config, res: Result<Response>) -> Response {
+fn wrap_err(state: BileState, res: Result<Response>) -> Response {
     match res {
         Ok(res) => res,
         Err(err) => {
             tracing::error!(err=?err, "failed to handle response");
 
-            ErrorPage::new(config)
+            ErrorPage::from(state)
                 .with_status(StatusCode::INTERNAL_SERVER_ERROR)
                 .into_response()
         }
diff --git a/src/http/path.rs b/src/http/path.rs
new file mode 100644
index 0000000..6ecda99
--- /dev/null
+++ b/src/http/path.rs
@@ -0,0 +1,34 @@
+use axum::extract::{FromRequestParts, path::ErrorKind, rejection::PathRejection};
+use http::{StatusCode, request::Parts};
+use serde::de::DeserializeOwned;
+
+use crate::http::{BileState, response::ErrorPage};
+
+pub(crate) struct Path<T>(pub T);
+
+impl<T> FromRequestParts<BileState> for Path<T>
+where
+    T: DeserializeOwned + Send,
+{
+    type Rejection = ErrorPage;
+
+    async fn from_request_parts(
+        parts: &mut Parts,
+        state: &BileState,
+    ) -> Result<Self, Self::Rejection> {
+        match axum::extract::Path::<T>::from_request_parts(parts, state).await {
+            Ok(value) => Ok(Self(value.0)),
+            Err(rejection) => match rejection {
+                PathRejection::FailedToDeserializePathParams(inner) => {
+                    let status = match inner.kind() {
+                        ErrorKind::UnsupportedType { .. } => StatusCode::INTERNAL_SERVER_ERROR,
+                        _ => StatusCode::BAD_REQUEST,
+                    };
+
+                    Err(ErrorPage::from(state).with_status(status))
+                }
+                _ => Err(ErrorPage::from(state).with_status(StatusCode::INTERNAL_SERVER_ERROR)),
+            },
+        }
+    }
+}
diff --git a/src/http/response.rs b/src/http/response.rs
index f4d8530..9ccc45b 100644
--- a/src/http/response.rs
+++ b/src/http/response.rs
@@ -1,9 +1,11 @@
+use std::sync::Arc;
+
 use axum::{
     http::{HeaderValue, StatusCode, header},
     response::{IntoResponse, Response},
 };
 
-use crate::config::Config;
+use crate::{config::Config, http::BileState};
 
 pub(crate) type Result<T = Response, E = crate::error::Error> = std::result::Result<T, E>;
 
@@ -247,28 +249,39 @@ impl IntoResponse for Redirect {
 
 #[derive(askama::Template)]
 #[template(path = "error.html")]
-pub(crate) struct ErrorPage<'c> {
-    config: &'c Config,
+pub(crate) struct ErrorPage {
+    config: Arc<Config>,
     status: StatusCode,
 }
 
-impl<'c> ErrorPage<'c> {
-    pub(crate) const fn new(config: &'c Config) -> Self {
+impl ErrorPage {
+    pub(crate) fn with_status(self, status: StatusCode) -> Self {
+        Self {
+            config: self.config,
+            status,
+        }
+    }
+}
+
+impl From<BileState> for ErrorPage {
+    fn from(value: BileState) -> Self {
         Self {
-            config,
+            config: value.config,
             status: StatusCode::INTERNAL_SERVER_ERROR,
         }
     }
+}
 
-    pub(crate) const fn with_status(self, status: StatusCode) -> Self {
+impl<'s> From<&'s BileState> for ErrorPage {
+    fn from(value: &'s BileState) -> Self {
         Self {
-            config: self.config,
-            status,
+            config: value.config.clone(),
+            status: StatusCode::INTERNAL_SERVER_ERROR,
         }
     }
 }
 
-impl IntoResponse for ErrorPage<'_> {
+impl IntoResponse for ErrorPage {
     fn into_response(self) -> Response {
         (self.status, Html(self)).into_response()
     }
diff --git a/src/utils/cache.rs b/src/utils/cache.rs
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/utils/cache.rs
@@ -0,0 +1 @@
+
diff --git a/src/utils/cache.rs b/src/utils/cache.rs
deleted file mode 100644
index e69de29..0000000
--- a/src/utils/cache.rs
+++ /dev/null