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