rama_http/service/web/endpoint/extract/body/
octet_stream.rs1use super::BytesRejection;
2use crate::Request;
3use crate::body::util::BodyExt;
4use crate::service::web::extract::{FromRequest, OptionalFromRequest};
5use crate::utils::macros::{composite_http_rejection, define_http_rejection};
6use rama_core::bytes::Bytes;
7use rama_http_types::{HeaderMap, header};
8use rama_utils::macros::impl_deref;
9
10#[derive(Debug, Clone)]
28pub struct OctetStream(pub Bytes);
29
30impl_deref!(OctetStream: Bytes);
31
32impl From<Bytes> for OctetStream {
33 fn from(value: Bytes) -> Self {
34 Self(value)
35 }
36}
37
38impl From<OctetStream> for Bytes {
39 fn from(value: OctetStream) -> Self {
40 value.0
41 }
42}
43
44define_http_rejection! {
45 #[status = UNSUPPORTED_MEDIA_TYPE]
46 #[body = "OctetStream requests must have `Content-Type: application/octet-stream` (or no Content-Type)"]
47 pub struct InvalidOctetStreamContentType;
51}
52
53composite_http_rejection! {
54 pub enum OctetStreamRejection {
59 InvalidOctetStreamContentType,
60 BytesRejection,
61 }
62}
63
64impl FromRequest for OctetStream {
65 type Rejection = OctetStreamRejection;
66
67 async fn from_request(req: Request) -> Result<Self, Self::Rejection> {
68 match content_type_match(req.headers()) {
69 ContentTypeMatch::Match | ContentTypeMatch::Absent => {}
70 ContentTypeMatch::Mismatch => return Err(InvalidOctetStreamContentType.into()),
71 }
72
73 match req.into_body().collect().await {
74 Ok(c) => Ok(Self(c.to_bytes())),
75 Err(err) => Err(BytesRejection::from_err(err).into()),
76 }
77 }
78}
79
80impl OptionalFromRequest for OctetStream {
81 type Rejection = OctetStreamRejection;
82
83 async fn from_request(req: Request) -> Result<Option<Self>, Self::Rejection> {
93 if request_has_body(req.headers()) {
94 let v = <Self as FromRequest>::from_request(req).await?;
95 Ok(Some(v))
96 } else {
97 Ok(None)
98 }
99 }
100}
101
102fn request_has_body(headers: &HeaderMap) -> bool {
103 if headers.get(header::CONTENT_TYPE).is_some() {
104 return true;
105 }
106 if headers.get(header::TRANSFER_ENCODING).is_some() {
107 return true;
108 }
109 headers
110 .get(header::CONTENT_LENGTH)
111 .and_then(|v| v.to_str().ok())
112 .and_then(|s| s.parse::<u64>().ok())
113 .is_some_and(|n| n > 0)
114}
115
116enum ContentTypeMatch {
117 Match,
118 Absent,
119 Mismatch,
120}
121
122fn content_type_match(headers: &HeaderMap) -> ContentTypeMatch {
123 let Some(value) = headers.get(header::CONTENT_TYPE) else {
124 return ContentTypeMatch::Absent;
125 };
126 let parsed = value
127 .to_str()
128 .ok()
129 .and_then(|s| s.parse::<crate::mime::Mime>().ok());
130 match parsed {
131 Some(mime)
132 if mime.type_() == crate::mime::APPLICATION
133 && mime.subtype() == crate::mime::OCTET_STREAM =>
134 {
135 ContentTypeMatch::Match
136 }
137 _ => ContentTypeMatch::Mismatch,
138 }
139}
140
141#[cfg(test)]
142mod test {
143 use super::*;
144 use crate::StatusCode;
145 use crate::service::web::WebService;
146 use rama_core::Service;
147
148 #[tokio::test]
149 async fn test_octet_stream() {
150 let service =
151 WebService::default().with_post("/", async |OctetStream(body): OctetStream| {
152 assert_eq!(body.as_ref(), b"\x00\x01\x02\x03");
153 });
154
155 let req = rama_http_types::Request::builder()
156 .method(rama_http_types::Method::POST)
157 .header(
158 rama_http_types::header::CONTENT_TYPE,
159 "application/octet-stream",
160 )
161 .body(vec![0u8, 1, 2, 3].into())
162 .unwrap();
163 let resp = service.serve(req).await.unwrap();
164 assert_eq!(resp.status(), StatusCode::OK);
165 }
166
167 #[tokio::test]
168 async fn test_octet_stream_missing_content_type() {
169 let service = WebService::default()
170 .with_post("/", async |OctetStream(_): OctetStream| StatusCode::OK);
171
172 let req = rama_http_types::Request::builder()
173 .method(rama_http_types::Method::POST)
174 .header(rama_http_types::header::CONTENT_TYPE, "text/plain")
175 .body(vec![0u8, 1, 2, 3].into())
176 .unwrap();
177 let resp = service.serve(req).await.unwrap();
178 assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
179 }
180
181 #[tokio::test]
182 async fn test_octet_stream_optional_present() {
183 let service = WebService::default().with_post("/", async |body: Option<OctetStream>| {
184 let OctetStream(b) = body.expect("body present");
185 assert_eq!(b.as_ref(), b"data");
186 });
187
188 let req = rama_http_types::Request::builder()
189 .method(rama_http_types::Method::POST)
190 .header(
191 rama_http_types::header::CONTENT_TYPE,
192 "application/octet-stream",
193 )
194 .body("data".into())
195 .unwrap();
196 let resp = service.serve(req).await.unwrap();
197 assert_eq!(resp.status(), StatusCode::OK);
198 }
199
200 #[tokio::test]
201 async fn test_octet_stream_accepts_absent_content_type() {
202 let service =
205 WebService::default().with_post("/", async |OctetStream(body): OctetStream| {
206 assert_eq!(body.as_ref(), b"raw");
207 });
208
209 let req = rama_http_types::Request::builder()
210 .method(rama_http_types::Method::POST)
211 .body("raw".into())
212 .unwrap();
213 let resp = service.serve(req).await.unwrap();
214 assert_eq!(resp.status(), StatusCode::OK);
215 }
216
217 #[tokio::test]
218 async fn test_octet_stream_optional_no_content_type_with_body() {
219 let service = WebService::default().with_post("/", async |body: Option<OctetStream>| {
225 let OctetStream(b) = body.expect("body present");
226 assert_eq!(b.as_ref(), b"data");
227 });
228
229 let req = rama_http_types::Request::builder()
230 .method(rama_http_types::Method::POST)
231 .header(rama_http_types::header::CONTENT_LENGTH, "4")
232 .body("data".into())
233 .unwrap();
234 let resp = service.serve(req).await.unwrap();
235 assert_eq!(resp.status(), StatusCode::OK);
236 }
237
238 #[tokio::test]
239 async fn test_octet_stream_optional_absent() {
240 let service = WebService::default().with_post("/", async |body: Option<OctetStream>| {
241 assert!(body.is_none());
242 StatusCode::OK
243 });
244
245 let req = rama_http_types::Request::builder()
246 .method(rama_http_types::Method::POST)
247 .body(rama_http_types::Body::empty())
248 .unwrap();
249 let resp = service.serve(req).await.unwrap();
250 assert_eq!(resp.status(), StatusCode::OK);
251 }
252}