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