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("lock path must not be empty")]
74 MalformedLockPath,
75
76 #[error("the file is already locked")]
77 LockHeld(Box<crate::locks::Lock>),
78
79 #[error("lock not found")]
80 LockNotFound,
81
82 #[error("object not found")]
83 NotFound,
84
85 #[error("storage failure: {0}")]
86 Storage(#[from] std::io::Error),
87
88 #[error("could not serialise: {0}")]
89 Serialisation(#[from] serde_json::Error),
90}
91
92const CHALLENGE: HeaderValue = HeaderValue::from_static("Basic realm=\"Git LFS\"");
93
94impl Error {
95 fn status(&self) -> StatusCode {
96 match self {
97 Self::MalformedOid
98 | Self::MalformedLockPath
99 | Self::MalformedNamespace
100 | Self::OidMismatch { .. }
101 | Self::SizeMismatch { .. }
102 | Self::BatchTooLarge { .. } => StatusCode::UNPROCESSABLE_ENTITY,
103 Self::TooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
104 Self::OverQuota { .. } => StatusCode::INSUFFICIENT_STORAGE,
105 Self::CompressionDisabled => StatusCode::CONFLICT,
106 Self::NotDecryptable | Self::UnknownKey => StatusCode::INTERNAL_SERVER_ERROR,
109 Self::Tampered => StatusCode::INTERNAL_SERVER_ERROR,
110 Self::Misconfigured(_) => StatusCode::INTERNAL_SERVER_ERROR,
111 Self::Unsupported(_) => StatusCode::NOT_IMPLEMENTED,
112 Self::Unauthenticated => StatusCode::UNAUTHORIZED,
113 Self::Forbidden => StatusCode::FORBIDDEN,
114 Self::LockHeld(_) => StatusCode::CONFLICT,
115 Self::NotFound | Self::LockNotFound => StatusCode::NOT_FOUND,
116 Self::Forge => StatusCode::BAD_GATEWAY,
117 Self::RateLimited { .. } | Self::LookupBudgetSpent { .. } => {
120 StatusCode::SERVICE_UNAVAILABLE
121 }
122 Self::Storage(_) | Self::Serialisation(_) => StatusCode::INTERNAL_SERVER_ERROR,
123 }
124 }
125}
126
127impl Error {
128 fn cause(&self) -> &'static str {
129 match self {
130 Self::MalformedOid => "malformed_oid",
131 Self::MalformedNamespace => "malformed_namespace",
132 Self::MalformedLockPath => "malformed_lock_path",
133 Self::OidMismatch { .. } => "oid_mismatch",
134 Self::SizeMismatch { .. } => "size_mismatch",
135 Self::TooLarge { .. } => "too_large",
136 Self::BatchTooLarge { .. } => "batch_too_large",
137 Self::OverQuota { .. } => "over_quota",
138 Self::CompressionDisabled => "compression_disabled",
139 Self::NotDecryptable => "not_decryptable",
140 Self::UnknownKey => "unknown_key",
141 Self::Tampered => "tampered",
142 Self::Misconfigured(_) => "misconfigured",
143 Self::Unsupported(_) => "unsupported",
144 Self::Unauthenticated => "unauthenticated",
145 Self::Forbidden => "forbidden",
146 Self::Forge => "forge_unreachable",
147 Self::RateLimited { .. } => "forge_rate_limited",
150 Self::LookupBudgetSpent { .. } => "lookup_budget_spent",
151 Self::LockHeld(_) => "lock_held",
152 Self::LockNotFound => "lock_not_found",
153 Self::NotFound => "not_found",
154 Self::Storage(_) => "storage",
155 Self::Serialisation(_) => "serialisation",
156 }
157 }
158}
159
160impl IntoResponse for Error {
161 fn into_response(self) -> Response {
162 let status = self.status();
163 let cause = crate::metrics::Cause(self.cause());
164
165 if status.is_server_error() {
166 tracing::error!(error = %self, "request failed");
167 }
168
169 if let Self::RateLimited { retry_after } | Self::LookupBudgetSpent { retry_after } = &self {
170 let mut response = (
171 status,
172 [(header::RETRY_AFTER, retry_after.to_string())],
173 Json(json!({ "message": self.to_string() })),
174 )
175 .into_response();
176 response.extensions_mut().insert(cause);
177 return response;
178 }
179
180 if let Self::LockHeld(lock) = &self {
181 let mut response = (
182 status,
183 Json(json!({ "lock": lock, "message": self.to_string() })),
184 )
185 .into_response();
186 response.extensions_mut().insert(cause);
187 return response;
188 }
189
190 let body = Json(json!({ "message": self.to_string() }));
191 let mut response = if status == StatusCode::UNAUTHORIZED {
192 (
193 status,
194 [
195 (header::WWW_AUTHENTICATE, CHALLENGE),
196 (
197 header::HeaderName::from_static("lfs-authenticate"),
198 CHALLENGE,
199 ),
200 ],
201 body,
202 )
203 .into_response()
204 } else {
205 (status, body).into_response()
206 };
207
208 response.extensions_mut().insert(cause);
209 response
210 }
211}