use crate::{Conn, IntoUrl, Pool, USER_AGENT, conn::H2Pooled, h3::H3ClientState};
use std::{fmt::Debug, sync::Arc, time::Duration};
use trillium_http::{
HeaderName, HeaderValues, Headers, HttpContext, KnownHeaderName, Method, ProtocolSession,
ReceivedBodyState, TypeSet, Version::Http1_1,
};
use trillium_server_common::{
ArcedConnector, ArcedQuicClientConfig, Connector, QuicClientConfig, Transport,
url::{Origin, Url},
};
const DEFAULT_H2_IDLE_TIMEOUT: Duration = Duration::from_secs(300);
const DEFAULT_H2_IDLE_PING_THRESHOLD: Duration = Duration::from_secs(10);
const DEFAULT_H2_IDLE_PING_TIMEOUT: Duration = Duration::from_secs(20);
#[derive(Clone, Debug, fieldwork::Fieldwork)]
pub struct Client {
config: ArcedConnector,
h3: Option<H3ClientState>,
pool: Option<Pool<Origin, Box<dyn Transport>>>,
h2_pool: Option<Pool<Origin, H2Pooled>>,
#[field(get, set, with, without, copy)]
h2_idle_timeout: Option<Duration>,
#[field(get, set, with, copy, without)]
h2_idle_ping_threshold: Option<Duration>,
#[field(get, set, with, copy)]
h2_idle_ping_timeout: Duration,
#[field(get)]
base: Option<Arc<Url>>,
#[field(get)]
default_headers: Arc<Headers>,
#[field(get, set, with, copy, without, option_set_some)]
timeout: Option<Duration>,
#[field(get, get_mut, set, with, into)]
context: Arc<HttpContext>,
}
macro_rules! method {
($fn_name:ident, $method:ident) => {
method!(
$fn_name,
$method,
concat!(
"Builds a new client conn with the ",
stringify!($fn_name),
" http method and the provided url.
```
use trillium_client::{Client, Method};
use trillium_testing::client_config;
let client = Client::new(client_config());
let conn = client.",
stringify!($fn_name),
"(\"http://localhost:8080/some/route\"); //<-
assert_eq!(conn.method(), Method::",
stringify!($method),
");
assert_eq!(conn.url().to_string(), \"http://localhost:8080/some/route\");
```
"
)
);
};
($fn_name:ident, $method:ident, $doc_comment:expr_2021) => {
#[doc = $doc_comment]
pub fn $fn_name(&self, url: impl IntoUrl) -> Conn {
self.build_conn(Method::$method, url)
}
};
}
pub(crate) fn default_request_headers() -> Headers {
Headers::new()
.with_inserted_header(KnownHeaderName::UserAgent, USER_AGENT)
.with_inserted_header(KnownHeaderName::Accept, "*/*")
}
impl Client {
method!(get, Get);
method!(post, Post);
method!(put, Put);
method!(delete, Delete);
method!(patch, Patch);
pub fn new(connector: impl Connector) -> Self {
Self {
config: ArcedConnector::new(connector),
h3: None,
pool: Some(Pool::default()),
h2_pool: Some(Pool::default()),
h2_idle_timeout: Some(DEFAULT_H2_IDLE_TIMEOUT),
h2_idle_ping_threshold: Some(DEFAULT_H2_IDLE_PING_THRESHOLD),
h2_idle_ping_timeout: DEFAULT_H2_IDLE_PING_TIMEOUT,
base: None,
default_headers: Arc::new(default_request_headers()),
timeout: None,
context: Default::default(),
}
}
pub fn new_with_quic<C: Connector, Q: QuicClientConfig<C>>(connector: C, quic: Q) -> Self {
let arced_quic = ArcedQuicClientConfig::new(&connector, quic);
#[cfg_attr(not(feature = "webtransport"), allow(unused_mut))]
let mut context = HttpContext::default();
#[cfg(feature = "webtransport")]
{
context
.config_mut()
.set_h3_datagrams_enabled(true)
.set_webtransport_enabled(true)
.set_extended_connect_enabled(true);
}
Self {
config: ArcedConnector::new(connector),
h3: Some(H3ClientState::new(arced_quic)),
pool: Some(Pool::default()),
h2_pool: Some(Pool::default()),
h2_idle_timeout: Some(DEFAULT_H2_IDLE_TIMEOUT),
h2_idle_ping_threshold: Some(DEFAULT_H2_IDLE_PING_THRESHOLD),
h2_idle_ping_timeout: DEFAULT_H2_IDLE_PING_TIMEOUT,
base: None,
default_headers: Arc::new(default_request_headers()),
timeout: None,
context: Arc::new(context),
}
}
pub fn without_default_header(mut self, name: impl Into<HeaderName<'static>>) -> Self {
self.default_headers_mut().remove(name);
self
}
pub fn with_default_header(
mut self,
name: impl Into<HeaderName<'static>>,
value: impl Into<HeaderValues>,
) -> Self {
self.default_headers_mut().insert(name, value);
self
}
pub fn default_headers_mut(&mut self) -> &mut Headers {
Arc::make_mut(&mut self.default_headers)
}
pub fn without_keepalive(mut self) -> Self {
self.pool = None;
self.h2_pool = None;
self
}
pub fn build_conn<M>(&self, method: M, url: impl IntoUrl) -> Conn
where
M: TryInto<Method>,
<M as TryInto<Method>>::Error: Debug,
{
let method = method.try_into().unwrap();
let (url, request_target) = if let Some(base) = &self.base
&& let Some(request_target) = url.request_target(method)
{
((**base).clone(), Some(request_target))
} else {
(self.build_url(url).unwrap(), None)
};
Conn {
url,
method,
request_headers: Headers::clone(&self.default_headers),
response_headers: Headers::new(),
transport: None,
status: None,
request_body: None,
pool: self.pool.clone(),
h2_pool: self.h2_pool.clone(),
h2_idle_timeout: self.h2_idle_timeout,
h2_idle_ping_threshold: self.h2_idle_ping_threshold,
h2_idle_ping_timeout: self.h2_idle_ping_timeout,
h3_client_state: self.h3.clone(),
protocol_session: ProtocolSession::Http1,
#[cfg(feature = "webtransport")]
wt_pool_entry: None,
buffer: Vec::with_capacity(128).into(),
response_body_state: ReceivedBodyState::Start,
config: self.config.clone(),
headers_finalized: false,
timeout: self.timeout,
http_version: Http1_1,
max_head_length: 8 * 1024,
state: TypeSet::new(),
context: self.context.clone(),
authority: None,
scheme: None,
path: None,
request_target,
protocol: None,
request_trailers: None,
response_trailers: None,
}
}
pub fn connector(&self) -> &ArcedConnector {
&self.config
}
pub fn clean_up_pool(&self) {
if let Some(pool) = &self.pool {
pool.cleanup();
}
if let Some(h2_pool) = &self.h2_pool {
h2_pool.cleanup();
}
}
pub fn with_base(mut self, base: impl IntoUrl) -> Self {
self.set_base(base).unwrap();
self
}
pub fn build_url(&self, url: impl IntoUrl) -> crate::Result<Url> {
url.into_url(self.base())
}
pub fn set_base(&mut self, base: impl IntoUrl) -> crate::Result<()> {
let mut base = base.into_url(None)?;
if !base.path().ends_with('/') {
log::warn!("appending a trailing / to {base}");
base.set_path(&format!("{}/", base.path()));
}
self.base = Some(Arc::new(base));
Ok(())
}
pub fn base_mut(&mut self) -> Option<&mut Url> {
let base = self.base.as_mut()?;
Some(Arc::make_mut(base))
}
}
impl<T: Connector> From<T> for Client {
fn from(connector: T) -> Self {
Self::new(connector)
}
}