lambda_http/ext/
request.rs

1//! Extension methods for `Request` types
2
3use std::{error::Error, fmt};
4
5use serde::{
6    de::{value::Error as SerdeError, DeserializeOwned},
7    Deserialize,
8};
9
10use crate::Body;
11
12/// Request payload deserialization errors
13///
14/// Returned by [`RequestPayloadExt::payload()`]
15#[derive(Debug)]
16pub enum PayloadError {
17    /// Returned when `application/json` bodies fail to deserialize a payload
18    Json(serde_json::Error),
19    /// Returned when `application/x-www-form-urlencoded` bodies fail to deserialize a payload
20    WwwFormUrlEncoded(SerdeError),
21}
22
23/// Indicates a problem processing a JSON payload.
24#[derive(Debug)]
25pub enum JsonPayloadError {
26    /// Problem deserializing a JSON payload.
27    Parsing(serde_json::Error),
28}
29
30/// Indicates a problem processing an x-www-form-urlencoded payload.
31#[derive(Debug)]
32pub enum FormUrlEncodedPayloadError {
33    /// Problem deserializing an x-www-form-urlencoded payload.
34    Parsing(SerdeError),
35}
36
37impl From<JsonPayloadError> for PayloadError {
38    fn from(err: JsonPayloadError) -> Self {
39        match err {
40            JsonPayloadError::Parsing(inner_err) => PayloadError::Json(inner_err),
41        }
42    }
43}
44
45impl From<FormUrlEncodedPayloadError> for PayloadError {
46    fn from(err: FormUrlEncodedPayloadError) -> Self {
47        match err {
48            FormUrlEncodedPayloadError::Parsing(inner_err) => PayloadError::WwwFormUrlEncoded(inner_err),
49        }
50    }
51}
52
53impl fmt::Display for PayloadError {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match self {
56            PayloadError::Json(json) => writeln!(f, "failed to parse payload from application/json {json}"),
57            PayloadError::WwwFormUrlEncoded(form) => writeln!(
58                f,
59                "failed to parse payload from application/x-www-form-urlencoded {form}"
60            ),
61        }
62    }
63}
64
65impl Error for PayloadError {
66    fn source(&self) -> Option<&(dyn Error + 'static)> {
67        match self {
68            PayloadError::Json(json) => Some(json),
69            PayloadError::WwwFormUrlEncoded(form) => Some(form),
70        }
71    }
72}
73
74/// Extends `http::Request<Body>` with payload deserialization helpers.
75pub trait RequestPayloadExt {
76    /// Return the result of a payload parsed into a type that implements [`serde::Deserialize`]
77    ///
78    /// Currently only `application/x-www-form-urlencoded`
79    /// and `application/json` flavors of content type
80    /// are supported
81    ///
82    /// A [`PayloadError`] will be returned for undeserializable payloads.
83    /// If no body is provided, the content-type header is missing,
84    /// or the content-type header is unsupported, then `Ok(None)` will
85    /// be returned. Note that a blank body (e.g. an empty string) is treated
86    /// like a present payload by some deserializers and may result in an error.
87    ///
88    /// ### Examples
89    ///
90    /// A request's body can be deserialized if its correctly encoded as per
91    /// the request's `Content-Type` header. The two supported content types are
92    /// `application/x-www-form-urlencoded` and `application/json`.
93    ///
94    /// The following handler will work an http request body of `x=1&y=2`
95    /// as well as `{"x":1, "y":2}` respectively.
96    ///
97    /// ```rust,no_run
98    /// use lambda_http::{
99    ///     service_fn, Body, Context, Error, IntoResponse, Request, RequestPayloadExt, Response,
100    /// };
101    /// use serde::Deserialize;
102    ///
103    /// #[derive(Debug, Default, Deserialize)]
104    /// struct Args {
105    ///   #[serde(default)]
106    ///   x: usize,
107    ///   #[serde(default)]
108    ///   y: usize
109    /// }
110    ///
111    /// #[tokio::main]
112    /// async fn main() -> Result<(), Error> {
113    ///   lambda_http::run(service_fn(add)).await?;
114    ///   Ok(())
115    /// }
116    ///
117    /// async fn add(
118    ///   request: Request
119    /// ) -> Result<Response<Body>, Error> {
120    ///   let args: Args = request.payload()
121    ///     .unwrap_or_else(|_parse_err| None)
122    ///     .unwrap_or_default();
123    ///   Ok(
124    ///      Response::new(
125    ///        format!(
126    ///          "{} + {} = {}",
127    ///          args.x,
128    ///          args.y,
129    ///          args.x + args.y
130    ///        ).into()
131    ///      )
132    ///   )
133    /// }
134    fn payload<D>(&self) -> Result<Option<D>, PayloadError>
135    where
136        D: DeserializeOwned;
137
138    /// Attempts to deserialize the request payload as JSON. When there is no payload,
139    /// `Ok(None)` is returned.
140    ///
141    /// ### Errors
142    ///
143    /// If a present payload is not a valid JSON payload matching the annotated type,
144    /// a [`JsonPayloadError`] is returned.
145    ///
146    /// ### Examples
147    ///
148    /// #### 1. Parsing a JSONString.
149    /// ```ignore
150    /// let req = http::Request::builder()
151    ///     .body(Body::from("\"I am a JSON string\""))
152    ///     .expect("failed to build request");
153    /// match req.json::<String>() {
154    ///     Ok(Some(json)) => assert_eq!(json, "I am a JSON string"),
155    ///     Ok(None) => panic!("payload is missing."),
156    ///     Err(err) => panic!("error processing json: {err:?}"),
157    /// }
158    /// ```
159    ///
160    /// #### 2. Parsing a JSONObject.
161    /// ```ignore
162    /// #[derive(Deserialize, Eq, PartialEq, Debug)]
163    /// struct Person {
164    ///     name: String,
165    ///     age: u8,
166    /// }
167    ///
168    /// let req = http::Request::builder()
169    ///     .body(Body::from(r#"{"name": "Adam", "age": 23}"#))
170    ///     .expect("failed to build request");
171    ///
172    /// match req.json::<Person>() {
173    ///     Ok(Some(person)) => assert_eq!(
174    ///         person,
175    ///         Person {
176    ///             name: "Adam".to_string(),
177    ///             age: 23
178    ///         }
179    ///     ),
180    ///     Ok(None) => panic!("payload is missing"),
181    ///     Err(JsonPayloadError::Parsing(err)) => {
182    ///         if err.is_data() {
183    ///             panic!("payload does not match Person schema: {err:?}")
184    ///         }
185    ///         if err.is_syntax() {
186    ///             panic!("payload is invalid json: {err:?}")
187    ///         }
188    ///         panic!("failed to parse json: {err:?}")
189    ///     }
190    /// }
191    /// ```
192    fn json<D>(&self) -> Result<Option<D>, JsonPayloadError>
193    where
194        D: DeserializeOwned;
195
196    /// Attempts to deserialize the request payload as an application/x-www-form-urlencoded
197    /// content type. When there is no payload, `Ok(None)` is returned.
198    ///
199    /// ### Errors
200    ///
201    /// If a present payload is not a valid application/x-www-form-urlencoded payload
202    /// matching the annotated type, a [`FormUrlEncodedPayloadError`] is returned.
203    ///
204    /// ### Examples
205    /// ```ignore
206    /// let req = http::Request::builder()
207    ///     .body(Body::from("name=Adam&age=23"))
208    ///     .expect("failed to build request");
209    /// match req.form_url_encoded::<Person>() {
210    ///     Ok(Some(person)) => assert_eq!(
211    ///         person,
212    ///         Person {
213    ///             name: "Adam".to_string(),
214    ///             age: 23
215    ///         }
216    ///     ),
217    ///     Ok(None) => panic!("payload is missing."),
218    ///     Err(err) => panic!("error processing payload: {err:?}"),
219    /// }
220    /// ```
221    fn form_url_encoded<D>(&self) -> Result<Option<D>, FormUrlEncodedPayloadError>
222    where
223        D: DeserializeOwned;
224}
225
226impl RequestPayloadExt for http::Request<Body> {
227    fn payload<D>(&self) -> Result<Option<D>, PayloadError>
228    where
229        for<'de> D: Deserialize<'de>,
230    {
231        self.headers()
232            .get(http::header::CONTENT_TYPE)
233            .map(|ct| match ct.to_str() {
234                Ok(content_type) => {
235                    if content_type.starts_with("application/x-www-form-urlencoded") {
236                        return self.form_url_encoded().map_err(PayloadError::from);
237                    } else if content_type.starts_with("application/json") {
238                        return self.json().map_err(PayloadError::from);
239                    }
240                    Ok(None)
241                }
242                _ => Ok(None),
243            })
244            .unwrap_or_else(|| Ok(None))
245    }
246
247    fn json<D>(&self) -> Result<Option<D>, JsonPayloadError>
248    where
249        D: DeserializeOwned,
250    {
251        if self.body().is_empty() {
252            return Ok(None);
253        }
254        serde_json::from_slice::<D>(self.body().as_ref())
255            .map(Some)
256            .map_err(JsonPayloadError::Parsing)
257    }
258
259    fn form_url_encoded<D>(&self) -> Result<Option<D>, FormUrlEncodedPayloadError>
260    where
261        D: DeserializeOwned,
262    {
263        if self.body().is_empty() {
264            return Ok(None);
265        }
266        serde_urlencoded::from_bytes::<D>(self.body().as_ref())
267            .map(Some)
268            .map_err(FormUrlEncodedPayloadError::Parsing)
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use serde::Deserialize;
275
276    use super::{FormUrlEncodedPayloadError, JsonPayloadError, RequestPayloadExt};
277
278    use crate::Body;
279
280    #[derive(Deserialize, Eq, PartialEq, Debug)]
281    struct Payload {
282        foo: String,
283        baz: usize,
284    }
285
286    fn get_test_payload_as_json_body() -> Body {
287        Body::from(r#"{"foo":"bar", "baz": 2}"#)
288    }
289
290    fn assert_eq_test_payload(payload: Option<Payload>) {
291        assert_eq!(
292            payload,
293            Some(Payload {
294                foo: "bar".into(),
295                baz: 2
296            })
297        );
298    }
299
300    #[test]
301    fn requests_have_form_post_parsable_payloads() {
302        let request = http::Request::builder()
303            .header("Content-Type", "application/x-www-form-urlencoded")
304            .body(Body::from("foo=bar&baz=2"))
305            .expect("failed to build request");
306        let payload: Option<Payload> = request.payload().unwrap_or_default();
307        assert_eq!(
308            payload,
309            Some(Payload {
310                foo: "bar".into(),
311                baz: 2
312            })
313        );
314    }
315
316    #[test]
317    fn requests_have_json_parsable_payloads() {
318        let request = http::Request::builder()
319            .header("Content-Type", "application/json")
320            .body(get_test_payload_as_json_body())
321            .expect("failed to build request");
322        let payload: Option<Payload> = request.payload().unwrap_or_default();
323        assert_eq_test_payload(payload)
324    }
325
326    #[test]
327    fn requests_match_form_post_content_type_with_charset() {
328        let request = http::Request::builder()
329            .header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
330            .body(Body::from("foo=bar&baz=2"))
331            .expect("failed to build request");
332        let payload: Option<Payload> = request.payload().unwrap_or_default();
333        assert_eq!(
334            payload,
335            Some(Payload {
336                foo: "bar".into(),
337                baz: 2
338            })
339        );
340    }
341
342    #[test]
343    fn requests_match_json_content_type_with_charset() {
344        let request = http::Request::builder()
345            .header("Content-Type", "application/json; charset=UTF-8")
346            .body(Body::from(r#"{"foo":"bar", "baz": 2}"#))
347            .expect("failed to build request");
348        let payload: Option<Payload> = request.payload().unwrap_or_default();
349        assert_eq!(
350            payload,
351            Some(Payload {
352                foo: "bar".into(),
353                baz: 2
354            })
355        );
356    }
357
358    #[test]
359    fn requests_omitting_content_types_do_not_support_parsable_payloads() {
360        let request = http::Request::builder()
361            .body(Body::from(r#"{"foo":"bar", "baz": 2}"#))
362            .expect("failed to build request");
363        let payload: Option<Payload> = request.payload().unwrap_or_default();
364        assert_eq!(payload, None);
365    }
366
367    #[test]
368    fn requests_omitting_body_returns_none() {
369        let request = http::Request::builder()
370            .body(Body::Empty)
371            .expect("failed to build request");
372        let payload: Option<String> = request.payload().unwrap();
373        assert_eq!(payload, None)
374    }
375
376    #[test]
377    fn requests_with_json_content_type_hdr_omitting_body_returns_none() {
378        let request = http::Request::builder()
379            .header("Content-Type", "application/json; charset=UTF-8")
380            .body(Body::Empty)
381            .expect("failed to build request");
382        let payload: Option<String> = request.payload().unwrap();
383        assert_eq!(payload, None)
384    }
385
386    #[test]
387    fn requests_with_formurlencoded_content_type_hdr_omitting_body_returns_none() {
388        let request = http::Request::builder()
389            .header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
390            .body(Body::Empty)
391            .expect("failed to build request");
392        let payload: Option<String> = request.payload().unwrap();
393        assert_eq!(payload, None)
394    }
395
396    #[derive(Deserialize, Eq, PartialEq, Debug)]
397    struct Person {
398        name: String,
399        age: u8,
400    }
401
402    #[test]
403    fn json_fn_parses_json_strings() {
404        let req = http::Request::builder()
405            .body(Body::from("\"I am a JSON string\""))
406            .expect("failed to build request");
407        match req.json::<String>() {
408            Ok(Some(json)) => assert_eq!(json, "I am a JSON string"),
409            Ok(None) => panic!("payload is missing."),
410            Err(err) => panic!("error processing json: {err:?}"),
411        }
412    }
413
414    #[test]
415    fn json_fn_parses_objects() {
416        let req = http::Request::builder()
417            .body(Body::from(r#"{"name": "Adam", "age": 23}"#))
418            .expect("failed to build request");
419
420        match req.json::<Person>() {
421            Ok(Some(person)) => assert_eq!(
422                person,
423                Person {
424                    name: "Adam".to_string(),
425                    age: 23
426                }
427            ),
428            Ok(None) => panic!("request data missing"),
429            Err(JsonPayloadError::Parsing(err)) => {
430                if err.is_data() {
431                    panic!("payload does not match Person: {err:?}")
432                }
433                if err.is_syntax() {
434                    panic!("invalid json: {err:?}")
435                }
436                panic!("failed to parse json: {err:?}")
437            }
438        }
439    }
440
441    #[test]
442    fn json_fn_parses_list_of_objects() {
443        let req = http::Request::builder()
444            .body(Body::from(
445                r#"[{"name": "Adam", "age": 23}, {"name": "Sarah", "age": 47}]"#,
446            ))
447            .expect("failed to build request");
448        let expected_result = vec![
449            Person {
450                name: "Adam".to_string(),
451                age: 23,
452            },
453            Person {
454                name: "Sarah".to_string(),
455                age: 47,
456            },
457        ];
458        let result: Vec<Person> = req.json().expect("invalid payload").expect("missing payload");
459        assert_eq!(result, expected_result);
460    }
461
462    #[test]
463    fn json_fn_parses_nested_objects() {
464        #[derive(Deserialize, Eq, PartialEq, Debug)]
465        struct Pet {
466            name: String,
467            owner: Person,
468        }
469
470        let req = http::Request::builder()
471            .body(Body::from(
472                r#"{"name": "Gumball", "owner": {"name": "Adam", "age": 23}}"#,
473            ))
474            .expect("failed to build request");
475
476        let expected_result = Pet {
477            name: "Gumball".to_string(),
478            owner: Person {
479                name: "Adam".to_string(),
480                age: 23,
481            },
482        };
483        let result: Pet = req.json().expect("invalid payload").expect("missing payload");
484        assert_eq!(result, expected_result);
485    }
486
487    #[test]
488    fn json_fn_accepts_request_with_content_type_header() {
489        let request = http::Request::builder()
490            .header("Content-Type", "application/json")
491            .body(get_test_payload_as_json_body())
492            .expect("failed to build request");
493        let payload: Option<Payload> = request.json().unwrap();
494        assert_eq_test_payload(payload)
495    }
496
497    #[test]
498    fn json_fn_accepts_request_without_content_type_header() {
499        let request = http::Request::builder()
500            .body(get_test_payload_as_json_body())
501            .expect("failed to build request");
502        let payload: Option<Payload> = request.json().expect("failed to parse json");
503        assert_eq_test_payload(payload)
504    }
505
506    #[test]
507    fn json_fn_given_nonjson_payload_returns_syntax_error() {
508        let request = http::Request::builder()
509            .body(Body::Text(String::from("Not a JSON")))
510            .expect("failed to build request");
511        let payload = request.json::<String>();
512        assert!(payload.is_err());
513
514        if let Err(JsonPayloadError::Parsing(err)) = payload {
515            assert!(err.is_syntax())
516        } else {
517            panic!(
518                "{}",
519                format!("payload should have caused a parsing error. instead, it was {payload:?}")
520            );
521        }
522    }
523
524    #[test]
525    fn json_fn_given_unexpected_payload_shape_returns_data_error() {
526        let request = http::Request::builder()
527            .body(Body::from(r#"{"foo":"bar", "baz": "!SHOULD BE A NUMBER!"}"#))
528            .expect("failed to build request");
529        let result = request.json::<Payload>();
530
531        if let Err(JsonPayloadError::Parsing(err)) = result {
532            assert!(err.is_data())
533        } else {
534            panic!(
535                "{}",
536                format!("payload should have caused a parsing error. instead, it was {result:?}")
537            );
538        }
539    }
540
541    #[test]
542    fn json_fn_given_empty_payload_returns_none() {
543        let empty_request = http::Request::default();
544        let payload: Option<Payload> = empty_request.json().expect("failed to parse json");
545        assert_eq!(payload, None)
546    }
547
548    #[test]
549    fn form_url_encoded_fn_parses_forms() {
550        let req = http::Request::builder()
551            .body(Body::from("name=Adam&age=23"))
552            .expect("failed to build request");
553        match req.form_url_encoded::<Person>() {
554            Ok(Some(person)) => assert_eq!(
555                person,
556                Person {
557                    name: "Adam".to_string(),
558                    age: 23
559                }
560            ),
561            Ok(None) => panic!("payload is missing."),
562            Err(err) => panic!("error processing payload: {err:?}"),
563        }
564    }
565
566    #[test]
567    fn form_url_encoded_fn_accepts_request_with_content_type_header() {
568        let request = http::Request::builder()
569            .header("Content-Type", "application/x-www-form-urlencoded")
570            .body(Body::from("foo=bar&baz=2"))
571            .expect("failed to build request");
572        let payload: Option<Payload> = request.form_url_encoded().unwrap();
573        assert_eq_test_payload(payload);
574    }
575
576    #[test]
577    fn form_url_encoded_fn_accepts_request_without_content_type_header() {
578        let request = http::Request::builder()
579            .body(Body::from("foo=bar&baz=2"))
580            .expect("failed to build request");
581        let payload: Option<Payload> = request.form_url_encoded().expect("failed to parse form");
582        assert_eq_test_payload(payload);
583    }
584
585    #[test]
586    fn form_url_encoded_fn_given_non_form_urlencoded_payload_errors() {
587        let request = http::Request::builder()
588            .body(Body::Text(String::from("Not a url-encoded form")))
589            .expect("failed to build request");
590        let payload = request.form_url_encoded::<String>();
591        assert!(payload.is_err());
592        assert!(matches!(payload, Err(FormUrlEncodedPayloadError::Parsing(_))));
593    }
594
595    #[test]
596    fn form_url_encoded_fn_given_unexpected_payload_shape_errors() {
597        let request = http::Request::builder()
598            .body(Body::from("foo=bar&baz=SHOULD_BE_A_NUMBER"))
599            .expect("failed to build request");
600        let result = request.form_url_encoded::<Payload>();
601        assert!(result.is_err());
602        assert!(matches!(result, Err(FormUrlEncodedPayloadError::Parsing(_))));
603    }
604
605    #[test]
606    fn form_url_encoded_fn_given_empty_payload_returns_none() {
607        let empty_request = http::Request::default();
608        let payload: Option<Payload> = empty_request.form_url_encoded().expect("failed to parse form");
609        assert_eq!(payload, None);
610    }
611}