Skip to main content

lambda_http/
request.rs

1//! ALB and API Gateway and VPC Lattice request adaptations
2//!
3//! Typically, these are exposed via the [`request_context()`] or [`request_context_ref()`]
4//! request extension methods provided by the [`RequestExt`] trait.
5//!
6//! [`request_context()`]: crate::RequestExt::request_context()
7//! [`request_context_ref()`]: crate::RequestExt::request_context_ref()
8//! [`RequestExt`]: crate::RequestExt
9#[cfg(any(feature = "apigw_rest", feature = "apigw_http", feature = "apigw_websockets"))]
10use crate::ext::extensions::{PathParameters, StageVariables};
11#[cfg(any(
12    feature = "apigw_rest",
13    feature = "apigw_http",
14    feature = "alb",
15    feature = "apigw_websockets",
16    feature = "vpc_lattice"
17))]
18use crate::ext::extensions::{QueryStringParameters, RawHttpPath};
19#[cfg(feature = "alb")]
20use aws_lambda_events::alb::{AlbTargetGroupRequest, AlbTargetGroupRequestContext};
21#[cfg(any(feature = "apigw_rest", feature = "apigw_http", feature = "apigw_websockets"))]
22use aws_lambda_events::apigw::ApiGatewayRequestAuthorizer;
23#[cfg(feature = "apigw_rest")]
24use aws_lambda_events::apigw::{ApiGatewayProxyRequest, ApiGatewayProxyRequestContext};
25#[cfg(feature = "apigw_http")]
26use aws_lambda_events::apigw::{ApiGatewayV2httpRequest, ApiGatewayV2httpRequestContext};
27#[cfg(feature = "apigw_websockets")]
28use aws_lambda_events::apigw::{ApiGatewayWebsocketProxyRequest, ApiGatewayWebsocketProxyRequestContext};
29#[cfg(feature = "vpc_lattice")]
30use aws_lambda_events::vpc_lattice::{VpcLatticeRequestV2, VpcLatticeRequestV2Context};
31
32use aws_lambda_events::{encodings::Body, query_map::QueryMap};
33use http::{header::HeaderName, HeaderMap, HeaderValue};
34
35use serde::{Deserialize, Serialize};
36use serde_json::error::Error as JsonError;
37
38use std::{env, future::Future, io::Read, pin::Pin};
39use url::Url;
40
41/// Internal representation of an Lambda http event from
42/// ALB, VPC Lattice Lambda, API Gateway REST and HTTP API proxy event perspectives
43///
44/// This is not intended to be a type consumed by crate users directly. The order
45/// of the variants are notable. Serde will try to deserialize in this order.
46#[non_exhaustive]
47#[doc(hidden)]
48#[derive(Debug)]
49pub enum LambdaRequest {
50    #[cfg(feature = "apigw_rest")]
51    ApiGatewayV1(ApiGatewayProxyRequest),
52    #[cfg(feature = "apigw_http")]
53    ApiGatewayV2(ApiGatewayV2httpRequest),
54    #[cfg(feature = "alb")]
55    Alb(AlbTargetGroupRequest),
56    #[cfg(feature = "apigw_websockets")]
57    WebSocket(ApiGatewayWebsocketProxyRequest),
58    #[cfg(feature = "vpc_lattice")]
59    VpcLatticeV2(VpcLatticeRequestV2),
60    #[cfg(feature = "pass_through")]
61    PassThrough(String),
62}
63
64impl LambdaRequest {
65    /// Return the `RequestOrigin` of the request to determine where the `LambdaRequest`
66    /// originated from, so that the appropriate response can be selected based on what
67    /// type of response the request origin expects.
68    pub fn request_origin(&self) -> RequestOrigin {
69        match self {
70            #[cfg(feature = "apigw_rest")]
71            LambdaRequest::ApiGatewayV1 { .. } => RequestOrigin::ApiGatewayV1,
72            #[cfg(feature = "apigw_http")]
73            LambdaRequest::ApiGatewayV2 { .. } => RequestOrigin::ApiGatewayV2,
74            #[cfg(feature = "alb")]
75            LambdaRequest::Alb { .. } => RequestOrigin::Alb,
76            #[cfg(feature = "apigw_websockets")]
77            LambdaRequest::WebSocket { .. } => RequestOrigin::WebSocket,
78            #[cfg(feature = "vpc_lattice")]
79            LambdaRequest::VpcLatticeV2 { .. } => RequestOrigin::VpcLatticeV2,
80            #[cfg(feature = "pass_through")]
81            LambdaRequest::PassThrough { .. } => RequestOrigin::PassThrough,
82            #[cfg(not(any(
83                feature = "apigw_rest",
84                feature = "apigw_http",
85                feature = "alb",
86                feature = "apigw_websockets",
87                feature = "vpc_lattice",
88            )))]
89            _ => compile_error!("Either feature `apigw_rest`, `apigw_http`, `alb`, `apigw_websockets` or `vpc_lattice` must be enabled for the `lambda-http` crate."),
90        }
91    }
92}
93
94/// RequestFuture type
95pub type RequestFuture<'a, R, E> = Pin<Box<dyn Future<Output = Result<R, E>> + Send + 'a>>;
96
97/// Represents the origin from which the lambda was requested from.
98#[non_exhaustive]
99#[doc(hidden)]
100#[derive(Debug, Clone)]
101pub enum RequestOrigin {
102    /// API Gateway request origin
103    #[cfg(feature = "apigw_rest")]
104    ApiGatewayV1,
105    /// API Gateway v2 request origin
106    #[cfg(feature = "apigw_http")]
107    ApiGatewayV2,
108    /// ALB request origin
109    #[cfg(feature = "alb")]
110    Alb,
111    /// API Gateway WebSocket
112    #[cfg(feature = "apigw_websockets")]
113    WebSocket,
114    /// VPC Lattice origin
115    #[cfg(feature = "vpc_lattice")]
116    VpcLatticeV2,
117    /// PassThrough request origin
118    #[cfg(feature = "pass_through")]
119    PassThrough,
120}
121
122#[cfg(feature = "apigw_http")]
123fn into_api_gateway_v2_request(ag: ApiGatewayV2httpRequest) -> http::Request<Body> {
124    let http_method = ag.request_context.http.method.clone();
125    let host = ag
126        .headers
127        .get(http::header::HOST)
128        .and_then(|s| s.to_str().ok())
129        .or(ag.request_context.domain_name.as_deref());
130    let raw_path = ag.raw_path.unwrap_or_default();
131    let path = apigw_path_with_stage(&ag.request_context.stage, &raw_path);
132
133    // don't use the query_string_parameters from API GW v2 to
134    // populate the QueryStringParameters extension because
135    // the value is not compatible with the whatgw specification.
136    // See: https://github.com/aws/aws-lambda-rust-runtime/issues/470
137    // See: https://url.spec.whatwg.org/#urlencoded-parsing
138    let query_string_parameters = if let Some(query) = &ag.raw_query_string {
139        query.parse().unwrap() // this is Infallible
140    } else {
141        ag.query_string_parameters
142    };
143
144    let mut uri = build_request_uri(&path, &ag.headers, host, None);
145    if let Some(query) = ag.raw_query_string {
146        if !query.is_empty() {
147            uri.push('?');
148            uri.push_str(&query);
149        }
150    }
151
152    let builder = http::Request::builder()
153        .uri(uri)
154        .extension(RawHttpPath(raw_path))
155        .extension(QueryStringParameters(query_string_parameters))
156        .extension(PathParameters(QueryMap::from(ag.path_parameters)))
157        .extension(StageVariables(QueryMap::from(ag.stage_variables)))
158        .extension(RequestContext::ApiGatewayV2(ag.request_context));
159
160    let mut headers = ag.headers;
161    if let Some(cookies) = ag.cookies {
162        if let Ok(header_value) = HeaderValue::from_str(&cookies.join("; ")) {
163            headers.insert(http::header::COOKIE, header_value);
164        }
165    }
166
167    let base64 = ag.is_base64_encoded;
168
169    let mut req = builder
170        .body(
171            ag.body
172                .as_deref()
173                .map_or_else(Body::default, |b| Body::from_maybe_encoded(base64, b)),
174        )
175        .expect("failed to build request");
176
177    // no builder method that sets headers in batch
178    let _ = std::mem::replace(req.headers_mut(), headers);
179    let _ = std::mem::replace(req.method_mut(), http_method);
180
181    req
182}
183
184#[cfg(feature = "apigw_rest")]
185fn into_proxy_request(ag: ApiGatewayProxyRequest) -> http::Request<Body> {
186    let http_method = ag.http_method;
187    let host = ag
188        .headers
189        .get(http::header::HOST)
190        .and_then(|s| s.to_str().ok())
191        .or(ag.request_context.domain_name.as_deref());
192    let raw_path = ag.path.unwrap_or_default();
193    let path = apigw_path_with_stage(&ag.request_context.stage, &raw_path);
194
195    let builder = http::Request::builder()
196        .uri(build_request_uri(
197            &path,
198            &ag.headers,
199            host,
200            Some((&ag.multi_value_query_string_parameters, &ag.query_string_parameters)),
201        ))
202        .extension(RawHttpPath(raw_path))
203        // multi-valued query string parameters are always a super
204        // set of singly valued query string parameters,
205        // when present, multi-valued query string parameters are preferred
206        .extension(QueryStringParameters(
207            if ag.multi_value_query_string_parameters.is_empty() {
208                ag.query_string_parameters
209            } else {
210                ag.multi_value_query_string_parameters
211            },
212        ))
213        .extension(PathParameters(QueryMap::from(ag.path_parameters)))
214        .extension(StageVariables(QueryMap::from(ag.stage_variables)))
215        .extension(RequestContext::ApiGatewayV1(ag.request_context));
216
217    // merge headers into multi_value_headers and make
218    // multi-value_headers our cannoncial source of request headers
219    let mut headers = ag.multi_value_headers;
220    headers.extend(ag.headers);
221
222    let base64 = ag.is_base64_encoded;
223    let mut req = builder
224        .body(
225            ag.body
226                .as_deref()
227                .map_or_else(Body::default, |b| Body::from_maybe_encoded(base64, b)),
228        )
229        .expect("failed to build request");
230
231    // no builder method that sets headers in batch
232    let _ = std::mem::replace(req.headers_mut(), headers);
233    let _ = std::mem::replace(req.method_mut(), http_method);
234
235    req
236}
237
238#[cfg(feature = "alb")]
239fn into_alb_request(alb: AlbTargetGroupRequest) -> http::Request<Body> {
240    let http_method = alb.http_method;
241    let host = alb.headers.get(http::header::HOST).and_then(|s| s.to_str().ok());
242    let raw_path = alb.path.unwrap_or_default();
243
244    let query_string_parameters = decode_query_map(alb.query_string_parameters);
245    let multi_value_query_string_parameters = decode_query_map(alb.multi_value_query_string_parameters);
246
247    let builder = http::Request::builder()
248        .uri(build_request_uri(
249            &raw_path,
250            &alb.headers,
251            host,
252            Some((&multi_value_query_string_parameters, &query_string_parameters)),
253        ))
254        .extension(RawHttpPath(raw_path))
255        // multi valued query string parameters are always a super
256        // set of singly valued query string parameters,
257        // when present, multi-valued query string parameters are preferred
258        .extension(QueryStringParameters(
259            if multi_value_query_string_parameters.is_empty() {
260                query_string_parameters
261            } else {
262                multi_value_query_string_parameters
263            },
264        ))
265        .extension(RequestContext::Alb(alb.request_context));
266
267    // merge headers into multi_value_headers and make
268    // multi-value_headers our cannoncial source of request headers
269    let mut headers = alb.multi_value_headers;
270    headers.extend(alb.headers);
271
272    let base64 = alb.is_base64_encoded;
273
274    let mut req = builder
275        .body(
276            alb.body
277                .as_deref()
278                .map_or_else(Body::default, |b| Body::from_maybe_encoded(base64, b)),
279        )
280        .expect("failed to build request");
281
282    // no builder method that sets headers in batch
283    let _ = std::mem::replace(req.headers_mut(), headers);
284    let _ = std::mem::replace(req.method_mut(), http_method);
285
286    req
287}
288
289#[cfg(any(feature = "alb", feature = "vpc_lattice"))]
290fn decode_query_map(query_map: QueryMap) -> QueryMap {
291    use std::str::FromStr;
292
293    let query_string = query_map.to_query_string();
294    let decoded = percent_encoding::percent_decode(query_string.as_bytes()).decode_utf8_lossy();
295    QueryMap::from_str(&decoded).unwrap_or_default()
296}
297
298#[cfg(feature = "apigw_websockets")]
299fn into_websocket_request(ag: ApiGatewayWebsocketProxyRequest) -> http::Request<Body> {
300    let http_method = ag.http_method;
301    let host = ag
302        .headers
303        .get(http::header::HOST)
304        .and_then(|s| s.to_str().ok())
305        .or(ag.request_context.domain_name.as_deref());
306    let raw_path = ag.path.unwrap_or_default();
307    let path = apigw_path_with_stage(&ag.request_context.stage, &raw_path);
308
309    let builder = http::Request::builder()
310        .uri(build_request_uri(
311            &path,
312            &ag.headers,
313            host,
314            Some((&ag.multi_value_query_string_parameters, &ag.query_string_parameters)),
315        ))
316        .extension(RawHttpPath(raw_path))
317        // multi-valued query string parameters are always a super
318        // set of singly valued query string parameters,
319        // when present, multi-valued query string parameters are preferred
320        .extension(QueryStringParameters(
321            if ag.multi_value_query_string_parameters.is_empty() {
322                ag.query_string_parameters
323            } else {
324                ag.multi_value_query_string_parameters
325            },
326        ))
327        .extension(PathParameters(QueryMap::from(ag.path_parameters)))
328        .extension(StageVariables(QueryMap::from(ag.stage_variables)))
329        .extension(RequestContext::WebSocket(ag.request_context));
330
331    // merge headers into multi_value_headers and make
332    // multi-value_headers our canonical source of request headers
333    let mut headers = ag.multi_value_headers;
334    headers.extend(ag.headers);
335
336    let base64 = ag.is_base64_encoded;
337    let mut req = builder
338        .body(
339            ag.body
340                .as_deref()
341                .map_or_else(Body::default, |b| Body::from_maybe_encoded(base64, b)),
342        )
343        .expect("failed to build request");
344
345    // no builder method that sets headers in batch
346    let _ = std::mem::replace(req.headers_mut(), headers);
347    let _ = std::mem::replace(req.method_mut(), http_method.unwrap_or(http::Method::GET));
348
349    req
350}
351
352#[cfg(feature = "vpc_lattice")]
353fn into_vpc_lattice_request(vlr: VpcLatticeRequestV2) -> http::Request<Body> {
354    let http_method = vlr.method;
355    let host = vlr.headers.get(http::header::HOST).and_then(|s| s.to_str().ok());
356    let raw_path = vlr.path.unwrap_or_default();
357
358    let query_string_parameters = decode_query_map(vlr.query_string_parameters);
359
360    let builder = http::Request::builder()
361        .uri(build_request_uri(
362            &raw_path,
363            &vlr.headers,
364            host,
365            Some((&query_string_parameters, &query_string_parameters)),
366        ))
367        .extension(RawHttpPath(raw_path))
368        .extension(QueryStringParameters(query_string_parameters))
369        .extension(RequestContext::VpcLattice(vlr.request_context));
370
371    let base64 = vlr.is_base64_encoded;
372
373    let mut req = builder
374        .body(
375            vlr.body
376                .as_deref()
377                .map_or_else(Body::default, |b| Body::from_maybe_encoded(base64, b)),
378        )
379        .expect("failed to build request");
380
381    // no builder method that sets headers in batch
382    let _ = std::mem::replace(req.headers_mut(), vlr.headers);
383    let _ = std::mem::replace(req.method_mut(), http_method.unwrap_or(http::Method::GET));
384
385    req
386}
387
388#[cfg(feature = "pass_through")]
389fn into_pass_through_request(data: String) -> http::Request<Body> {
390    let mut builder = http::Request::builder();
391
392    let headers = builder.headers_mut().unwrap();
393    headers.insert("Content-Type", "application/json".parse().unwrap());
394
395    let raw_path = "/events";
396
397    builder
398        .method(http::Method::POST)
399        .uri(raw_path)
400        .extension(RawHttpPath(raw_path.to_string()))
401        .extension(RequestContext::PassThrough)
402        .body(Body::from(data))
403        .expect("failed to build request")
404}
405
406#[cfg(any(feature = "apigw_rest", feature = "apigw_http", feature = "apigw_websockets"))]
407fn apigw_path_with_stage(stage: &Option<String>, path: &str) -> String {
408    if env::var("AWS_LAMBDA_HTTP_IGNORE_STAGE_IN_PATH").is_ok() {
409        return path.into();
410    }
411
412    let stage = match stage {
413        None => return path.into(),
414        Some(stage) if stage == "$default" => return path.into(),
415        Some(stage) => stage,
416    };
417
418    let prefix = format!("/{stage}/");
419    if path.starts_with(&prefix) {
420        path.into()
421    } else {
422        format!("/{stage}{path}")
423    }
424}
425
426/// Event request context as an enumeration of request contexts
427/// for both ALB and API Gateway and HTTP API events
428#[non_exhaustive]
429#[derive(Deserialize, Debug, Clone, Serialize)]
430#[serde(untagged)]
431pub enum RequestContext {
432    /// API Gateway proxy request context
433    #[cfg(feature = "apigw_rest")]
434    ApiGatewayV1(ApiGatewayProxyRequestContext),
435    /// API Gateway v2 request context
436    #[cfg(feature = "apigw_http")]
437    ApiGatewayV2(ApiGatewayV2httpRequestContext),
438    /// ALB request context
439    #[cfg(feature = "alb")]
440    Alb(AlbTargetGroupRequestContext),
441    /// WebSocket request context
442    #[cfg(feature = "apigw_websockets")]
443    WebSocket(ApiGatewayWebsocketProxyRequestContext),
444    /// VPC Lattice request context
445    #[cfg(feature = "vpc_lattice")]
446    VpcLattice(VpcLatticeRequestV2Context),
447    /// Custom request context
448    #[cfg(feature = "pass_through")]
449    PassThrough,
450}
451
452/// Converts LambdaRequest types into `http::Request<Body>` types
453impl From<LambdaRequest> for http::Request<Body> {
454    fn from(value: LambdaRequest) -> Self {
455        match value {
456            #[cfg(feature = "apigw_rest")]
457            LambdaRequest::ApiGatewayV1(ag) => into_proxy_request(ag),
458            #[cfg(feature = "apigw_http")]
459            LambdaRequest::ApiGatewayV2(ag) => into_api_gateway_v2_request(ag),
460            #[cfg(feature = "alb")]
461            LambdaRequest::Alb(alb) => into_alb_request(alb),
462            #[cfg(feature = "apigw_websockets")]
463            LambdaRequest::WebSocket(ag) => into_websocket_request(ag),
464            #[cfg(feature = "vpc_lattice")]
465            LambdaRequest::VpcLatticeV2(vpclat) => into_vpc_lattice_request(vpclat),
466            #[cfg(feature = "pass_through")]
467            LambdaRequest::PassThrough(data) => into_pass_through_request(data),
468        }
469    }
470}
471
472impl RequestContext {
473    /// Returns the Api Gateway Authorizer information for a request.
474    #[cfg(any(feature = "apigw_rest", feature = "apigw_http", feature = "apigw_websockets"))]
475    pub fn authorizer(&self) -> Option<&ApiGatewayRequestAuthorizer> {
476        match self {
477            #[cfg(feature = "apigw_rest")]
478            Self::ApiGatewayV1(ag) => Some(&ag.authorizer),
479            #[cfg(feature = "apigw_http")]
480            Self::ApiGatewayV2(ag) => ag.authorizer.as_ref(),
481            #[cfg(feature = "apigw_websockets")]
482            Self::WebSocket(ag) => Some(&ag.authorizer),
483            #[cfg(any(feature = "alb", feature = "pass_through", feature = "vpc_lattice"))]
484            _ => None,
485        }
486    }
487}
488
489/// Deserializes a `Request` from a `Read` impl providing JSON events.
490///
491/// # Example
492///
493/// ```rust,no_run
494/// use lambda_http::request::from_reader;
495/// use std::fs::File;
496/// use std::error::Error;
497///
498/// fn main() -> Result<(), Box<dyn Error>> {
499///     let request = from_reader(
500///         File::open("path/to/request.json")?
501///     )?;
502///     Ok(println!("{:#?}", request))
503/// }
504/// ```
505pub fn from_reader<R>(rdr: R) -> Result<crate::Request, JsonError>
506where
507    R: Read,
508{
509    serde_json::from_reader(rdr).map(LambdaRequest::into)
510}
511
512/// Deserializes a `Request` from a string of JSON text.
513///
514/// # Example
515///
516/// ```rust,no_run
517/// use lambda_http::request::from_str;
518/// use std::fs::File;
519/// use std::error::Error;
520///
521/// fn main() -> Result<(), Box<dyn Error>> {
522///     let request = from_str(
523///         r#"{ ...raw json here... }"#
524///     )?;
525///     Ok(println!("{:#?}", request))
526/// }
527/// ```
528pub fn from_str(s: &str) -> Result<crate::Request, JsonError> {
529    serde_json::from_str(s).map(LambdaRequest::into)
530}
531
532fn x_forwarded_proto() -> HeaderName {
533    HeaderName::from_static("x-forwarded-proto")
534}
535
536fn build_request_uri(
537    path: &str,
538    headers: &HeaderMap,
539    host: Option<&str>,
540    queries: Option<(&QueryMap, &QueryMap)>,
541) -> String {
542    let mut url = match host {
543        None => {
544            let rel_url = Url::parse(&format!("http://localhost{path}")).unwrap();
545            rel_url.path().to_string()
546        }
547        Some(host) => {
548            let scheme = headers
549                .get(x_forwarded_proto())
550                .and_then(|s| s.to_str().ok())
551                .unwrap_or("https");
552            let url = format!("{scheme}://{host}{path}");
553            Url::parse(&url).unwrap().to_string()
554        }
555    };
556
557    if let Some((mv, sv)) = queries {
558        if !mv.is_empty() {
559            url.push('?');
560            url.push_str(&mv.to_query_string());
561        } else if !sv.is_empty() {
562            url.push('?');
563            url.push_str(&sv.to_query_string());
564        }
565    }
566
567    url
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573    use crate::ext::RequestExt;
574    use std::fs::File;
575
576    #[test]
577    fn deserializes_apigw_request_events_from_readables() {
578        // from the docs
579        // https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-api-gateway-request
580        // note: file paths are relative to the directory of the crate at runtime
581        let result = from_reader(File::open("tests/data/apigw_proxy_request.json").expect("expected file"));
582        assert!(result.is_ok(), "event was not parsed as expected {result:?}");
583    }
584
585    #[test]
586    fn deserializes_minimal_apigw_http_request_events() {
587        // from the docs
588        // https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-api-gateway-request
589        let input = include_str!("../tests/data/apigw_v2_proxy_request_minimal.json");
590        let result = from_str(input);
591        assert!(
592            result.is_ok(),
593            "event was not parsed as expected {result:?} given {input}"
594        );
595        let req = result.expect("failed to parse request");
596        assert_eq!(req.method(), "GET");
597        assert_eq!(req.uri(), "https://xxx.execute-api.us-east-1.amazonaws.com/");
598
599        // Ensure this is an APIGWv2 request
600        let req_context = req.request_context_ref().expect("Request is missing RequestContext");
601        assert!(
602            matches!(req_context, &RequestContext::ApiGatewayV2(_)),
603            "expected ApiGatewayV2 context, got {req_context:?}"
604        );
605    }
606
607    #[test]
608    fn deserializes_apigw_http_request_events() {
609        // from the docs
610        // https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-api-gateway-request
611        let input = include_str!("../tests/data/apigw_v2_proxy_request.json");
612        let result = from_str(input);
613        assert!(
614            result.is_ok(),
615            "event was not parsed as expected {result:?} given {input}"
616        );
617        let req = result.expect("failed to parse request");
618        let cookie_header = req
619            .headers()
620            .get(http::header::COOKIE)
621            .ok_or_else(|| "Cookie header not found".to_string())
622            .and_then(|v| v.to_str().map_err(|e| e.to_string()));
623
624        assert_eq!(req.method(), "POST");
625        assert_eq!(req.uri(), "https://id.execute-api.us-east-1.amazonaws.com/my/path?parameter1=value1&parameter1=value2&parameter2=value");
626        assert_eq!(cookie_header, Ok("cookie1=value1; cookie2=value2"));
627
628        // Ensure this is an APIGWv2 request
629        let req_context = req.request_context_ref().expect("Request is missing RequestContext");
630        assert!(
631            matches!(req_context, &RequestContext::ApiGatewayV2(_)),
632            "expected ApiGatewayV2 context, got {req_context:?}"
633        );
634
635        let (parts, _) = req.into_parts();
636        assert_eq!("https://id.execute-api.us-east-1.amazonaws.com/my/path?parameter1=value1&parameter1=value2&parameter2=value", parts.uri.to_string());
637    }
638
639    #[test]
640    fn deserializes_apigw_request_events() {
641        // from the docs
642        // https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
643        // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
644        let input = include_str!("../tests/data/apigw_proxy_request.json");
645        let result = from_str(input);
646        assert!(
647            result.is_ok(),
648            "event was not parsed as expected {result:?} given {input}"
649        );
650        let req = result.expect("failed to parse request");
651        assert_eq!(req.method(), "GET");
652        assert_eq!(
653            req.uri(),
654            "https://wt6mne2s9k.execute-api.us-west-2.amazonaws.com/test/hello?name=me"
655        );
656
657        // Ensure this is an APIGW request
658        let req_context = req.request_context_ref().expect("Request is missing RequestContext");
659        assert!(
660            matches!(req_context, &RequestContext::ApiGatewayV1(_)),
661            "expected ApiGateway context, got {req_context:?}"
662        );
663    }
664
665    #[test]
666    fn deserializes_lambda_function_url_request_events() {
667        // from the docs
668        // https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads
669        let input = include_str!("../tests/data/lambda_function_url_request.json");
670        let result = from_str(input);
671        assert!(
672            result.is_ok(),
673            "event was not parsed as expected {result:?} given {input}"
674        );
675        let req = result.expect("failed to parse request");
676        let cookie_header = req
677            .headers()
678            .get_all(http::header::COOKIE)
679            .iter()
680            .map(|v| v.to_str().unwrap().to_string())
681            .reduce(|acc, nxt| [acc, nxt].join(";"));
682
683        assert_eq!(req.method(), "GET");
684        assert_eq!(
685            req.uri(),
686            "https://id.lambda-url.eu-west-2.on.aws/my/path?parameter1=value1&parameter1=value2&parameter2=value"
687        );
688        assert_eq!(cookie_header, Some("test=hi".to_string()));
689
690        // Ensure this is an APIGWv2 request (Lambda Function URL requests confirm to API GW v2 Request format)
691        let req_context = req.request_context_ref().expect("Request is missing RequestContext");
692        assert!(
693            matches!(req_context, &RequestContext::ApiGatewayV2(_)),
694            "expected ApiGatewayV2 context, got {req_context:?}"
695        );
696    }
697
698    #[test]
699    fn deserializes_alb_request_events() {
700        // from the docs
701        // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers
702        let input = include_str!("../tests/data/alb_request.json");
703        let result = from_str(input);
704        assert!(
705            result.is_ok(),
706            "event was not parsed as expected {result:?} given {input}"
707        );
708        let req = result.expect("failed to parse request");
709        assert_eq!(req.method(), "GET");
710        assert_eq!(
711            req.uri(),
712            "https://lambda-846800462-us-east-2.elb.amazonaws.com/?myKey=val2"
713        );
714
715        // Ensure this is an ALB request
716        let req_context = req.request_context_ref().expect("Request is missing RequestContext");
717        assert!(
718            matches!(req_context, &RequestContext::Alb(_)),
719            "expected Alb context, got {req_context:?}"
720        );
721    }
722
723    #[test]
724    fn deserializes_alb_request_encoded_query_parameters_events() {
725        // from the docs
726        // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers
727        let input = include_str!("../tests/data/alb_request_encoded_query_parameters.json");
728        let result = from_str(input);
729        assert!(
730            result.is_ok(),
731            "event was not parsed as expected {result:?} given {input}"
732        );
733        let req = result.expect("failed to parse request");
734        assert_eq!(req.method(), "GET");
735        assert_eq!(
736            req.uri(),
737            "https://lambda-846800462-us-east-2.elb.amazonaws.com/?myKey=%3FshowAll%3Dtrue"
738        );
739
740        // Ensure this is an ALB request
741        let req_context = req.request_context_ref().expect("Request is missing RequestContext");
742        assert!(
743            matches!(req_context, &RequestContext::Alb(_)),
744            "expected Alb context, got {req_context:?}"
745        );
746    }
747
748    #[test]
749    fn deserializes_apigw_multi_value_request_events() {
750        // from docs
751        // https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
752        let input = include_str!("../tests/data/apigw_multi_value_proxy_request.json");
753        let result = from_str(input);
754        assert!(
755            result.is_ok(),
756            "event is was not parsed as expected {result:?} given {input}"
757        );
758        let request = result.expect("failed to parse request");
759
760        assert!(!request
761            .query_string_parameters_ref()
762            .expect("Request is missing query parameters")
763            .is_empty());
764
765        // test RequestExt#query_string_parameters_ref does the right thing
766        let params = request.query_string_parameters();
767        assert_eq!(Some(vec!["you", "me"]), params.all("multiValueName"));
768        assert_eq!(Some(vec!["me"]), params.all("name"));
769
770        let query = request.uri().query().unwrap();
771        assert!(query.contains("name=me"));
772        assert!(query.contains("multiValueName=you&multiValueName=me"));
773        let (parts, _) = request.into_parts();
774        assert!(parts.uri.to_string().contains("name=me"));
775        assert!(parts.uri.to_string().contains("multiValueName=you&multiValueName=me"));
776    }
777
778    #[test]
779    fn deserializes_alb_multi_value_request_events() {
780        // from docs
781        // https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
782        let input = include_str!("../tests/data/alb_multi_value_request.json");
783        let result = from_str(input);
784        assert!(
785            result.is_ok(),
786            "event is was not parsed as expected {result:?} given {input}"
787        );
788        let request = result.expect("failed to parse request");
789        assert!(!request
790            .query_string_parameters_ref()
791            .expect("Request is missing query parameters")
792            .is_empty());
793
794        // test RequestExt#query_string_parameters_ref does the right thing
795        let params = request.query_string_parameters();
796        assert_eq!(Some(vec!["val1", "val2"]), params.all("myKey"));
797        assert_eq!(Some(vec!["val3", "val4"]), params.all("myOtherKey"));
798
799        let query = request.uri().query().unwrap();
800        assert!(query.contains("myKey=val1&myKey=val2"));
801        assert!(query.contains("myOtherKey=val3&myOtherKey=val4"));
802    }
803
804    #[test]
805    fn deserializes_alb_multi_value_request_encoded_query_parameters_events() {
806        // from docs
807        // https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
808        let input = include_str!("../tests/data/alb_multi_value_request_encoded_query_parameters.json");
809        let result = from_str(input);
810        assert!(
811            result.is_ok(),
812            "event is was not parsed as expected {result:?} given {input}"
813        );
814        let request = result.expect("failed to parse request");
815        assert!(!request
816            .query_string_parameters_ref()
817            .expect("Request is missing query parameters")
818            .is_empty());
819
820        // test RequestExt#query_string_parameters_ref does the right thing
821        assert_eq!(
822            request
823                .query_string_parameters_ref()
824                .and_then(|params| params.all("myKey")),
825            Some(vec!["?showAll=true", "?showAll=false"])
826        );
827    }
828
829    #[test]
830    #[cfg(feature = "vpc_lattice")]
831    fn deserializes_vpc_lattice_basic() {
832        let input = include_str!("../tests/data/vpc_lattice_v2_request.json");
833        let result = from_str(input);
834        assert!(
835            result.is_ok(),
836            "event is was not parsed as expected {result:?} given {input}"
837        );
838        let request = result.expect("failed to parse request");
839        assert_eq!(request.method(), "GET");
840
841        let body_str = match request.body() {
842            Body::Text(s) => s.as_str(),
843            _ => "",
844        };
845
846        assert_eq!(body_str, "All is good");
847
848        let uri = request.uri().to_string();
849        assert!(uri.starts_with("/health?"), "unexpected uri: {uri}");
850        assert!(uri.contains("multi=a"), "unexpected uri: {uri}");
851        assert!(uri.contains("multi=DEF"), "unexpected uri: {uri}");
852        assert!(uri.contains("multi=g"), "unexpected uri: {uri}");
853        assert!(uri.contains("state=prod"), "unexpected uri: {uri}");
854
855        // Ensure this is an VPC Lattice request
856        let req_context = request
857            .request_context_ref()
858            .expect("Request is missing RequestContext");
859        assert!(
860            matches!(req_context, &RequestContext::VpcLattice(_)),
861            "expected Vpc lattice context, got {req_context:?}"
862        );
863    }
864
865    #[test]
866    #[cfg(feature = "vpc_lattice")]
867    fn deserializes_vpc_lattice_basic_base64() {
868        let input = include_str!("../tests/data/vpc_lattice_v2_request_base64.json");
869        let result = from_str(input);
870        assert!(
871            result.is_ok(),
872            "event is was not parsed as expected {result:?} given {input}"
873        );
874        let request = result.expect("failed to parse request");
875        assert_eq!(request.method(), "GET");
876
877        let body_array = match request.body() {
878            Body::Binary(s) => s.as_slice(),
879            _ => &[],
880        };
881
882        assert_eq!(body_array, *b"All is good");
883
884        // URI should have been built from host header, query and protocol etc
885        let uri = request.uri();
886
887        assert!(uri.to_string().starts_with("https://www.site.com/health?"));
888        assert!(uri.to_string().contains("multi=a&multi=DEF&multi=g"));
889        assert!(uri.to_string().contains("state=prod"));
890
891        // Ensure this is an VPC Lattice request
892        let req_context = request
893            .request_context_ref()
894            .expect("Request is missing RequestContext");
895        assert!(
896            matches!(req_context, &RequestContext::VpcLattice(_)),
897            "expected Vpc lattice context, got {req_context:?}"
898        );
899    }
900
901    #[test]
902    #[cfg(feature = "vpc_lattice")]
903    fn deserializes_vpc_lattice_headers() {
904        let input = include_str!("../tests/data/vpc_lattice_v2_request.json");
905        let result = from_str(input);
906        assert!(
907            result.is_ok(),
908            "event is was not parsed as expected {result:?} given {input}"
909        );
910        let request = result.expect("failed to parse request");
911
912        // decoding multi value headers
913        let multi_headers_as_big_string = request
914            .headers()
915            .get_all("multi")
916            .iter()
917            .map(|v| v.to_str().unwrap().to_string())
918            .reduce(|acc, nxt| [acc, nxt].join(";"));
919
920        assert_eq!(multi_headers_as_big_string, Some("x;y".to_string()));
921
922        // decoding regular headers
923        let basic_headers_as_big_string = request
924            .headers()
925            .get_all("user-agent")
926            .iter()
927            .map(|v| v.to_str().unwrap().to_string())
928            .reduce(|acc, nxt| [acc, nxt].join(";"));
929
930        assert_eq!(basic_headers_as_big_string, Some("curl/7.68.0".to_string()));
931
932        // Ensure this is an VPC Lattice request
933        let req_context = request
934            .request_context_ref()
935            .expect("Request is missing RequestContext");
936        assert!(
937            matches!(req_context, &RequestContext::VpcLattice(_)),
938            "expected Vpc lattice context, got {req_context:?}"
939        );
940    }
941
942    #[test]
943    #[cfg(feature = "vpc_lattice")]
944    fn deserializes_vpc_lattice_multi_value_querys() {
945        let input = include_str!("../tests/data/vpc_lattice_v2_request.json");
946        let result = from_str(input);
947        assert!(
948            result.is_ok(),
949            "event is was not parsed as expected {result:?} given {input}"
950        );
951        let request = result.expect("failed to parse request");
952        assert!(!request
953            .query_string_parameters_ref()
954            .expect("Request is missing query parameters")
955            .is_empty());
956
957        let params = request.query_string_parameters();
958        assert_eq!(Some(vec!["prod"]), params.all("state"));
959        assert_eq!(Some(vec!["a", "DEF", "g"]), params.all("multi"));
960
961        let query = request.uri().query().unwrap();
962        assert!(query.contains("multi=a&multi=DEF&multi=g"));
963        assert!(query.contains("state=prod"));
964    }
965
966    #[test]
967    #[cfg(feature = "vpc_lattice")]
968    fn deserializes_vpc_lattice_encoded_query_parameters() {
969        let input = include_str!("../tests/data/vpc_lattice_v2_request_encoded_query.json");
970        let result = from_str(input);
971        assert!(
972            result.is_ok(),
973            "event is was not parsed as expected {result:?} given {input}"
974        );
975        let request = result.expect("failed to parse request");
976
977        let params = request.query_string_parameters();
978        // percent-encoded values should be decoded
979        assert_eq!(Some(vec!["?showAll=true"]), params.all("filter"));
980        assert_eq!(Some(vec!["hello world"]), params.all("q"));
981
982        let query = request.uri().query().unwrap();
983        assert!(query.contains("filter="), "unexpected uri query: {query}");
984        assert!(query.contains("q="), "unexpected uri query: {query}");
985    }
986
987    #[test]
988    #[cfg(feature = "vpc_lattice")]
989    fn deserializes_vpc_lattice_no_body() {
990        let input = r#"{
991            "version": "2.0",
992            "path": "/ping",
993            "method": "GET",
994            "headers": {"accept": ["*/*"]},
995            "queryStringParameters": {},
996            "isBase64Encoded": false,
997            "requestContext": {
998                "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-1:123456789012:servicenetwork/sn-abc",
999                "serviceArn": "arn:aws:vpc-lattice:us-east-1:123456789012:service/svc-abc",
1000                "targetGroupArn": "arn:aws:vpc-lattice:us-east-1:123456789012:targetgroup/tg-abc",
1001                "region": "us-east-1",
1002                "timeEpoch": "1724875399456789"
1003            }
1004        }"#;
1005        let result = from_str(input);
1006        assert!(result.is_ok(), "event was not parsed as expected {result:?}");
1007        let request = result.expect("failed to parse request");
1008        assert_eq!(request.method(), "GET");
1009        assert!(
1010            matches!(request.body(), Body::Empty),
1011            "expected empty body, got {:?}",
1012            request.body()
1013        );
1014    }
1015
1016    #[test]
1017    fn deserialize_apigw_http_sam_local() {
1018        // manually generated from AWS SAM CLI
1019        // Steps to recreate:
1020        // * sam init
1021        // * Use, Zip Python 3.9, and Hello World example
1022        // * Change the template to use HttpApi instead of Api
1023        // * Change the function code to return the Lambda event serialized
1024        // * sam local start-api
1025        // * Invoke the API
1026        let input = include_str!("../tests/data/apigw_v2_sam_local.json");
1027        let result = from_str(input);
1028        assert!(
1029            result.is_ok(),
1030            "event was not parsed as expected {result:?} given {input}"
1031        );
1032        let req = result.expect("failed to parse request");
1033        assert_eq!(req.method(), "GET");
1034        assert_eq!(req.uri(), "http://127.0.0.1:3000/hello");
1035    }
1036
1037    #[test]
1038    fn deserialize_apigw_no_host() {
1039        // generated from the 'apigateway-aws-proxy' test event template in the Lambda console
1040        let input = include_str!("../tests/data/apigw_no_host.json");
1041        let result = from_str(input);
1042        assert!(
1043            result.is_ok(),
1044            "event was not parsed as expected {result:?} given {input}"
1045        );
1046        let req = result.expect("failed to parse request");
1047        assert_eq!(req.method(), "GET");
1048        assert_eq!(req.uri(), "/test/hello?name=me");
1049    }
1050
1051    #[test]
1052    fn deserialize_alb_no_host() {
1053        // generated from ALB health checks
1054        let input = include_str!("../tests/data/alb_no_host.json");
1055        let result = from_str(input);
1056        assert!(
1057            result.is_ok(),
1058            "event was not parsed as expected {result:?} given {input}"
1059        );
1060        let req = result.expect("failed to parse request");
1061        assert_eq!(req.method(), "GET");
1062        assert_eq!(req.uri(), "/v1/health/");
1063    }
1064
1065    #[test]
1066    fn deserialize_apigw_path_with_space() {
1067        // generated from ALB health checks
1068        let input = include_str!("../tests/data/apigw_request_path_with_space.json");
1069        let result = from_str(input);
1070        assert!(
1071            result.is_ok(),
1072            "event was not parsed as expected {result:?} given {input}"
1073        );
1074        let req = result.expect("failed to parse request");
1075        assert_eq!(req.uri(), "https://id.execute-api.us-east-1.amazonaws.com/my/path-with%20space?parameter1=value1&parameter1=value2&parameter2=value");
1076    }
1077
1078    #[test]
1079    fn parse_paths_with_spaces() {
1080        let url = build_request_uri("/path with spaces/and multiple segments", &HeaderMap::new(), None, None);
1081        assert_eq!("/path%20with%20spaces/and%20multiple%20segments", url);
1082    }
1083
1084    #[test]
1085    fn deserializes_apigw_http_request_with_stage_in_path() {
1086        let input = include_str!("../tests/data/apigw_v2_proxy_request_with_stage_in_path.json");
1087        let result = from_str(input);
1088        assert!(
1089            result.is_ok(),
1090            "event was not parsed as expected {result:?} given {input}"
1091        );
1092        let req = result.expect("failed to parse request");
1093        assert_eq!("/Prod/my/path", req.uri().path());
1094        assert_eq!("/Prod/my/path", req.raw_http_path());
1095    }
1096
1097    #[test]
1098    fn test_apigw_path_with_stage() {
1099        assert_eq!("/path", apigw_path_with_stage(&None, "/path"));
1100        assert_eq!("/path", apigw_path_with_stage(&Some("$default".into()), "/path"));
1101        assert_eq!("/Prod/path", apigw_path_with_stage(&Some("Prod".into()), "/Prod/path"));
1102        assert_eq!("/Prod/path", apigw_path_with_stage(&Some("Prod".into()), "/path"));
1103    }
1104
1105    #[tokio::test]
1106    #[cfg(feature = "apigw_rest")]
1107    async fn test_axum_query_extractor_apigw_rest() {
1108        use axum_core::extract::FromRequestParts;
1109        use axum_extra::extract::Query;
1110        // from docs
1111        // https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
1112        let input = include_str!("../tests/data/apigw_multi_value_proxy_request.json");
1113        let request = from_str(input).expect("failed to parse request");
1114        let (mut parts, _) = request.into_parts();
1115
1116        #[derive(Deserialize)]
1117        #[serde(rename_all = "camelCase")]
1118        struct Params {
1119            name: Vec<String>,
1120            multi_value_name: Vec<String>,
1121        }
1122        struct State;
1123
1124        let query = Query::<Params>::from_request_parts(&mut parts, &State).await.unwrap();
1125        assert_eq!(vec!["me"], query.0.name);
1126        assert_eq!(vec!["you", "me"], query.0.multi_value_name);
1127    }
1128
1129    #[tokio::test]
1130    #[cfg(feature = "apigw_http")]
1131    async fn test_axum_query_extractor_apigw_http() {
1132        use axum_core::extract::FromRequestParts;
1133        use axum_extra::extract::Query;
1134        let input = include_str!("../tests/data/apigw_v2_proxy_request.json");
1135        let request = from_str(input).expect("failed to parse request");
1136        let (mut parts, _) = request.into_parts();
1137
1138        #[derive(Deserialize)]
1139        struct Params {
1140            parameter1: Vec<String>,
1141            parameter2: Vec<String>,
1142        }
1143        struct State;
1144
1145        let query = Query::<Params>::from_request_parts(&mut parts, &State).await.unwrap();
1146        assert_eq!(vec!["value1", "value2"], query.0.parameter1);
1147        assert_eq!(vec!["value"], query.0.parameter2);
1148    }
1149
1150    #[tokio::test]
1151    #[cfg(feature = "alb")]
1152    async fn test_axum_query_extractor_alb() {
1153        use axum_core::extract::FromRequestParts;
1154        use axum_extra::extract::Query;
1155        let input = include_str!("../tests/data/alb_multi_value_request.json");
1156        let request = from_str(input).expect("failed to parse request");
1157        let (mut parts, _) = request.into_parts();
1158
1159        #[derive(Deserialize)]
1160        #[serde(rename_all = "camelCase")]
1161        struct Params {
1162            my_key: Vec<String>,
1163            my_other_key: Vec<String>,
1164        }
1165        struct State;
1166
1167        let query = Query::<Params>::from_request_parts(&mut parts, &State).await.unwrap();
1168        assert_eq!(vec!["val1", "val2"], query.0.my_key);
1169        assert_eq!(vec!["val3", "val4"], query.0.my_other_key);
1170    }
1171
1172    #[test]
1173    #[cfg(feature = "apigw_rest")]
1174    fn deserializes_request_authorizer() {
1175        let input = include_str!("../../lambda-events/src/fixtures/example-apigw-request.json");
1176        let result = from_str(input);
1177        assert!(
1178            result.is_ok(),
1179            "event was not parsed as expected {result:?} given {input}"
1180        );
1181        let req = result.expect("failed to parse request");
1182
1183        let req_context = req.request_context_ref().expect("Request is missing RequestContext");
1184        let authorizer = req_context.authorizer().expect("authorizer is missing");
1185        assert_eq!(Some("admin"), authorizer.fields.get("principalId").unwrap().as_str());
1186    }
1187
1188    #[test]
1189    #[cfg(all(feature = "apigw_http", feature = "vpc_lattice"))]
1190    fn vpc_lattice_event_does_not_match_apigw_v2() {
1191        let data = include_bytes!("../../lambda-events/src/fixtures/example-vpc-lattice-v2-request.json");
1192        let result = serde_json::from_slice::<ApiGatewayV2httpRequest>(data);
1193        assert!(result.is_err(), "VPC Lattice event should not deserialize as APIGW V2");
1194    }
1195
1196    #[test]
1197    #[cfg(feature = "vpc_lattice")]
1198    fn vpc_lattice_method_none_defaults_to_get() {
1199        let input = r#"{
1200            "version": "2.0",
1201            "path": "/ping",
1202            "headers": {"accept": ["*/*"]},
1203            "queryStringParameters": {},
1204            "isBase64Encoded": false,
1205            "requestContext": {
1206                "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-1:123456789012:servicenetwork/sn-abc",
1207                "serviceArn": "arn:aws:vpc-lattice:us-east-1:123456789012:service/svc-abc",
1208                "targetGroupArn": "arn:aws:vpc-lattice:us-east-1:123456789012:targetgroup/tg-abc",
1209                "region": "us-east-1",
1210                "timeEpoch": "1724875399456789"
1211            }
1212        }"#;
1213        let result = from_str(input);
1214        assert!(result.is_ok(), "event was not parsed as expected {result:?}");
1215        let request = result.expect("failed to parse request");
1216        assert_eq!(request.method(), "GET");
1217    }
1218
1219    #[test]
1220    #[cfg(feature = "vpc_lattice")]
1221    fn vpc_lattice_post_with_body() {
1222        let input = r#"{
1223            "version": "2.0",
1224            "path": "/submit",
1225            "method": "POST",
1226            "headers": {"content-type": ["application/json"]},
1227            "queryStringParameters": {},
1228            "body": "{\"key\":\"value\"}",
1229            "isBase64Encoded": false,
1230            "requestContext": {
1231                "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-1:123456789012:servicenetwork/sn-abc",
1232                "serviceArn": "arn:aws:vpc-lattice:us-east-1:123456789012:service/svc-abc",
1233                "targetGroupArn": "arn:aws:vpc-lattice:us-east-1:123456789012:targetgroup/tg-abc",
1234                "region": "us-east-1",
1235                "timeEpoch": "1724875399456789"
1236            }
1237        }"#;
1238        let result = from_str(input);
1239        assert!(result.is_ok(), "event was not parsed as expected {result:?}");
1240        let request = result.expect("failed to parse request");
1241        assert_eq!(request.method(), "POST");
1242        let body_str = match request.body() {
1243            Body::Text(s) => s.as_str(),
1244            other => panic!("expected text body, got {other:?}"),
1245        };
1246        assert_eq!(body_str, r#"{"key":"value"}"#);
1247    }
1248}