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