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("{0}")]
30 Misconfigured(&'static str),
31
32 #[error("{0}")]
33 Unsupported(&'static str),
34
35 #[error("credentials are required for this repository")]
36 Unauthenticated,
37
38 #[error("these credentials do not grant that access to this repository")]
39 Forbidden,
40
41 #[error("the forge could not be reached to check permissions")]
42 Forge,
43
44 #[error("lock path must not be empty")]
45 MalformedLockPath,
46
47 #[error("the file is already locked")]
48 LockHeld(Box<crate::locks::Lock>),
49
50 #[error("lock not found")]
51 LockNotFound,
52
53 #[error("object not found")]
54 NotFound,
55
56 #[error("storage failure: {0}")]
57 Storage(#[from] std::io::Error),
58
59 #[error("could not serialise: {0}")]
60 Serialisation(#[from] serde_json::Error),
61}
62
63const CHALLENGE: HeaderValue = HeaderValue::from_static("Basic realm=\"Git LFS\"");
64
65impl Error {
66 fn status(&self) -> StatusCode {
67 match self {
68 Self::MalformedOid
69 | Self::MalformedLockPath
70 | Self::MalformedNamespace
71 | Self::OidMismatch { .. }
72 | Self::SizeMismatch { .. } => StatusCode::UNPROCESSABLE_ENTITY,
73 Self::TooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
74 Self::OverQuota { .. } => StatusCode::INSUFFICIENT_STORAGE,
75 Self::CompressionDisabled => StatusCode::CONFLICT,
76 Self::Misconfigured(_) => StatusCode::INTERNAL_SERVER_ERROR,
77 Self::Unsupported(_) => StatusCode::NOT_IMPLEMENTED,
78 Self::Unauthenticated => StatusCode::UNAUTHORIZED,
79 Self::Forbidden => StatusCode::FORBIDDEN,
80 Self::LockHeld(_) => StatusCode::CONFLICT,
81 Self::NotFound | Self::LockNotFound => StatusCode::NOT_FOUND,
82 Self::Forge => StatusCode::BAD_GATEWAY,
83 Self::Storage(_) | Self::Serialisation(_) => StatusCode::INTERNAL_SERVER_ERROR,
84 }
85 }
86}
87
88impl Error {
89 fn cause(&self) -> &'static str {
90 match self {
91 Self::MalformedOid => "malformed_oid",
92 Self::MalformedNamespace => "malformed_namespace",
93 Self::MalformedLockPath => "malformed_lock_path",
94 Self::OidMismatch { .. } => "oid_mismatch",
95 Self::SizeMismatch { .. } => "size_mismatch",
96 Self::TooLarge { .. } => "too_large",
97 Self::OverQuota { .. } => "over_quota",
98 Self::CompressionDisabled => "compression_disabled",
99 Self::Misconfigured(_) => "misconfigured",
100 Self::Unsupported(_) => "unsupported",
101 Self::Unauthenticated => "unauthenticated",
102 Self::Forbidden => "forbidden",
103 Self::Forge => "forge_unreachable",
104 Self::LockHeld(_) => "lock_held",
105 Self::LockNotFound => "lock_not_found",
106 Self::NotFound => "not_found",
107 Self::Storage(_) => "storage",
108 Self::Serialisation(_) => "serialisation",
109 }
110 }
111}
112
113impl IntoResponse for Error {
114 fn into_response(self) -> Response {
115 let status = self.status();
116 let cause = crate::metrics::Cause(self.cause());
117
118 if status.is_server_error() {
119 tracing::error!(error = %self, "request failed");
120 }
121
122 if let Self::LockHeld(lock) = &self {
123 let mut response = (
124 status,
125 Json(json!({ "lock": lock, "message": self.to_string() })),
126 )
127 .into_response();
128 response.extensions_mut().insert(cause);
129 return response;
130 }
131
132 let body = Json(json!({ "message": self.to_string() }));
133 let mut response = if status == StatusCode::UNAUTHORIZED {
134 (
135 status,
136 [
137 (header::WWW_AUTHENTICATE, CHALLENGE),
138 (
139 header::HeaderName::from_static("lfs-authenticate"),
140 CHALLENGE,
141 ),
142 ],
143 body,
144 )
145 .into_response()
146 } else {
147 (status, body).into_response()
148 };
149
150 response.extensions_mut().insert(cause);
151 response
152 }
153}