Skip to main content

lfsx_server/
error.rs

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            // The object is there and the request was fine; this server cannot
86            // serve it, which is a fact about the deployment.
87            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}