lambda_runtime_api_client/
lib.rs

1#![deny(clippy::all, clippy::cargo)]
2#![warn(missing_docs, nonstandard_style, rust_2018_idioms)]
3#![allow(clippy::multiple_crate_versions)]
4#![cfg_attr(docsrs, feature(doc_cfg))]
5
6//! This crate includes a base HTTP client to interact with
7//! the AWS Lambda Runtime API.
8use futures_util::{future::BoxFuture, FutureExt, TryFutureExt};
9use http::{
10    uri::{PathAndQuery, Scheme},
11    Request, Response, Uri,
12};
13use hyper::body::Incoming;
14use hyper_util::client::legacy::connect::HttpConnector;
15use std::{convert::TryInto, fmt::Debug, future};
16
17const USER_AGENT_HEADER: &str = "User-Agent";
18const DEFAULT_USER_AGENT: &str = concat!("aws-lambda-rust/", env!("CARGO_PKG_VERSION"));
19const CUSTOM_USER_AGENT: Option<&str> = option_env!("LAMBDA_RUNTIME_USER_AGENT");
20
21mod error;
22pub use error::*;
23pub mod body;
24
25#[cfg(feature = "tracing")]
26#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
27pub mod tracing;
28
29/// API client to interact with the AWS Lambda Runtime API.
30#[derive(Debug)]
31pub struct Client {
32    /// The runtime API URI
33    pub base: Uri,
34    /// The client that manages the API connections
35    pub client: hyper_util::client::legacy::Client<HttpConnector, body::Body>,
36}
37
38impl Client {
39    /// Create a builder struct to configure the client.
40    pub fn builder() -> ClientBuilder {
41        ClientBuilder {
42            connector: HttpConnector::new(),
43            uri: None,
44        }
45    }
46}
47
48impl Client {
49    /// Send a given request to the Runtime API.
50    /// Use the client's base URI to ensure the API endpoint is correct.
51    pub fn call(&self, req: Request<body::Body>) -> BoxFuture<'static, Result<Response<Incoming>, BoxError>> {
52        // NOTE: This method returns a boxed future such that the future has a static lifetime.
53        //       Due to limitations around the Rust async implementation as of Mar 2024, this is
54        //       required to minimize constraints on the handler passed to [lambda_runtime::run].
55        let req = match self.set_origin(req) {
56            Ok(req) => req,
57            Err(err) => return future::ready(Err(err)).boxed(),
58        };
59        self.client.request(req).map_err(Into::into).boxed()
60    }
61
62    /// Create a new client with a given base URI and HTTP connector.
63    fn with(base: Uri, connector: HttpConnector) -> Self {
64        let client = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
65            .http1_max_buf_size(1024 * 1024)
66            .build(connector);
67        Self { base, client }
68    }
69
70    fn set_origin<B>(&self, req: Request<B>) -> Result<Request<B>, BoxError> {
71        let (mut parts, body) = req.into_parts();
72        let (scheme, authority, base_path) = {
73            let scheme = self.base.scheme().unwrap_or(&Scheme::HTTP);
74            let authority = self.base.authority().expect("Authority not found");
75            let base_path = self.base.path().trim_end_matches('/');
76            (scheme, authority, base_path)
77        };
78        let path = parts.uri.path_and_query().expect("PathAndQuery not found");
79        let pq: PathAndQuery = format!("{base_path}{path}").parse().expect("PathAndQuery invalid");
80
81        let uri = Uri::builder()
82            .scheme(scheme.as_ref())
83            .authority(authority.as_ref())
84            .path_and_query(pq)
85            .build()
86            .map_err(Box::new)?;
87
88        parts.uri = uri;
89        Ok(Request::from_parts(parts, body))
90    }
91}
92
93/// Builder implementation to construct any Runtime API clients.
94pub struct ClientBuilder {
95    connector: HttpConnector,
96    uri: Option<http::Uri>,
97}
98
99impl ClientBuilder {
100    /// Create a new builder with a given HTTP connector.
101    pub fn with_connector(self, connector: HttpConnector) -> ClientBuilder {
102        ClientBuilder {
103            connector,
104            uri: self.uri,
105        }
106    }
107
108    /// Create a new builder with a given base URI.
109    /// Inherits all other attributes from the existent builder.
110    pub fn with_endpoint(self, uri: http::Uri) -> Self {
111        Self { uri: Some(uri), ..self }
112    }
113
114    /// Create the new client to interact with the Runtime API.
115    pub fn build(self) -> Result<Client, Error> {
116        let uri = match self.uri {
117            Some(uri) => uri,
118            None => {
119                let uri = std::env::var("AWS_LAMBDA_RUNTIME_API").expect("Missing AWS_LAMBDA_RUNTIME_API env var");
120                uri.try_into().expect("Unable to convert to URL")
121            }
122        };
123        Ok(Client::with(uri, self.connector))
124    }
125}
126
127/// Create a request builder.
128/// This builder uses `aws-lambda-rust/CRATE_VERSION` as
129/// the default User-Agent.
130/// Configure environment variable `LAMBDA_RUNTIME_USER_AGENT`
131/// at compile time to modify User-Agent value.
132pub fn build_request() -> http::request::Builder {
133    const USER_AGENT: &str = match CUSTOM_USER_AGENT {
134        Some(value) => value,
135        None => DEFAULT_USER_AGENT,
136    };
137    http::Request::builder().header(USER_AGENT_HEADER, USER_AGENT)
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_set_origin() {
146        let base = "http://localhost:9001";
147        let client = Client::builder().with_endpoint(base.parse().unwrap()).build().unwrap();
148        let req = build_request()
149            .uri("/2018-06-01/runtime/invocation/next")
150            .body(())
151            .unwrap();
152        let req = client.set_origin(req).unwrap();
153        assert_eq!(
154            "http://localhost:9001/2018-06-01/runtime/invocation/next",
155            &req.uri().to_string()
156        );
157    }
158
159    #[test]
160    fn test_set_origin_with_base_path() {
161        let base = "http://localhost:9001/foo";
162        let client = Client::builder().with_endpoint(base.parse().unwrap()).build().unwrap();
163        let req = build_request()
164            .uri("/2018-06-01/runtime/invocation/next")
165            .body(())
166            .unwrap();
167        let req = client.set_origin(req).unwrap();
168        assert_eq!(
169            "http://localhost:9001/foo/2018-06-01/runtime/invocation/next",
170            &req.uri().to_string()
171        );
172
173        let base = "http://localhost:9001/foo/";
174        let client = Client::builder().with_endpoint(base.parse().unwrap()).build().unwrap();
175        let req = build_request()
176            .uri("/2018-06-01/runtime/invocation/next")
177            .body(())
178            .unwrap();
179        let req = client.set_origin(req).unwrap();
180        assert_eq!(
181            "http://localhost:9001/foo/2018-06-01/runtime/invocation/next",
182            &req.uri().to_string()
183        );
184    }
185}