#![deny(clippy::all, clippy::cargo)]
#![warn(missing_docs, nonstandard_style, rust_2018_idioms)]
#![allow(clippy::multiple_crate_versions)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use futures_util::{future::BoxFuture, FutureExt, TryFutureExt};
use http::{
uri::{PathAndQuery, Scheme},
Request, Response, Uri,
};
use hyper::body::Incoming;
use hyper_util::client::legacy::connect::HttpConnector;
use std::{convert::TryInto, fmt::Debug, future, sync::OnceLock};
const USER_AGENT_HEADER: &str = "User-Agent";
const DEFAULT_USER_AGENT: &str = concat!("aws-lambda-rust/", env!("CARGO_PKG_VERSION"));
const CUSTOM_USER_AGENT: Option<&str> = option_env!("LAMBDA_RUNTIME_USER_AGENT");
mod error;
pub use error::*;
pub mod body;
#[cfg(feature = "tracing")]
#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
pub mod tracing;
pub trait RuntimeApiClient {
fn call(&self, req: Request<body::Body>) -> BoxFuture<'static, Result<Response<Incoming>, BoxError>>;
}
#[derive(Debug)]
pub struct Client {
pub base: Uri,
pub client: hyper_util::client::legacy::Client<HttpConnector, body::Body>,
}
impl Client {
pub fn builder() -> ClientBuilder {
ClientBuilder::new()
}
pub fn call(&self, req: Request<body::Body>) -> BoxFuture<'static, Result<Response<Incoming>, BoxError>> {
<Self as RuntimeApiClient>::call(self, req)
}
fn with(base: Uri, connector: HttpConnector, pool_size: Option<usize>) -> Self {
Self {
base,
client: build_hyper_client(&connector, pool_size),
}
}
}
impl RuntimeApiClient for Client {
fn call(&self, req: Request<body::Body>) -> BoxFuture<'static, Result<Response<Incoming>, BoxError>> {
let req = match set_origin(&self.base, req) {
Ok(req) => req,
Err(err) => return future::ready(Err(err)).boxed(),
};
self.client.request(req).map_err(Into::into).boxed()
}
}
#[derive(Debug)]
pub struct PooledClient {
pub base: Uri,
client: hyper_util::client::legacy::Client<HttpConnector, body::Body>,
connector: HttpConnector,
pool_size: Option<usize>,
restored: OnceLock<hyper_util::client::legacy::Client<HttpConnector, body::Body>>,
}
impl PooledClient {
pub fn builder() -> PooledClientBuilder {
PooledClientBuilder::new()
}
pub fn call(&self, req: Request<body::Body>) -> BoxFuture<'static, Result<Response<Incoming>, BoxError>> {
<Self as RuntimeApiClient>::call(self, req)
}
fn with(base: Uri, connector: HttpConnector, pool_size: Option<usize>) -> Self {
let client = build_hyper_client(&connector, pool_size);
Self {
base,
client,
connector,
pool_size,
restored: OnceLock::new(),
}
}
pub fn reset_pool(&self) {
let _ = self.restored.set(build_hyper_client(&self.connector, self.pool_size));
}
}
impl RuntimeApiClient for PooledClient {
fn call(&self, req: Request<body::Body>) -> BoxFuture<'static, Result<Response<Incoming>, BoxError>> {
let req = match set_origin(&self.base, req) {
Ok(req) => req,
Err(err) => return future::ready(Err(err)).boxed(),
};
let hyper_client = self.restored.get().unwrap_or(&self.client);
hyper_client.request(req).map_err(Into::into).boxed()
}
}
fn build_hyper_client(
connector: &HttpConnector,
pool_size: Option<usize>,
) -> hyper_util::client::legacy::Client<HttpConnector, body::Body> {
let mut builder = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new());
builder.http1_max_buf_size(1024 * 1024);
if let Some(size) = pool_size {
builder.pool_max_idle_per_host(size);
}
builder.build(connector.clone())
}
fn set_origin<B>(base: &Uri, req: Request<B>) -> Result<Request<B>, BoxError> {
let (mut parts, body) = req.into_parts();
let (scheme, authority, base_path) = {
let scheme = base.scheme().unwrap_or(&Scheme::HTTP);
let authority = base.authority().expect("Authority not found");
let base_path = base.path().trim_end_matches('/');
(scheme, authority, base_path)
};
let path = parts.uri.path_and_query().expect("PathAndQuery not found");
let pq: PathAndQuery = format!("{base_path}{path}").parse().expect("PathAndQuery invalid");
let uri = Uri::builder()
.scheme(scheme.as_ref())
.authority(authority.as_ref())
.path_and_query(pq)
.build()
.map_err(Box::new)?;
parts.uri = uri;
Ok(Request::from_parts(parts, body))
}
pub struct ClientBuilder {
connector: HttpConnector,
uri: Option<http::Uri>,
pool_size: Option<usize>,
}
impl ClientBuilder {
fn new() -> ClientBuilder {
ClientBuilder {
connector: HttpConnector::new(),
uri: None,
pool_size: None,
}
}
pub fn with_connector(self, connector: HttpConnector) -> ClientBuilder {
ClientBuilder {
connector,
uri: self.uri,
pool_size: self.pool_size,
}
}
pub fn with_endpoint(self, uri: http::Uri) -> Self {
Self { uri: Some(uri), ..self }
}
pub fn with_pool_size(self, pool_size: usize) -> Self {
Self {
pool_size: Some(pool_size),
..self
}
}
pub fn build(self) -> Result<Client, Error> {
let uri = resolve_uri(self.uri);
Ok(Client::with(uri, self.connector, self.pool_size))
}
}
pub struct PooledClientBuilder {
connector: HttpConnector,
uri: Option<http::Uri>,
pool_size: Option<usize>,
}
impl PooledClientBuilder {
fn new() -> PooledClientBuilder {
PooledClientBuilder {
connector: HttpConnector::new(),
uri: None,
pool_size: None,
}
}
pub fn with_connector(self, connector: HttpConnector) -> PooledClientBuilder {
PooledClientBuilder { connector, ..self }
}
pub fn with_endpoint(self, uri: http::Uri) -> Self {
Self { uri: Some(uri), ..self }
}
pub fn with_pool_size(self, pool_size: usize) -> Self {
Self {
pool_size: Some(pool_size),
..self
}
}
pub fn build(self) -> PooledClient {
let uri = resolve_uri(self.uri);
PooledClient::with(uri, self.connector, self.pool_size)
}
}
fn resolve_uri(uri: Option<http::Uri>) -> Uri {
match uri {
Some(uri) => uri,
None => {
let uri = std::env::var("AWS_LAMBDA_RUNTIME_API").expect("Missing AWS_LAMBDA_RUNTIME_API env var");
uri.try_into().expect("Unable to convert to URL")
}
}
}
pub fn build_request() -> http::request::Builder {
const USER_AGENT: &str = match CUSTOM_USER_AGENT {
Some(value) => value,
None => DEFAULT_USER_AGENT,
};
http::Request::builder().header(USER_AGENT_HEADER, USER_AGENT)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_set_origin() {
let base: Uri = "http://localhost:9001".parse().unwrap();
let req = build_request()
.uri("/2018-06-01/runtime/invocation/next")
.body(())
.unwrap();
let req = set_origin(&base, req).unwrap();
assert_eq!(
"http://localhost:9001/2018-06-01/runtime/invocation/next",
&req.uri().to_string()
);
}
#[test]
fn test_set_origin_with_base_path() {
let base: Uri = "http://localhost:9001/foo".parse().unwrap();
let req = build_request()
.uri("/2018-06-01/runtime/invocation/next")
.body(())
.unwrap();
let req = set_origin(&base, req).unwrap();
assert_eq!(
"http://localhost:9001/foo/2018-06-01/runtime/invocation/next",
&req.uri().to_string()
);
let base: Uri = "http://localhost:9001/foo/".parse().unwrap();
let req = build_request()
.uri("/2018-06-01/runtime/invocation/next")
.body(())
.unwrap();
let req = set_origin(&base, req).unwrap();
assert_eq!(
"http://localhost:9001/foo/2018-06-01/runtime/invocation/next",
&req.uri().to_string()
);
}
#[test]
fn builder_accepts_pool_size() {
let base = "http://localhost:9001";
let expected: Uri = base.parse().unwrap();
let client = PooledClient::builder()
.with_pool_size(4)
.with_endpoint(base.parse().unwrap())
.build();
assert_eq!(client.base, expected);
}
#[test]
fn test_reset_pool() {
let base = "http://localhost:9001";
let client = PooledClient::builder()
.with_pool_size(4)
.with_endpoint(base.parse().unwrap())
.build();
client.reset_pool();
assert_eq!(client.base, base.parse::<Uri>().unwrap());
}
}