aws_smithy_http_server/routing/
lambda_handler.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use http::uri;
7use lambda_http::{Request, RequestExt};
8use std::{
9    fmt::Debug,
10    task::{Context, Poll},
11};
12use tower::Service;
13
14type HyperRequest = http::Request<hyper::Body>;
15
16/// A [`Service`] that takes a `lambda_http::Request` and converts
17/// it to `http::Request<hyper::Body>`.
18///
19/// **This version is only guaranteed to be compatible with
20/// [`lambda_http`](https://docs.rs/lambda_http) ^0.7.0.** Please ensure that your service crate's
21/// `Cargo.toml` depends on a compatible version.
22///
23/// [`Service`]: tower::Service
24#[derive(Debug, Clone)]
25pub struct LambdaHandler<S> {
26    service: S,
27}
28
29impl<S> LambdaHandler<S> {
30    pub fn new(service: S) -> Self {
31        Self { service }
32    }
33}
34
35impl<S> Service<Request> for LambdaHandler<S>
36where
37    S: Service<HyperRequest>,
38{
39    type Error = S::Error;
40    type Response = S::Response;
41    type Future = S::Future;
42
43    #[inline]
44    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
45        self.service.poll_ready(cx)
46    }
47
48    fn call(&mut self, event: Request) -> Self::Future {
49        self.service.call(convert_event(event))
50    }
51}
52
53/// Converts a `lambda_http::Request` into a `http::Request<hyper::Body>`
54/// Issue: <https://github.com/smithy-lang/smithy-rs/issues/1125>
55///
56/// While converting the event the [API Gateway Stage] portion of the URI
57/// is removed from the uri that gets returned as a new `http::Request`.
58///
59/// [API Gateway Stage]: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-stages.html
60fn convert_event(request: Request) -> HyperRequest {
61    let raw_path: &str = request.extensions().raw_http_path();
62    let path: &str = request.uri().path();
63
64    let (parts, body) = if !raw_path.is_empty() && raw_path != path {
65        let mut path = raw_path.to_owned(); // Clone only when we need to strip out the stage.
66        let (mut parts, body) = request.into_parts();
67
68        let uri_parts: uri::Parts = parts.uri.into();
69        let path_and_query = uri_parts
70            .path_and_query
71            .expect("request URI does not have `PathAndQuery`");
72
73        if let Some(query) = path_and_query.query() {
74            path.push('?');
75            path.push_str(query);
76        }
77
78        parts.uri = uri::Uri::builder()
79            .authority(uri_parts.authority.expect("request URI does not have authority set"))
80            .scheme(uri_parts.scheme.expect("request URI does not have scheme set"))
81            .path_and_query(path)
82            .build()
83            .expect("unable to construct new URI");
84
85        (parts, body)
86    } else {
87        request.into_parts()
88    };
89
90    let body = match body {
91        lambda_http::Body::Empty => hyper::Body::empty(),
92        lambda_http::Body::Text(s) => hyper::Body::from(s),
93        lambda_http::Body::Binary(v) => hyper::Body::from(v),
94    };
95
96    http::Request::from_parts(parts, body)
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use lambda_http::RequestExt;
103
104    #[test]
105    fn traits() {
106        use crate::test_helpers::*;
107
108        assert_send::<LambdaHandler<()>>();
109        assert_sync::<LambdaHandler<()>>();
110    }
111
112    #[test]
113    fn raw_http_path() {
114        // lambda_http::Request doesn't have a fn `builder`
115        let event = http::Request::builder()
116            .uri("https://id.execute-api.us-east-1.amazonaws.com/prod/resources/1")
117            .body(())
118            .expect("unable to build Request");
119        let (parts, _) = event.into_parts();
120
121        // the lambda event will have a raw path which is the path without stage name in it
122        let event =
123            lambda_http::Request::from_parts(parts, lambda_http::Body::Empty).with_raw_http_path("/resources/1");
124        let request = convert_event(event);
125
126        assert_eq!(request.uri().path(), "/resources/1")
127    }
128}