Skip to main content

git_lfs_api/
error.rs

1use serde::{Deserialize, Serialize};
2
3/// The standard error body returned by the LFS server for non-2xx responses.
4///
5/// Defined in `docs/api/batch.md` ยง "Response Errors". The same shape is
6/// reused by the locking endpoints.
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8pub struct ServerError {
9    pub message: String,
10    #[serde(default, skip_serializing_if = "Option::is_none")]
11    pub request_id: Option<String>,
12    #[serde(default, skip_serializing_if = "Option::is_none")]
13    pub documentation_url: Option<String>,
14}
15
16/// Errors returned by the API client.
17#[derive(Debug, thiserror::Error)]
18pub enum ApiError {
19    /// Network / TLS / connection-level failure.
20    #[error("transport error: {0}")]
21    Transport(#[from] reqwest::Error),
22
23    /// Server returned a non-success HTTP status. `body` is `Some` if the
24    /// response had a parseable LFS error body. `lfs_authenticate` mirrors
25    /// the `LFS-Authenticate` response header (only present on 401).
26    #[error("server returned status {status}{}", body.as_ref().map(|b| format!(": {}", b.message)).unwrap_or_default())]
27    Status {
28        status: u16,
29        lfs_authenticate: Option<String>,
30        body: Option<ServerError>,
31    },
32
33    /// JSON body did not match the expected schema.
34    #[error("malformed response body: {0}")]
35    Decode(String),
36
37    /// Failed to construct the request URL from the endpoint.
38    #[error("url error: {0}")]
39    Url(#[from] url::ParseError),
40}
41
42impl ApiError {
43    /// True for 401 responses โ€” caller should resolve credentials and retry.
44    pub fn is_unauthorized(&self) -> bool {
45        matches!(self, ApiError::Status { status: 401, .. })
46    }
47
48    /// True for 403 responses โ€” caller lacks permission for this operation.
49    pub fn is_forbidden(&self) -> bool {
50        matches!(self, ApiError::Status { status: 403, .. })
51    }
52
53    /// True for 404 responses.
54    pub fn is_not_found(&self) -> bool {
55        matches!(self, ApiError::Status { status: 404, .. })
56    }
57
58    /// True for 5xx and 408/429 โ€” transient errors a caller may want to retry.
59    pub fn is_retryable(&self) -> bool {
60        matches!(
61            self,
62            ApiError::Transport(_)
63                | ApiError::Status {
64                    status: 408 | 429 | 500..=599,
65                    ..
66                }
67        )
68    }
69}