dioxus_fullstack/payloads/
cbor.rs

1use axum::{
2    body::Bytes,
3    extract::{rejection::BytesRejection, FromRequest, Request},
4    http::{header, HeaderMap, HeaderValue, StatusCode},
5    response::{IntoResponse, Response},
6};
7use serde::{de::DeserializeOwned, Serialize};
8
9/// CBOR Extractor / Response.
10///
11/// When used as an extractor, it can deserialize request bodies into some type that
12/// implements [`serde::Deserialize`]. The request will be rejected (and a [`CborRejection`] will
13/// be returned) if:
14///
15/// - The request doesn't have a `Content-Type: application/cbor` (or similar) header.
16/// - The body doesn't contain syntactically valid CBOR.
17/// - The body contains syntactically valid CBOR but it couldn't be deserialized into the target type.
18/// - Buffering the request body fails.
19///
20/// ⚠️ Since parsing CBOR requires consuming the request body, the `Cbor` extractor must be
21/// *last* if there are multiple extractors in a handler.
22/// See ["the order of extractors"][order-of-extractors]
23///
24/// [order-of-extractors]: mod@crate::extract#the-order-of-extractors
25#[must_use]
26pub struct Cbor<T>(pub T);
27
28/// Check if the request has a valid CBOR content type header.
29///
30/// This function validates that the `Content-Type` header is set to `application/cbor`
31/// or a compatible CBOR media type (including subtypes with `+cbor` suffix).
32fn is_valid_cbor_content_type(headers: &HeaderMap) -> bool {
33    let Some(content_type) = headers.get(header::CONTENT_TYPE) else {
34        return false;
35    };
36
37    let Ok(content_type) = content_type.to_str() else {
38        return false;
39    };
40
41    let Ok(mime) = content_type.parse::<mime::Mime>() else {
42        return false;
43    };
44
45    let is_cbor_content_type = mime.type_() == "application"
46        && (mime.subtype() == "cbor" || mime.suffix().is_some_and(|name| name == "cbor"));
47
48    is_cbor_content_type
49}
50
51impl<S, T> FromRequest<S> for Cbor<T>
52where
53    S: Send + Sync,
54    T: DeserializeOwned,
55{
56    type Rejection = CborRejection;
57
58    /// Extract a CBOR payload from the request body.
59    ///
60    /// This implementation validates the content type and deserializes the CBOR data.
61    /// Returns a `CborRejection` if validation or deserialization fails.
62    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
63        if !is_valid_cbor_content_type(req.headers()) {
64            return Err(CborRejection::MissingCborContentType);
65        }
66        let bytes = Bytes::from_request(req, state).await?;
67        let value =
68            ciborium::from_reader(&bytes as &[u8]).map_err(|_| CborRejection::FailedToParseCbor)?;
69        Ok(Cbor(value))
70    }
71}
72
73impl<T> IntoResponse for Cbor<T>
74where
75    T: Serialize,
76{
77    /// Convert the CBOR payload into an HTTP response.
78    ///
79    /// This serializes the inner value to CBOR format and sets the appropriate
80    /// `Content-Type: application/cbor` header. Returns a 500 Internal Server Error
81    /// if serialization fails.
82    fn into_response(self) -> Response {
83        let mut buf = Vec::new();
84        match ciborium::into_writer(&self.0, &mut buf) {
85            Err(_) => (
86                StatusCode::INTERNAL_SERVER_ERROR,
87                [(
88                    header::CONTENT_TYPE,
89                    HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
90                )],
91                "Failed to serialize to CBOR".to_string(),
92            )
93                .into_response(),
94            Ok(()) => (
95                [(
96                    header::CONTENT_TYPE,
97                    HeaderValue::from_static("application/cbor"),
98                )],
99                buf,
100            )
101                .into_response(),
102        }
103    }
104}
105
106impl<T> From<T> for Cbor<T> {
107    /// Create a `Cbor<T>` from the inner value.
108    ///
109    /// This is a convenience constructor that wraps any value in the `Cbor` struct.
110    fn from(inner: T) -> Self {
111        Self(inner)
112    }
113}
114
115impl<T> Cbor<T>
116where
117    T: DeserializeOwned,
118{
119    /// Construct a `Cbor<T>` from a byte slice.
120    ///
121    /// This method attempts to deserialize the provided bytes as CBOR data.
122    /// Returns a `CborRejection` if deserialization fails.
123    pub fn from_bytes(bytes: &[u8]) -> Result<Self, CborRejection> {
124        ciborium::de::from_reader(bytes)
125            .map(Cbor)
126            .map_err(|_| CborRejection::FailedToParseCbor)
127    }
128}
129
130/// Rejection type for CBOR extraction failures.
131///
132/// This enum represents the various ways that CBOR extraction can fail.
133/// It implements `IntoResponse` to provide appropriate HTTP responses for each error type.
134#[derive(thiserror::Error, Debug)]
135pub enum CborRejection {
136    /// The request is missing the required `Content-Type: application/cbor` header.
137    #[error("Expected request with `Content-Type: application/cbor`")]
138    MissingCborContentType,
139
140    /// Failed to parse the request body as valid CBOR.
141    #[error("Invalid CBOR data")]
142    FailedToParseCbor,
143
144    /// Failed to read the request body bytes.
145    #[error(transparent)]
146    BytesRejection(#[from] BytesRejection),
147}
148
149impl IntoResponse for CborRejection {
150    fn into_response(self) -> Response {
151        use CborRejection::*;
152        match self {
153            MissingCborContentType => {
154                (StatusCode::UNSUPPORTED_MEDIA_TYPE, self.to_string()).into_response()
155            }
156            FailedToParseCbor => (StatusCode::BAD_REQUEST, self.to_string()).into_response(),
157            BytesRejection(rejection) => rejection.into_response(),
158        }
159    }
160}