dioxus_fullstack/payloads/cbor.rs
1use axum::{
2 body::Bytes,
3 extract::{FromRequest, Request, rejection::BytesRejection},
4 http::{HeaderMap, HeaderValue, StatusCode, header},
5 response::{IntoResponse, Response},
6};
7use serde::{Serialize, de::DeserializeOwned};
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 mime.type_() == "application"
46 && (mime.subtype() == "cbor" || mime.suffix().is_some_and(|name| name == "cbor"))
47}
48
49impl<S, T> FromRequest<S> for Cbor<T>
50where
51 S: Send + Sync,
52 T: DeserializeOwned,
53{
54 type Rejection = CborRejection;
55
56 /// Extract a CBOR payload from the request body.
57 ///
58 /// This implementation validates the content type and deserializes the CBOR data.
59 /// Returns a `CborRejection` if validation or deserialization fails.
60 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
61 if !is_valid_cbor_content_type(req.headers()) {
62 return Err(CborRejection::MissingCborContentType);
63 }
64 let bytes = Bytes::from_request(req, state).await?;
65 let value =
66 ciborium::from_reader(&bytes as &[u8]).map_err(|_| CborRejection::FailedToParseCbor)?;
67 Ok(Cbor(value))
68 }
69}
70
71impl<T> IntoResponse for Cbor<T>
72where
73 T: Serialize,
74{
75 /// Convert the CBOR payload into an HTTP response.
76 ///
77 /// This serializes the inner value to CBOR format and sets the appropriate
78 /// `Content-Type: application/cbor` header. Returns a 500 Internal Server Error
79 /// if serialization fails.
80 fn into_response(self) -> Response {
81 let mut buf = Vec::new();
82 match ciborium::into_writer(&self.0, &mut buf) {
83 Err(_) => (
84 StatusCode::INTERNAL_SERVER_ERROR,
85 [(
86 header::CONTENT_TYPE,
87 HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
88 )],
89 "Failed to serialize to CBOR".to_string(),
90 )
91 .into_response(),
92 Ok(()) => (
93 [(
94 header::CONTENT_TYPE,
95 HeaderValue::from_static("application/cbor"),
96 )],
97 buf,
98 )
99 .into_response(),
100 }
101 }
102}
103
104impl<T> From<T> for Cbor<T> {
105 /// Create a `Cbor<T>` from the inner value.
106 ///
107 /// This is a convenience constructor that wraps any value in the `Cbor` struct.
108 fn from(inner: T) -> Self {
109 Self(inner)
110 }
111}
112
113impl<T> Cbor<T>
114where
115 T: DeserializeOwned,
116{
117 /// Construct a `Cbor<T>` from a byte slice.
118 ///
119 /// This method attempts to deserialize the provided bytes as CBOR data.
120 /// Returns a `CborRejection` if deserialization fails.
121 pub fn from_bytes(bytes: &[u8]) -> Result<Self, CborRejection> {
122 ciborium::de::from_reader(bytes)
123 .map(Cbor)
124 .map_err(|_| CborRejection::FailedToParseCbor)
125 }
126}
127
128/// Rejection type for CBOR extraction failures.
129///
130/// This enum represents the various ways that CBOR extraction can fail.
131/// It implements `IntoResponse` to provide appropriate HTTP responses for each error type.
132#[derive(thiserror::Error, Debug)]
133pub enum CborRejection {
134 /// The request is missing the required `Content-Type: application/cbor` header.
135 #[error("Expected request with `Content-Type: application/cbor`")]
136 MissingCborContentType,
137
138 /// Failed to parse the request body as valid CBOR.
139 #[error("Invalid CBOR data")]
140 FailedToParseCbor,
141
142 /// Failed to read the request body bytes.
143 #[error(transparent)]
144 BytesRejection(#[from] BytesRejection),
145}
146
147impl IntoResponse for CborRejection {
148 fn into_response(self) -> Response {
149 use CborRejection::*;
150 match self {
151 MissingCborContentType => {
152 (StatusCode::UNSUPPORTED_MEDIA_TYPE, self.to_string()).into_response()
153 }
154 FailedToParseCbor => (StatusCode::BAD_REQUEST, self.to_string()).into_response(),
155 BytesRejection(rejection) => rejection.into_response(),
156 }
157 }
158}