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 #[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 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 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 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}