Skip to main content

ferro_cargo_registry_server/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Cargo registry protocol errors.
3//!
4//! Cargo clients consume JSON on error; the registry API §"Errors"
5//! defines the envelope as `{ "errors": [{ "detail": "..." }] }`.
6//! Reference: <https://doc.rust-lang.org/cargo/reference/registry-web-api.html#errors>.
7
8use axum::http::StatusCode;
9use axum::response::{IntoResponse, Response};
10use ferro_blob_store::BlobStoreError;
11use serde_json::json;
12
13/// Errors raised by the Cargo registry protocol.
14#[derive(Debug, thiserror::Error)]
15pub enum CargoError {
16    /// A crate name failed validation (registry §"Crate name
17    /// restrictions").
18    #[error("invalid crate name: {0}")]
19    InvalidName(String),
20
21    /// A version string is not legal semver.
22    #[error("invalid semver: {0}")]
23    InvalidVersion(String),
24
25    /// The publish request body is malformed (LE-length / JSON /
26    /// tarball mismatch).
27    #[error("invalid publish payload: {0}")]
28    InvalidPublish(String),
29
30    /// The declared `cksum` did not match the tarball SHA-256.
31    #[error("checksum mismatch: declared {declared}, computed {computed}")]
32    ChecksumMismatch {
33        /// Client-declared SHA-256 hex.
34        declared: String,
35        /// Server-computed SHA-256 hex.
36        computed: String,
37    },
38
39    /// The requested resource does not exist.
40    #[error("not found: {0}")]
41    NotFound(String),
42
43    /// Feature not yet implemented in this phase (Git index).
44    #[error("not implemented: {0}")]
45    NotImplemented(String),
46
47    /// Underlying blob-store error (I/O, digest mismatch, missing blob).
48    #[error(transparent)]
49    Storage(#[from] BlobStoreError),
50}
51
52impl CargoError {
53    /// HTTP status code for this error.
54    #[must_use]
55    pub fn status(&self) -> StatusCode {
56        match self {
57            Self::InvalidName(_)
58            | Self::InvalidVersion(_)
59            | Self::InvalidPublish(_)
60            | Self::ChecksumMismatch { .. } => StatusCode::BAD_REQUEST,
61            Self::NotFound(_) => StatusCode::NOT_FOUND,
62            Self::NotImplemented(_) => StatusCode::NOT_IMPLEMENTED,
63            Self::Storage(err) => storage_status(err),
64        }
65    }
66}
67
68fn storage_status(err: &BlobStoreError) -> StatusCode {
69    match err {
70        BlobStoreError::NotFound(_) => StatusCode::NOT_FOUND,
71        BlobStoreError::DigestMismatch { .. } | BlobStoreError::InvalidDigest(_) => {
72            StatusCode::BAD_REQUEST
73        }
74        BlobStoreError::Io(_) => StatusCode::INTERNAL_SERVER_ERROR,
75        _ => StatusCode::INTERNAL_SERVER_ERROR,
76    }
77}
78
79impl IntoResponse for CargoError {
80    fn into_response(self) -> Response {
81        let status = self.status();
82        let body = json!({
83            "errors": [{ "detail": self.to_string() }]
84        });
85        (status, axum::Json(body)).into_response()
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::CargoError;
92    use axum::http::StatusCode;
93
94    #[test]
95    fn invalid_name_is_400() {
96        assert_eq!(
97            CargoError::InvalidName(String::new()).status(),
98            StatusCode::BAD_REQUEST
99        );
100    }
101
102    #[test]
103    fn not_found_is_404() {
104        assert_eq!(
105            CargoError::NotFound("x".into()).status(),
106            StatusCode::NOT_FOUND
107        );
108    }
109
110    #[test]
111    fn not_implemented_is_501() {
112        assert_eq!(
113            CargoError::NotImplemented("git".into()).status(),
114            StatusCode::NOT_IMPLEMENTED
115        );
116    }
117
118    #[test]
119    fn checksum_mismatch_is_400() {
120        let e = CargoError::ChecksumMismatch {
121            declared: "a".into(),
122            computed: "b".into(),
123        };
124        assert_eq!(e.status(), StatusCode::BAD_REQUEST);
125    }
126}