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(
24 "a batch asks about {asked} objects, and this server answers at most {limit} at a time"
25 )]
26 BatchTooLarge { asked: usize, limit: usize },
27
28 #[error("this repository holds {used} bytes of its {limit} byte budget")]
29 OverQuota { used: u64, limit: u64 },
30
31 #[error("this server does not compress objects: set LFSX_COMPRESSION first")]
32 CompressionDisabled,
33
34 #[error("this object is encrypted and this server holds no key: set LFSX_ENCRYPTION_KEY_FILE")]
35 NotDecryptable,
36
37 #[error("this object was encrypted with a key this server does not hold")]
38 UnknownKey,
39
40 #[error("this object failed its integrity check: the bytes on disk are not what was stored")]
41 Tampered,
42
43 #[error("{0}")]
44 Misconfigured(&'static str),
45
46 #[error("{0}")]
47 Unsupported(&'static str),
48
49 #[error("credentials are required for this repository")]
50 Unauthenticated,
51
52 #[error("these credentials do not grant that access to this repository")]
53 Forbidden,
54
55 #[error("the forge could not be reached to check permissions")]
56 Forge,
57
58 #[error("the forge is rate-limiting this server: retry in {retry_after} seconds")]
63 RateLimited { retry_after: u64 },
64
65 #[error("this server is not asking the forge again yet, retry in {retry_after} seconds")]
71 LookupBudgetSpent { retry_after: u64 },
72
73 #[error("this server is at its concurrent transfer limit, retry in {retry_after} seconds")]
74 TransfersSaturated { retry_after: u64 },
75
76 #[error("lock path must not be empty")]
77 MalformedLockPath,
78
79 #[error("lock path is {actual} bytes, and this server accepts at most {limit}")]
80 LockPathTooLong { actual: usize, limit: usize },
81
82 #[error("this repository already holds {limit} locks, and this server refuses to add more")]
83 LockLimitReached { limit: usize },
84
85 #[error("the file is already locked")]
86 LockHeld(Box<crate::locks::Lock>),
87
88 #[error("lock not found")]
89 LockNotFound,
90
91 #[error("object not found")]
92 NotFound,
93
94 #[error("storage failure: {0}")]
95 Storage(#[from] std::io::Error),
96
97 #[error("could not serialise: {0}")]
98 Serialisation(#[from] serde_json::Error),
99}
100
101const CHALLENGE: HeaderValue = HeaderValue::from_static("Basic realm=\"Git LFS\"");
102
103impl Error {
104 fn status(&self) -> StatusCode {
105 match self {
106 Self::MalformedOid
107 | Self::MalformedLockPath
108 | Self::LockPathTooLong { .. }
109 | Self::MalformedNamespace
110 | Self::OidMismatch { .. }
111 | Self::SizeMismatch { .. }
112 | Self::BatchTooLarge { .. } => StatusCode::UNPROCESSABLE_ENTITY,
113 Self::TooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
114 Self::OverQuota { .. } | Self::LockLimitReached { .. } => {
115 StatusCode::INSUFFICIENT_STORAGE
116 }
117 Self::CompressionDisabled => StatusCode::CONFLICT,
118 Self::NotDecryptable | Self::UnknownKey => StatusCode::INTERNAL_SERVER_ERROR,
121 Self::Tampered => StatusCode::INTERNAL_SERVER_ERROR,
122 Self::Misconfigured(_) => StatusCode::INTERNAL_SERVER_ERROR,
123 Self::Unsupported(_) => StatusCode::NOT_IMPLEMENTED,
124 Self::Unauthenticated => StatusCode::UNAUTHORIZED,
125 Self::Forbidden => StatusCode::FORBIDDEN,
126 Self::LockHeld(_) => StatusCode::CONFLICT,
127 Self::NotFound | Self::LockNotFound => StatusCode::NOT_FOUND,
128 Self::Forge => StatusCode::BAD_GATEWAY,
129 Self::RateLimited { .. }
132 | Self::LookupBudgetSpent { .. }
133 | Self::TransfersSaturated { .. } => StatusCode::SERVICE_UNAVAILABLE,
134 Self::Storage(_) | Self::Serialisation(_) => StatusCode::INTERNAL_SERVER_ERROR,
135 }
136 }
137}
138
139impl Error {
140 fn cause(&self) -> &'static str {
141 match self {
142 Self::MalformedOid => "malformed_oid",
143 Self::MalformedNamespace => "malformed_namespace",
144 Self::MalformedLockPath => "malformed_lock_path",
145 Self::LockPathTooLong { .. } => "lock_path_too_long",
146 Self::LockLimitReached { .. } => "lock_limit_reached",
147 Self::OidMismatch { .. } => "oid_mismatch",
148 Self::SizeMismatch { .. } => "size_mismatch",
149 Self::TooLarge { .. } => "too_large",
150 Self::BatchTooLarge { .. } => "batch_too_large",
151 Self::OverQuota { .. } => "over_quota",
152 Self::CompressionDisabled => "compression_disabled",
153 Self::NotDecryptable => "not_decryptable",
154 Self::UnknownKey => "unknown_key",
155 Self::Tampered => "tampered",
156 Self::Misconfigured(_) => "misconfigured",
157 Self::Unsupported(_) => "unsupported",
158 Self::Unauthenticated => "unauthenticated",
159 Self::Forbidden => "forbidden",
160 Self::Forge => "forge_unreachable",
161 Self::RateLimited { .. } => "forge_rate_limited",
164 Self::LookupBudgetSpent { .. } => "lookup_budget_spent",
165 Self::TransfersSaturated { .. } => "transfers_saturated",
166 Self::LockHeld(_) => "lock_held",
167 Self::LockNotFound => "lock_not_found",
168 Self::NotFound => "not_found",
169 Self::Storage(_) => "storage",
170 Self::Serialisation(_) => "serialisation",
171 }
172 }
173}
174
175impl IntoResponse for Error {
176 fn into_response(self) -> Response {
177 let status = self.status();
178 let cause = crate::metrics::Cause(self.cause());
179
180 if status.is_server_error() {
181 tracing::error!(error = %self, "request failed");
182 }
183
184 if let Self::Storage(_) | Self::Serialisation(_) = &self {
190 let mut response = (
191 status,
192 Json(json!({ "message": "the server could not complete this request" })),
193 )
194 .into_response();
195 response.extensions_mut().insert(cause);
196 return response;
197 }
198
199 if let Self::RateLimited { retry_after }
200 | Self::LookupBudgetSpent { retry_after }
201 | Self::TransfersSaturated { retry_after } = &self
202 {
203 let mut response = (
204 status,
205 [(header::RETRY_AFTER, retry_after.to_string())],
206 Json(json!({ "message": self.to_string() })),
207 )
208 .into_response();
209 response.extensions_mut().insert(cause);
210 return response;
211 }
212
213 if let Self::LockHeld(lock) = &self {
214 let mut response = (
215 status,
216 Json(json!({ "lock": lock, "message": self.to_string() })),
217 )
218 .into_response();
219 response.extensions_mut().insert(cause);
220 return response;
221 }
222
223 let body = Json(json!({ "message": self.to_string() }));
224 let mut response = if status == StatusCode::UNAUTHORIZED {
225 (
226 status,
227 [
228 (header::WWW_AUTHENTICATE, CHALLENGE),
229 (
230 header::HeaderName::from_static("lfs-authenticate"),
231 CHALLENGE,
232 ),
233 ],
234 body,
235 )
236 .into_response()
237 } else {
238 (status, body).into_response()
239 };
240
241 response.extensions_mut().insert(cause);
242 response
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 async fn body_of(error: Error) -> (StatusCode, String) {
251 let response = error.into_response();
252 let status = response.status();
253 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
254 .await
255 .unwrap();
256
257 (status, String::from_utf8(bytes.to_vec()).unwrap())
258 }
259
260 #[tokio::test]
264 async fn a_storage_failure_does_not_quote_the_operating_system() {
265 let inner = std::io::Error::new(
266 std::io::ErrorKind::NotFound,
267 "/var/lib/lfsx/org/repo/ab/cd/abcdef (No such file or directory)",
268 );
269
270 let (status, body) = body_of(Error::Storage(inner)).await;
271
272 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
273 assert!(!body.contains("/var/lib/lfsx"), "{body}");
274 assert!(!body.contains("No such file"), "{body}");
275 assert!(
276 body.contains("the server could not complete this request"),
277 "{body}"
278 );
279 }
280
281 #[tokio::test]
282 async fn a_serialisation_failure_does_not_quote_the_parser() {
283 let inner = serde_json::from_str::<serde_json::Value>("{broken").unwrap_err();
284
285 let (status, body) = body_of(Error::Serialisation(inner)).await;
286
287 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
288 assert!(!body.contains("line 1"), "{body}");
289 assert!(
290 body.contains("the server could not complete this request"),
291 "{body}"
292 );
293 }
294
295 #[tokio::test]
299 async fn a_deployment_refusal_keeps_its_own_words() {
300 let (status, body) = body_of(Error::NotDecryptable).await;
301
302 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
303 assert!(body.contains("LFSX_ENCRYPTION_KEY_FILE"), "{body}");
304 }
305}