1use crate::{Conn, IntoUrl, Pool, USER_AGENT, conn::H2Pooled, h3::H3ClientState};
2use std::{fmt::Debug, sync::Arc, time::Duration};
3use trillium_http::{
4 HeaderName, HeaderValues, Headers, HttpContext, KnownHeaderName, Method, ProtocolSession,
5 ReceivedBodyState, TypeSet, Version::Http1_1,
6};
7use trillium_server_common::{
8 ArcedConnector, ArcedQuicClientConfig, Connector, QuicClientConfig, Transport,
9 url::{Origin, Url},
10};
11
12const DEFAULT_H2_IDLE_TIMEOUT: Duration = Duration::from_secs(300);
16
17const DEFAULT_H2_IDLE_PING_THRESHOLD: Duration = Duration::from_secs(10);
20
21const DEFAULT_H2_IDLE_PING_TIMEOUT: Duration = Duration::from_secs(20);
24
25#[derive(Clone, Debug, fieldwork::Fieldwork)]
28pub struct Client {
29 config: ArcedConnector,
30 h3: Option<H3ClientState>,
31 pool: Option<Pool<Origin, Box<dyn Transport>>>,
32 h2_pool: Option<Pool<Origin, H2Pooled>>,
33
34 #[field(get, set, with, without, copy)]
38 h2_idle_timeout: Option<Duration>,
39
40 #[field(get, set, with, copy, without)]
45 h2_idle_ping_threshold: Option<Duration>,
46
47 #[field(get, set, with, copy)]
54 h2_idle_ping_timeout: Duration,
55
56 #[field(get)]
58 base: Option<Arc<Url>>,
59
60 #[field(get)]
62 default_headers: Arc<Headers>,
63
64 #[field(get, set, with, copy, without, option_set_some)]
66 timeout: Option<Duration>,
67
68 #[field(get, get_mut, set, with, into)]
70 context: Arc<HttpContext>,
71}
72
73macro_rules! method {
74 ($fn_name:ident, $method:ident) => {
75 method!(
76 $fn_name,
77 $method,
78 concat!(
79 "Builds a new client conn with the ",
81 stringify!($fn_name),
82 " http method and the provided url.
83
84```
85use trillium_client::{Client, Method};
86use trillium_testing::client_config;
87
88let client = Client::new(client_config());
89let conn = client.",
90 stringify!($fn_name),
91 "(\"http://localhost:8080/some/route\"); //<-
92
93assert_eq!(conn.method(), Method::",
94 stringify!($method),
95 ");
96assert_eq!(conn.url().to_string(), \"http://localhost:8080/some/route\");
97```
98"
99 )
100 );
101 };
102
103 ($fn_name:ident, $method:ident, $doc_comment:expr_2021) => {
104 #[doc = $doc_comment]
105 pub fn $fn_name(&self, url: impl IntoUrl) -> Conn {
106 self.build_conn(Method::$method, url)
107 }
108 };
109}
110
111pub(crate) fn default_request_headers() -> Headers {
112 Headers::new()
113 .with_inserted_header(KnownHeaderName::UserAgent, USER_AGENT)
114 .with_inserted_header(KnownHeaderName::Accept, "*/*")
115}
116
117impl Client {
118 method!(get, Get);
119
120 method!(post, Post);
121
122 method!(put, Put);
123
124 method!(delete, Delete);
125
126 method!(patch, Patch);
127
128 pub fn new(connector: impl Connector) -> Self {
130 Self {
131 config: ArcedConnector::new(connector),
132 h3: None,
133 pool: Some(Pool::default()),
134 h2_pool: Some(Pool::default()),
135 h2_idle_timeout: Some(DEFAULT_H2_IDLE_TIMEOUT),
136 h2_idle_ping_threshold: Some(DEFAULT_H2_IDLE_PING_THRESHOLD),
137 h2_idle_ping_timeout: DEFAULT_H2_IDLE_PING_TIMEOUT,
138 base: None,
139 default_headers: Arc::new(default_request_headers()),
140 timeout: None,
141 context: Default::default(),
142 }
143 }
144
145 pub fn new_with_quic<C: Connector, Q: QuicClientConfig<C>>(connector: C, quic: Q) -> Self {
155 let arced_quic = ArcedQuicClientConfig::new(&connector, quic);
157
158 #[cfg_attr(not(feature = "webtransport"), allow(unused_mut))]
159 let mut context = HttpContext::default();
160 #[cfg(feature = "webtransport")]
161 {
162 context
167 .config_mut()
168 .set_h3_datagrams_enabled(true)
169 .set_webtransport_enabled(true)
170 .set_extended_connect_enabled(true);
171 }
172
173 Self {
174 config: ArcedConnector::new(connector),
175 h3: Some(H3ClientState::new(arced_quic)),
176 pool: Some(Pool::default()),
177 h2_pool: Some(Pool::default()),
178 h2_idle_timeout: Some(DEFAULT_H2_IDLE_TIMEOUT),
179 h2_idle_ping_threshold: Some(DEFAULT_H2_IDLE_PING_THRESHOLD),
180 h2_idle_ping_timeout: DEFAULT_H2_IDLE_PING_TIMEOUT,
181 base: None,
182 default_headers: Arc::new(default_request_headers()),
183 timeout: None,
184 context: Arc::new(context),
185 }
186 }
187
188 pub fn without_default_header(mut self, name: impl Into<HeaderName<'static>>) -> Self {
190 self.default_headers_mut().remove(name);
191 self
192 }
193
194 pub fn with_default_header(
196 mut self,
197 name: impl Into<HeaderName<'static>>,
198 value: impl Into<HeaderValues>,
199 ) -> Self {
200 self.default_headers_mut().insert(name, value);
201 self
202 }
203
204 pub fn default_headers_mut(&mut self) -> &mut Headers {
208 Arc::make_mut(&mut self.default_headers)
209 }
210
211 pub fn without_keepalive(mut self) -> Self {
220 self.pool = None;
221 self.h2_pool = None;
222 self
223 }
224
225 pub fn build_conn<M>(&self, method: M, url: impl IntoUrl) -> Conn
242 where
243 M: TryInto<Method>,
244 <M as TryInto<Method>>::Error: Debug,
245 {
246 let method = method.try_into().unwrap();
247 let (url, request_target) = if let Some(base) = &self.base
248 && let Some(request_target) = url.request_target(method)
249 {
250 ((**base).clone(), Some(request_target))
251 } else {
252 (self.build_url(url).unwrap(), None)
253 };
254
255 Conn {
256 url,
257 method,
258 request_headers: Headers::clone(&self.default_headers),
259 response_headers: Headers::new(),
260 transport: None,
261 status: None,
262 request_body: None,
263 pool: self.pool.clone(),
264 h2_pool: self.h2_pool.clone(),
265 h2_idle_timeout: self.h2_idle_timeout,
266 h2_idle_ping_threshold: self.h2_idle_ping_threshold,
267 h2_idle_ping_timeout: self.h2_idle_ping_timeout,
268 h3_client_state: self.h3.clone(),
269 protocol_session: ProtocolSession::Http1,
270 #[cfg(feature = "webtransport")]
271 wt_pool_entry: None,
272 buffer: Vec::with_capacity(128).into(),
273 response_body_state: ReceivedBodyState::Start,
274 config: self.config.clone(),
275 headers_finalized: false,
276 timeout: self.timeout,
277 http_version: Http1_1,
278 max_head_length: 8 * 1024,
279 state: TypeSet::new(),
280 context: self.context.clone(),
281 authority: None,
282 scheme: None,
283 path: None,
284 request_target,
285 protocol: None,
286 request_trailers: None,
287 response_trailers: None,
288 }
289 }
290
291 pub fn connector(&self) -> &ArcedConnector {
293 &self.config
294 }
295
296 pub fn clean_up_pool(&self) {
301 if let Some(pool) = &self.pool {
302 pool.cleanup();
303 }
304 if let Some(h2_pool) = &self.h2_pool {
305 h2_pool.cleanup();
306 }
307 }
308
309 pub fn with_base(mut self, base: impl IntoUrl) -> Self {
311 self.set_base(base).unwrap();
312 self
313 }
314
315 pub fn build_url(&self, url: impl IntoUrl) -> crate::Result<Url> {
317 url.into_url(self.base())
318 }
319
320 pub fn set_base(&mut self, base: impl IntoUrl) -> crate::Result<()> {
322 let mut base = base.into_url(None)?;
323
324 if !base.path().ends_with('/') {
325 log::warn!("appending a trailing / to {base}");
326 base.set_path(&format!("{}/", base.path()));
327 }
328
329 self.base = Some(Arc::new(base));
330 Ok(())
331 }
332
333 pub fn base_mut(&mut self) -> Option<&mut Url> {
338 let base = self.base.as_mut()?;
339 Some(Arc::make_mut(base))
340 }
341}
342
343impl<T: Connector> From<T> for Client {
344 fn from(connector: T) -> Self {
345 Self::new(connector)
346 }
347}