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