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("this server does not serve that repository")]
95 NotServed,
96
97 #[error("storage failure: {0}")]
98 Storage(#[from] std::io::Error),
99
100 #[error("could not serialise: {0}")]
101 Serialisation(#[from] serde_json::Error),
102}
103
104const CHALLENGE: HeaderValue = HeaderValue::from_static("Basic realm=\"Git LFS\"");
105
106impl Error {
107 fn status(&self) -> StatusCode {
108 match self {
109 Self::MalformedOid
110 | Self::MalformedLockPath
111 | Self::LockPathTooLong { .. }
112 | Self::MalformedNamespace
113 | Self::OidMismatch { .. }
114 | Self::SizeMismatch { .. }
115 | Self::BatchTooLarge { .. } => StatusCode::UNPROCESSABLE_ENTITY,
116 Self::TooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
117 Self::OverQuota { .. } | Self::LockLimitReached { .. } => {
118 StatusCode::INSUFFICIENT_STORAGE
119 }
120 Self::CompressionDisabled => StatusCode::CONFLICT,
121 Self::NotDecryptable | Self::UnknownKey => StatusCode::INTERNAL_SERVER_ERROR,
124 Self::Tampered => StatusCode::INTERNAL_SERVER_ERROR,
125 Self::Misconfigured(_) => StatusCode::INTERNAL_SERVER_ERROR,
126 Self::Unsupported(_) => StatusCode::NOT_IMPLEMENTED,
127 Self::Unauthenticated => StatusCode::UNAUTHORIZED,
128 Self::Forbidden => StatusCode::FORBIDDEN,
129 Self::LockHeld(_) => StatusCode::CONFLICT,
130 Self::NotFound | Self::LockNotFound | Self::NotServed => StatusCode::NOT_FOUND,
131 Self::Forge => StatusCode::BAD_GATEWAY,
132 Self::RateLimited { .. }
135 | Self::LookupBudgetSpent { .. }
136 | Self::TransfersSaturated { .. } => StatusCode::SERVICE_UNAVAILABLE,
137 Self::Storage(_) | Self::Serialisation(_) => StatusCode::INTERNAL_SERVER_ERROR,
138 }
139 }
140}
141
142impl Error {
143 fn cause(&self) -> &'static str {
144 match self {
145 Self::MalformedOid => "malformed_oid",
146 Self::MalformedNamespace => "malformed_namespace",
147 Self::MalformedLockPath => "malformed_lock_path",
148 Self::LockPathTooLong { .. } => "lock_path_too_long",
149 Self::LockLimitReached { .. } => "lock_limit_reached",
150 Self::OidMismatch { .. } => "oid_mismatch",
151 Self::SizeMismatch { .. } => "size_mismatch",
152 Self::TooLarge { .. } => "too_large",
153 Self::BatchTooLarge { .. } => "batch_too_large",
154 Self::OverQuota { .. } => "over_quota",
155 Self::CompressionDisabled => "compression_disabled",
156 Self::NotDecryptable => "not_decryptable",
157 Self::UnknownKey => "unknown_key",
158 Self::Tampered => "tampered",
159 Self::Misconfigured(_) => "misconfigured",
160 Self::Unsupported(_) => "unsupported",
161 Self::Unauthenticated => "unauthenticated",
162 Self::Forbidden => "forbidden",
163 Self::Forge => "forge_unreachable",
164 Self::RateLimited { .. } => "forge_rate_limited",
167 Self::LookupBudgetSpent { .. } => "lookup_budget_spent",
168 Self::TransfersSaturated { .. } => "transfers_saturated",
169 Self::LockHeld(_) => "lock_held",
170 Self::LockNotFound => "lock_not_found",
171 Self::NotFound => "not_found",
172 Self::NotServed => "not_served",
173 Self::Storage(_) => "storage",
174 Self::Serialisation(_) => "serialisation",
175 }
176 }
177}
178
179impl IntoResponse for Error {
180 fn into_response(self) -> Response {
181 let status = self.status();
182 let cause = crate::metrics::Cause(self.cause());
183
184 if status.is_server_error() {
185 tracing::error!(error = %self, "request failed");
186 }
187
188 if let Self::Storage(_) | Self::Serialisation(_) = &self {
194 let mut response = (
195 status,
196 Json(json!({ "message": "the server could not complete this request" })),
197 )
198 .into_response();
199 response.extensions_mut().insert(cause);
200 return response;
201 }
202
203 if let Self::RateLimited { retry_after }
204 | Self::LookupBudgetSpent { retry_after }
205 | Self::TransfersSaturated { retry_after } = &self
206 {
207 let mut response = (
208 status,
209 [(header::RETRY_AFTER, retry_after.to_string())],
210 Json(json!({ "message": self.to_string() })),
211 )
212 .into_response();
213 response.extensions_mut().insert(cause);
214 return response;
215 }
216
217 if let Self::LockHeld(lock) = &self {
218 let mut response = (
219 status,
220 Json(json!({ "lock": lock, "message": self.to_string() })),
221 )
222 .into_response();
223 response.extensions_mut().insert(cause);
224 return response;
225 }
226
227 let body = Json(json!({ "message": self.to_string() }));
228 let mut response = if status == StatusCode::UNAUTHORIZED {
229 (
230 status,
231 [
232 (header::WWW_AUTHENTICATE, CHALLENGE),
233 (
234 header::HeaderName::from_static("lfs-authenticate"),
235 CHALLENGE,
236 ),
237 ],
238 body,
239 )
240 .into_response()
241 } else {
242 (status, body).into_response()
243 };
244
245 response.extensions_mut().insert(cause);
246 response
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 async fn body_of(error: Error) -> (StatusCode, String) {
255 let response = error.into_response();
256 let status = response.status();
257 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
258 .await
259 .unwrap();
260
261 (status, String::from_utf8(bytes.to_vec()).unwrap())
262 }
263
264 #[tokio::test]
268 async fn a_storage_failure_does_not_quote_the_operating_system() {
269 let inner = std::io::Error::new(
270 std::io::ErrorKind::NotFound,
271 "/var/lib/lfsx/org/repo/ab/cd/abcdef (No such file or directory)",
272 );
273
274 let (status, body) = body_of(Error::Storage(inner)).await;
275
276 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
277 assert!(!body.contains("/var/lib/lfsx"), "{body}");
278 assert!(!body.contains("No such file"), "{body}");
279 assert!(
280 body.contains("the server could not complete this request"),
281 "{body}"
282 );
283 }
284
285 #[tokio::test]
286 async fn a_serialisation_failure_does_not_quote_the_parser() {
287 let inner = serde_json::from_str::<serde_json::Value>("{broken").unwrap_err();
288
289 let (status, body) = body_of(Error::Serialisation(inner)).await;
290
291 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
292 assert!(!body.contains("line 1"), "{body}");
293 assert!(
294 body.contains("the server could not complete this request"),
295 "{body}"
296 );
297 }
298
299 #[tokio::test]
303 async fn a_deployment_refusal_keeps_its_own_words() {
304 let (status, body) = body_of(Error::NotDecryptable).await;
305
306 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
307 assert!(body.contains("LFSX_ENCRYPTION_KEY_FILE"), "{body}");
308 }
309}