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("object exceeds the {limit} byte limit this server accepts")]
21    TooLarge { limit: u64 },
22
23    #[error("this repository holds {used} bytes of its {limit} byte budget")]
24    OverQuota { used: u64, limit: u64 },
25
26    #[error("this server does not compress objects — set LFSX_COMPRESSION first")]
27    CompressionDisabled,
28
29    #[error("this object is encrypted and this server holds no key — set LFSX_ENCRYPTION_KEY_FILE")]
30    NotDecryptable,
31
32    #[error("this object was encrypted with a key this server does not hold")]
33    UnknownKey,
34
35    #[error("this object failed its integrity check — the bytes on disk are not what was stored")]
36    Tampered,
37
38    #[error("{0}")]
39    Misconfigured(&'static str),
40
41    #[error("{0}")]
42    Unsupported(&'static str),
43
44    #[error("credentials are required for this repository")]
45    Unauthenticated,
46
47    #[error("these credentials do not grant that access to this repository")]
48    Forbidden,
49
50    #[error("the forge could not be reached to check permissions")]
51    Forge,
52
53    // Distinct from `Forge` on purpose. A throttled forge is not a broken one:
54    // it is working, it has said when to come back, and the answer has to carry
55    // that so a client waits instead of spending the next request on the same
56    // exhausted quota.
57    #[error("the forge is rate-limiting this server — retry in {retry_after} seconds")]
58    RateLimited { retry_after: u64 },
59
60    #[error("lock path must not be empty")]
61    MalformedLockPath,
62
63    #[error("the file is already locked")]
64    LockHeld(Box<crate::locks::Lock>),
65
66    #[error("lock not found")]
67    LockNotFound,
68
69    #[error("object not found")]
70    NotFound,
71
72    #[error("storage failure: {0}")]
73    Storage(#[from] std::io::Error),
74
75    #[error("could not serialise: {0}")]
76    Serialisation(#[from] serde_json::Error),
77}
78
79const CHALLENGE: HeaderValue = HeaderValue::from_static("Basic realm=\"Git LFS\"");
80
81impl Error {
82    fn status(&self) -> StatusCode {
83        match self {
84            Self::MalformedOid
85            | Self::MalformedLockPath
86            | Self::MalformedNamespace
87            | Self::OidMismatch { .. }
88            | Self::SizeMismatch { .. } => StatusCode::UNPROCESSABLE_ENTITY,
89            Self::TooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
90            Self::OverQuota { .. } => StatusCode::INSUFFICIENT_STORAGE,
91            Self::CompressionDisabled => StatusCode::CONFLICT,
92            // The object is there and the request was fine; this server cannot
93            // serve it, which is a fact about the deployment.
94            Self::NotDecryptable | Self::UnknownKey => StatusCode::INTERNAL_SERVER_ERROR,
95            Self::Tampered => StatusCode::INTERNAL_SERVER_ERROR,
96            Self::Misconfigured(_) => StatusCode::INTERNAL_SERVER_ERROR,
97            Self::Unsupported(_) => StatusCode::NOT_IMPLEMENTED,
98            Self::Unauthenticated => StatusCode::UNAUTHORIZED,
99            Self::Forbidden => StatusCode::FORBIDDEN,
100            Self::LockHeld(_) => StatusCode::CONFLICT,
101            Self::NotFound | Self::LockNotFound => StatusCode::NOT_FOUND,
102            Self::Forge => StatusCode::BAD_GATEWAY,
103            // Not 502: a bad gateway invites an immediate retry, which is the
104            // one thing that must not happen here.
105            Self::RateLimited { .. } => StatusCode::SERVICE_UNAVAILABLE,
106            Self::Storage(_) | Self::Serialisation(_) => StatusCode::INTERNAL_SERVER_ERROR,
107        }
108    }
109}
110
111impl Error {
112    fn cause(&self) -> &'static str {
113        match self {
114            Self::MalformedOid => "malformed_oid",
115            Self::MalformedNamespace => "malformed_namespace",
116            Self::MalformedLockPath => "malformed_lock_path",
117            Self::OidMismatch { .. } => "oid_mismatch",
118            Self::SizeMismatch { .. } => "size_mismatch",
119            Self::TooLarge { .. } => "too_large",
120            Self::OverQuota { .. } => "over_quota",
121            Self::CompressionDisabled => "compression_disabled",
122            Self::NotDecryptable => "not_decryptable",
123            Self::UnknownKey => "unknown_key",
124            Self::Tampered => "tampered",
125            Self::Misconfigured(_) => "misconfigured",
126            Self::Unsupported(_) => "unsupported",
127            Self::Unauthenticated => "unauthenticated",
128            Self::Forbidden => "forbidden",
129            Self::Forge => "forge_unreachable",
130            // "the forge is throttling us" and "the forge is broken" are
131            // different afternoons, and sharing one label hides which.
132            Self::RateLimited { .. } => "forge_rate_limited",
133            Self::LockHeld(_) => "lock_held",
134            Self::LockNotFound => "lock_not_found",
135            Self::NotFound => "not_found",
136            Self::Storage(_) => "storage",
137            Self::Serialisation(_) => "serialisation",
138        }
139    }
140}
141
142impl IntoResponse for Error {
143    fn into_response(self) -> Response {
144        let status = self.status();
145        let cause = crate::metrics::Cause(self.cause());
146
147        if status.is_server_error() {
148            tracing::error!(error = %self, "request failed");
149        }
150
151        if let Self::RateLimited { retry_after } = &self {
152            let mut response = (
153                status,
154                [(header::RETRY_AFTER, retry_after.to_string())],
155                Json(json!({ "message": self.to_string() })),
156            )
157                .into_response();
158            response.extensions_mut().insert(cause);
159            return response;
160        }
161
162        if let Self::LockHeld(lock) = &self {
163            let mut response = (
164                status,
165                Json(json!({ "lock": lock, "message": self.to_string() })),
166            )
167                .into_response();
168            response.extensions_mut().insert(cause);
169            return response;
170        }
171
172        let body = Json(json!({ "message": self.to_string() }));
173        let mut response = if status == StatusCode::UNAUTHORIZED {
174            (
175                status,
176                [
177                    (header::WWW_AUTHENTICATE, CHALLENGE),
178                    (
179                        header::HeaderName::from_static("lfs-authenticate"),
180                        CHALLENGE,
181                    ),
182                ],
183                body,
184            )
185                .into_response()
186        } else {
187            (status, body).into_response()
188        };
189
190        response.extensions_mut().insert(cause);
191        response
192    }
193}