solid_pod_rs_git/error.rs
1//! Typed error enum for the Git HTTP backend.
2//!
3//! Mirrors the failure shapes produced by JSS `src/handlers/git.js`:
4//! 400 on path traversal, 401 on missing/invalid auth, 404 on unknown
5//! repo, 500 on CGI spawn failure, and a dedicated variant for when
6//! the `git-http-backend` binary is not installed (so CI without git
7//! can still exercise unit tests).
8
9use thiserror::Error;
10
11/// All failure modes the Git HTTP service may surface.
12#[derive(Debug, Error)]
13pub enum GitError {
14 /// Path-traversal attempt or otherwise invalid URL. JSS returns
15 /// 400 for malformed requests and 403 for traversal; we fold both
16 /// into a single 400 per Rust idiom (the leakage surface is
17 /// identical — the client must not learn whether a repo exists
18 /// via the status code).
19 #[error("path traversal or invalid path: {0}")]
20 PathTraversal(String),
21
22 /// Authorisation required but missing / malformed / rejected.
23 #[error("unauthorised: {0}")]
24 Unauthorised(String),
25
26 /// The request targets a path that is not a git repository.
27 #[error("not a git repository: {0}")]
28 NotARepository(String),
29
30 /// `git-http-backend` (or `git`) binary not installed in PATH.
31 /// Distinguished from a generic I/O error so callers can gate
32 /// integration tests.
33 #[error("git-http-backend binary not available: {0}")]
34 BackendNotAvailable(String),
35
36 /// The CGI process exited non-zero before emitting headers, or was
37 /// killed after exceeding the execution timeout (`exit_code: None`).
38 #[error("git backend failed: exit={exit_code:?}, stderr={stderr}")]
39 BackendFailed {
40 /// Process exit code, if the child did terminate.
41 exit_code: Option<i32>,
42 /// Captured stderr content for diagnostics.
43 stderr: String,
44 },
45
46 /// The CGI process emitted malformed output.
47 #[error("malformed CGI output: {0}")]
48 MalformedCgi(String),
49
50 /// Generic underlying I/O failure.
51 #[error("i/o: {0}")]
52 Io(#[from] std::io::Error),
53
54 /// A push was refused by the pre-receive path guard before any of it was
55 /// applied. The message names a path the client itself supplied, never
56 /// anything about the server's state.
57 #[error("push refused: {0}")]
58 PushRefused(String),
59
60 /// Underlying auth-layer error (NIP-98 decode, Schnorr mismatch, …).
61 #[error("auth: {0}")]
62 Auth(#[from] crate::auth::AuthError),
63}
64
65impl GitError {
66 /// Recommended HTTP status code to surface to the client.
67 #[must_use]
68 pub fn status_code(&self) -> u16 {
69 match self {
70 GitError::PathTraversal(_) => 400,
71 GitError::Unauthorised(_) | GitError::Auth(_) => 401,
72 GitError::PushRefused(_) => 403,
73 GitError::NotARepository(_) => 404,
74 GitError::BackendNotAvailable(_)
75 | GitError::BackendFailed { .. }
76 | GitError::MalformedCgi(_)
77 | GitError::Io(_) => 500,
78 }
79 }
80}