lambda_runtime_api_client/
lib.rs1#![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
6use 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#[derive(Debug)]
31pub struct Client {
32 pub base: Uri,
34 pub client: hyper_util::client::legacy::Client<HttpConnector, body::Body>,
36}
37
38impl Client {
39 pub fn builder() -> ClientBuilder {
41 ClientBuilder {
42 connector: HttpConnector::new(),
43 uri: None,
44 pool_size: None,
45 }
46 }
47}
48
49impl Client {
50 pub fn call(&self, req: Request<body::Body>) -> BoxFuture<'static, Result<Response<Incoming>, BoxError>> {
53 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 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
99pub struct ClientBuilder {
101 connector: HttpConnector,
102 uri: Option<http::Uri>,
103 pool_size: Option<usize>,
104}
105
106impl ClientBuilder {
107 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 pub fn with_endpoint(self, uri: http::Uri) -> Self {
119 Self { uri: Some(uri), ..self }
120 }
121
122 pub fn with_pool_size(self, pool_size: usize) -> Self {
128 Self {
129 pool_size: Some(pool_size),
130 ..self
131 }
132 }
133
134 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
147pub 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}