Skip to main content

rama_http/service/web/endpoint/extract/body/
text.rs

1use super::BytesRejection;
2use crate::Request;
3use crate::body::util::BodyExt;
4use crate::service::web::extract::{FromRequest, FromRequestBody};
5use crate::utils::macros::{composite_http_rejection, define_http_rejection};
6use rama_utils::macros::impl_deref;
7
8/// Extractor to get the response body, collected as [`String`].
9#[derive(Debug, Clone)]
10pub struct Text(pub String);
11
12impl_deref!(Text: String);
13
14define_http_rejection! {
15    #[status = UNSUPPORTED_MEDIA_TYPE]
16    #[body = "Text requests must have `Content-Type: text/plain`"]
17    /// Rejection type for [`Text`]
18    /// used if the `Content-Type` header is missing
19    /// or its value is not `text/plain`.
20    pub struct InvalidTextContentType;
21}
22
23define_http_rejection! {
24    #[status = BAD_REQUEST]
25    #[body = "Failed to decode text payload"]
26    /// Rejection type used if the [`Text`]
27    /// was not valid UTF-8.
28    pub struct InvalidUtf8Text(Error);
29}
30
31composite_http_rejection! {
32    /// Rejection used for [`Text`]
33    ///
34    /// Contains one variant for each way the [`Text`] extractor
35    /// can fail.
36    pub enum TextRejection {
37        InvalidTextContentType,
38        InvalidUtf8Text,
39        BytesRejection,
40    }
41}
42
43impl FromRequest for Text {
44    type Rejection = TextRejection;
45
46    async fn from_request(req: Request) -> Result<Self, Self::Rejection> {
47        let (parts, body) = req.into_parts();
48        let future = Self::from_request_body(&parts, body);
49        future.await
50    }
51}
52
53impl FromRequestBody for Text {
54    fn from_request_body(
55        parts: &crate::request::Parts,
56        body: crate::Body,
57    ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send + 'static {
58        let has_valid_content_type = crate::service::web::extract::has_any_content_type(
59            &parts.headers,
60            &[&crate::mime::TEXT_PLAIN],
61        );
62
63        async move {
64            if !has_valid_content_type {
65                return Err(InvalidTextContentType.into());
66            }
67
68            match body.collect().await {
69                Ok(c) => match String::from_utf8(c.to_bytes().to_vec()) {
70                    Ok(s) => Ok(Self(s)),
71                    Err(err) => Err(InvalidUtf8Text::from_err(err).into()),
72                },
73                Err(err) => Err(BytesRejection::from_err(err).into()),
74            }
75        }
76    }
77}
78
79#[cfg(test)]
80mod test {
81    use super::*;
82    use crate::service::web::WebService;
83    use crate::{Method, Request, StatusCode, header};
84    use rama_core::Service;
85
86    #[tokio::test]
87    async fn test_text() {
88        let service = WebService::default().with_post("/", async |Text(body): Text| {
89            assert_eq!(body, "test");
90        });
91
92        let req = Request::builder()
93            .method(Method::POST)
94            .header(header::CONTENT_TYPE, "text/plain")
95            .body("test".into())
96            .unwrap();
97        let resp = service.serve(req).await.unwrap();
98        assert_eq!(resp.status(), StatusCode::OK);
99    }
100
101    #[tokio::test]
102    async fn test_text_missing_content_type() {
103        let service = WebService::default().with_post("/", async |Text(_): Text| StatusCode::OK);
104
105        let req = Request::builder()
106            .method(Method::POST)
107            .body("test".into())
108            .unwrap();
109        let resp = service.serve(req).await.unwrap();
110        assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
111    }
112
113    #[tokio::test]
114    async fn test_text_incorrect_content_type() {
115        let service = WebService::default().with_post("/", async |Text(_): Text| StatusCode::OK);
116
117        let req = Request::builder()
118            .method(Method::POST)
119            .header(header::CONTENT_TYPE, "application/json")
120            .body("test".into())
121            .unwrap();
122        let resp = service.serve(req).await.unwrap();
123        assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
124    }
125
126    #[tokio::test]
127    async fn test_text_invalid_utf8() {
128        let service = WebService::default().with_post("/", async |Text(_): Text| StatusCode::OK);
129
130        let req = Request::builder()
131            .method(Method::POST)
132            .header(header::CONTENT_TYPE, "text/plain")
133            .body(vec![0, 159, 146, 150].into())
134            .unwrap();
135        let resp = service.serve(req).await.unwrap();
136        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
137    }
138}