Skip to main content

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            pool_size: None,
45        }
46    }
47}
48
49impl Client {
50    /// Send a given request to the Runtime API.
51    /// Use the client's base URI to ensure the API endpoint is correct.
52    pub fn call(&self, req: Request<body::Body>) -> BoxFuture<'static, Result<Response<Incoming>, BoxError>> {
53        // NOTE: This method returns a boxed future such that the future has a static lifetime.
54        //       Due to limitations around the Rust async implementation as of Mar 2024, this is
55        //       required to minimize constraints on the handler passed to [lambda_runtime::run].
56        let req = match self.set_origin(req) {
57            Ok(req) => req,
58            Err(err) => return future::ready(Err(err)).boxed(),
59        };
60        self.client.request(req).map_err(Into::into).boxed()
61    }
62
63    /// Create a new client with a given base URI, HTTP connector, and optional pool size hint.
64    fn with(base: Uri, connector: HttpConnector, pool_size: Option<usize>) -> Self {
65        let mut builder = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new());
66        builder.http1_max_buf_size(1024 * 1024);
67
68        if let Some(size) = pool_size {
69            builder.pool_max_idle_per_host(size);
70        }
71
72        let client = builder.build(connector);
73        Self { base, client }
74    }
75
76    fn set_origin<B>(&self, req: Request<B>) -> Result<Request<B>, BoxError> {
77        let (mut parts, body) = req.into_parts();
78        let (scheme, authority, base_path) = {
79            let scheme = self.base.scheme().unwrap_or(&Scheme::HTTP);
80            let authority = self.base.authority().expect("Authority not found");
81            let base_path = self.base.path().trim_end_matches('/');
82            (scheme, authority, base_path)
83        };
84        let path = parts.uri.path_and_query().expect("PathAndQuery not found");
85        let pq: PathAndQuery = format!("{base_path}{path}").parse().expect("PathAndQuery invalid");
86
87        let uri = Uri::builder()
88            .scheme(scheme.as_ref())
89            .authority(authority.as_ref())
90            .path_and_query(pq)
91            .build()
92            .map_err(Box::new)?;
93
94        parts.uri = uri;
95        Ok(Request::from_parts(parts, body))
96    }
97}
98
99/// Builder implementation to construct any Runtime API clients.
100pub struct ClientBuilder {
101    connector: HttpConnector,
102    uri: Option<http::Uri>,
103    pool_size: Option<usize>,
104}
105
106impl ClientBuilder {
107    /// Create a new builder with a given HTTP connector.
108    pub fn with_connector(self, connector: HttpConnector) -> ClientBuilder {
109        ClientBuilder {
110            connector,
111            uri: self.uri,
112            pool_size: self.pool_size,
113        }
114    }
115
116    /// Create a new builder with a given base URI.
117    /// Inherits all other attributes from the existent builder.
118    pub fn with_endpoint(self, uri: http::Uri) -> Self {
119        Self { uri: Some(uri), ..self }
120    }
121
122    /// Provide a pool size hint for the underlying Hyper client.
123    ///
124    /// When using concurrent polling, this should be at least the maximum
125    /// concurrency (e.g., `AWS_LAMBDA_MAX_CONCURRENCY`) to avoid connection
126    /// starvation.
127    pub fn with_pool_size(self, pool_size: usize) -> Self {
128        Self {
129            pool_size: Some(pool_size),
130            ..self
131        }
132    }
133
134    /// Create the new client to interact with the Runtime API.
135    pub fn build(self) -> Result<Client, Error> {
136        let uri = match self.uri {
137            Some(uri) => uri,
138            None => {
139                let uri = std::env::var("AWS_LAMBDA_RUNTIME_API").expect("Missing AWS_LAMBDA_RUNTIME_API env var");
140                uri.try_into().expect("Unable to convert to URL")
141            }
142        };
143        Ok(Client::with(uri, self.connector, self.pool_size))
144    }
145}
146
147/// Create a request builder.
148/// This builder uses `aws-lambda-rust/CRATE_VERSION` as
149/// the default User-Agent.
150/// Configure environment variable `LAMBDA_RUNTIME_USER_AGENT`
151/// at compile time to modify User-Agent value.
152pub fn build_request() -> http::request::Builder {
153    const USER_AGENT: &str = match CUSTOM_USER_AGENT {
154        Some(value) => value,
155        None => DEFAULT_USER_AGENT,
156    };
157    http::Request::builder().header(USER_AGENT_HEADER, USER_AGENT)
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_set_origin() {
166        let base = "http://localhost:9001";
167        let client = Client::builder().with_endpoint(base.parse().unwrap()).build().unwrap();
168        let req = build_request()
169            .uri("/2018-06-01/runtime/invocation/next")
170            .body(())
171            .unwrap();
172        let req = client.set_origin(req).unwrap();
173        assert_eq!(
174            "http://localhost:9001/2018-06-01/runtime/invocation/next",
175            &req.uri().to_string()
176        );
177    }
178
179    #[test]
180    fn test_set_origin_with_base_path() {
181        let base = "http://localhost:9001/foo";
182        let client = Client::builder().with_endpoint(base.parse().unwrap()).build().unwrap();
183        let req = build_request()
184            .uri("/2018-06-01/runtime/invocation/next")
185            .body(())
186            .unwrap();
187        let req = client.set_origin(req).unwrap();
188        assert_eq!(
189            "http://localhost:9001/foo/2018-06-01/runtime/invocation/next",
190            &req.uri().to_string()
191        );
192
193        let base = "http://localhost:9001/foo/";
194        let client = Client::builder().with_endpoint(base.parse().unwrap()).build().unwrap();
195        let req = build_request()
196            .uri("/2018-06-01/runtime/invocation/next")
197            .body(())
198            .unwrap();
199        let req = client.set_origin(req).unwrap();
200        assert_eq!(
201            "http://localhost:9001/foo/2018-06-01/runtime/invocation/next",
202            &req.uri().to_string()
203        );
204    }
205
206    #[test]
207    fn builder_accepts_pool_size() {
208        let base = "http://localhost:9001";
209        let expected: Uri = base.parse().unwrap();
210        let client = Client::builder()
211            .with_pool_size(4)
212            .with_endpoint(base.parse().unwrap())
213            .build()
214            .unwrap();
215
216        assert_eq!(client.base, expected);
217    }
218}