Skip to main content

lfsx_server/
error.rs

1use axum::Json;
2use axum::http::{HeaderValue, StatusCode, header};
3use axum::response::{IntoResponse, Response};
4use serde_json::json;
5
6#[derive(Debug, thiserror::Error)]
7pub enum Error {
8    #[error("object id is not a lowercase hex sha256 digest")]
9    MalformedOid,
10
11    #[error("organisation and repository must be plain names")]
12    MalformedNamespace,
13
14    #[error("content hashes to {actual}, which does not match the declared object id {declared}")]
15    OidMismatch { declared: String, actual: String },
16
17    #[error("content is {actual} bytes, but {declared} were declared")]
18    SizeMismatch { declared: u64, actual: u64 },
19
20    #[error("credentials are required for this repository")]
21    Unauthenticated,
22
23    #[error("these credentials do not grant that access to this repository")]
24    Forbidden,
25
26    #[error("the forge could not be reached to check permissions")]
27    Forge,
28
29    #[error("lock path must not be empty")]
30    MalformedLockPath,
31
32    #[error("the file is already locked")]
33    LockHeld(Box<crate::locks::Lock>),
34
35    #[error("lock not found")]
36    LockNotFound,
37
38    #[error("object not found")]
39    NotFound,
40
41    #[error("storage failure: {0}")]
42    Storage(#[from] std::io::Error),
43
44    #[error("could not serialise: {0}")]
45    Serialisation(#[from] serde_json::Error),
46}
47
48const CHALLENGE: HeaderValue = HeaderValue::from_static("Basic realm=\"Git LFS\"");
49
50impl Error {
51    fn status(&self) -> StatusCode {
52        match self {
53            Self::MalformedOid
54            | Self::MalformedLockPath
55            | Self::MalformedNamespace
56            | Self::OidMismatch { .. }
57            | Self::SizeMismatch { .. } => StatusCode::UNPROCESSABLE_ENTITY,
58            Self::Unauthenticated => StatusCode::UNAUTHORIZED,
59            Self::Forbidden => StatusCode::FORBIDDEN,
60            Self::LockHeld(_) => StatusCode::CONFLICT,
61            Self::NotFound | Self::LockNotFound => StatusCode::NOT_FOUND,
62            Self::Forge => StatusCode::BAD_GATEWAY,
63            Self::Storage(_) | Self::Serialisation(_) => StatusCode::INTERNAL_SERVER_ERROR,
64        }
65    }
66}
67
68impl Error {
69    fn cause(&self) -> &'static str {
70        match self {
71            Self::MalformedOid => "malformed_oid",
72            Self::MalformedNamespace => "malformed_namespace",
73            Self::MalformedLockPath => "malformed_lock_path",
74            Self::OidMismatch { .. } => "oid_mismatch",
75            Self::SizeMismatch { .. } => "size_mismatch",
76            Self::Unauthenticated => "unauthenticated",
77            Self::Forbidden => "forbidden",
78            Self::Forge => "forge_unreachable",
79            Self::LockHeld(_) => "lock_held",
80            Self::LockNotFound => "lock_not_found",
81            Self::NotFound => "not_found",
82            Self::Storage(_) => "storage",
83            Self::Serialisation(_) => "serialisation",
84        }
85    }
86}
87
88impl IntoResponse for Error {
89    fn into_response(self) -> Response {
90        let status = self.status();
91        let cause = crate::metrics::Cause(self.cause());
92
93        if status.is_server_error() {
94            tracing::error!(error = %self, "request failed");
95        }
96
97        if let Self::LockHeld(lock) = &self {
98            let mut response = (
99                status,
100                Json(json!({ "lock": lock, "message": self.to_string() })),
101            )
102                .into_response();
103            response.extensions_mut().insert(cause);
104            return response;
105        }
106
107        let body = Json(json!({ "message": self.to_string() }));
108        let mut response = if status == StatusCode::UNAUTHORIZED {
109            (
110                status,
111                [
112                    (header::WWW_AUTHENTICATE, CHALLENGE),
113                    (
114                        header::HeaderName::from_static("lfs-authenticate"),
115                        CHALLENGE,
116                    ),
117                ],
118                body,
119            )
120                .into_response()
121        } else {
122            (status, body).into_response()
123        };
124
125        response.extensions_mut().insert(cause);
126        response
127    }
128}