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("{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}