Skip to main content

ntex/client/
cfg.rs

1use std::{fmt, time::Duration};
2
3use base64::{Engine, engine::general_purpose::STANDARD as base64};
4
5use crate::http::header::{self, HeaderName, HeaderValue};
6use crate::http::{HeaderMap, error::HttpError};
7use crate::service::cfg::{CfgContext, Configuration};
8use crate::time::{Millis, Seconds};
9
10#[derive(Debug)]
11/// Runtime configuration for an HTTP [`Client`](super::Client).
12///
13/// The configuration is stored in [`SharedCfg`](crate::SharedCfg) and can be
14/// supplied before constructing a client with [`Client::with_config`](super::Client::with_config)
15/// or [`ClientBuilder::build`](super::ClientBuilder::build).
16pub struct ClientConfig {
17    pub(super) headers: HeaderMap,
18    pub(super) timeout: Millis,
19    pub(super) pl_limit: usize,
20    pub(super) pl_timeout: Millis,
21    pub(super) h1_lifetime: Duration,
22    pub(super) h1_keep_alive: Duration,
23    pub(super) h1_limit: usize,
24    pub(super) h2_lifetime: Duration,
25    pub(super) h2_keep_alive: Duration,
26    pub(super) h2_limit: usize,
27    pub(super) h2_max_streams: u32,
28
29    config: CfgContext,
30}
31
32impl Default for ClientConfig {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl Configuration for ClientConfig {
39    const NAME: &str = "Http client configuration";
40
41    fn ctx(&self) -> &CfgContext {
42        &self.config
43    }
44
45    fn set_ctx(&mut self, ctx: CfgContext) {
46        self.config = ctx;
47    }
48}
49
50impl ClientConfig {
51    #[must_use]
52    /// Creates an HTTP client configuration with default values.
53    pub fn new() -> ClientConfig {
54        ClientConfig {
55            headers: HeaderMap::new(),
56            timeout: Millis(5_000),
57            pl_limit: 262_144,
58            pl_timeout: Millis(10_000),
59            h1_lifetime: Duration::from_secs(75),
60            h1_keep_alive: Duration::from_secs(15),
61            h1_limit: 8,
62            h2_lifetime: Duration::from_hours(1),
63            h2_keep_alive: Duration::from_mins(1),
64            h2_limit: 16,
65            h2_max_streams: 100,
66
67            config: CfgContext::default(),
68        }
69    }
70
71    /// Returns the headers added to every request.
72    pub fn headers(&self) -> &HeaderMap {
73        &self.headers
74    }
75
76    /// Returns the response-header timeout.
77    pub fn response_timeout(&self) -> Millis {
78        self.timeout
79    }
80
81    /// Returns the maximum response payload size.
82    ///
83    /// A value of zero disables the limit.
84    pub fn response_payload_limit(&self) -> usize {
85        self.pl_limit
86    }
87
88    /// Returns the timeout for reading a complete response payload.
89    pub fn response_payload_timeout(&self) -> Millis {
90        self.pl_timeout
91    }
92
93    /// Returns the maximum number of simultaneous HTTP/1 connections per connection pool.
94    pub fn h1_connection_limit(&self) -> usize {
95        self.h1_limit
96    }
97
98    /// Returns the maximum number of HTTP/2 connections per host.
99    pub fn h2_connection_limit(&self) -> usize {
100        self.h2_limit
101    }
102
103    /// Returns the maximum number of concurrent requests per HTTP/2 connection.
104    pub fn h2_max_streams(&self) -> u32 {
105        self.h2_max_streams
106    }
107
108    /// Returns the keep-alive period for idle HTTP/2 connections.
109    pub fn h2_keepalive(&self) -> Seconds {
110        Seconds(self.h2_keep_alive.as_secs().try_into().unwrap_or(u16::MAX))
111    }
112
113    /// Returns the maximum lifetime of an HTTP/2 connection.
114    pub fn h2_lifetime(&self) -> Seconds {
115        Seconds(self.h2_lifetime.as_secs().try_into().unwrap_or(u16::MAX))
116    }
117
118    #[must_use]
119    /// Sets the maximum number of simultaneous connections per connection pool.
120    ///
121    /// The limit is shared by all hosts. A client keeps separate pools for
122    /// plain and TLS connections, and each pool has its own limit. The limit
123    /// counts HTTP/1 connections in use and connections being opened;
124    /// HTTP/2 connections are limited by
125    /// [`set_h2_connection_limit`](Self::set_h2_connection_limit). A value of
126    /// zero disables the limit. The default is 8.
127    pub fn set_h1_connection_limit(mut self, limit: usize) -> Self {
128        self.h1_limit = limit;
129        self
130    }
131
132    #[must_use]
133    /// Sets the keep-alive period for idle pooled HTTP/1 connections.
134    ///
135    /// HTTP/2 connections use [`set_h2_keepalive`](Self::set_h2_keepalive).
136    /// A pooled connection that has been idle longer than this period is not
137    /// reused. Expiration is checked lazily, when a connection for the same
138    /// host is next requested; the expired connection is closed at that point.
139    /// A zero duration disables the idle check; use
140    /// [`ClientRequest::force_close`](super::ClientRequest::force_close) to
141    /// avoid reusing a connection. The default is 15 seconds.
142    pub fn set_h1_keepalive<T: Into<Seconds>>(mut self, dur: T) -> Self {
143        self.h1_keep_alive = dur.into().into();
144        self
145    }
146
147    #[must_use]
148    /// Sets the maximum lifetime of a pooled HTTP/1 connection.
149    ///
150    /// HTTP/2 connections use [`set_h2_lifetime`](Self::set_h2_lifetime).
151    /// A connection older than this period is not reused, regardless of how
152    /// recently it was used. Like the keep-alive period, this is checked when a
153    /// connection for the same host is next requested. A zero duration disables
154    /// the limit. The default is 75 seconds.
155    pub fn set_h1_lifetime<T: Into<Seconds>>(mut self, dur: T) -> Self {
156        self.h1_lifetime = dur.into().into();
157        self
158    }
159
160    #[must_use]
161    /// Sets the maximum number of HTTP/2 connections per host.
162    ///
163    /// HTTP/2 connections are shared by concurrent requests. A new connection
164    /// to a host is opened only when every existing HTTP/2 connection to that
165    /// host has reached its stream limit, see
166    /// [`set_h2_max_streams`](Self::set_h2_max_streams). When the limit is
167    /// reached, requests wait for a free stream.
168    ///
169    /// Requests on established HTTP/2 connections do not count against
170    /// [`set_h1_connection_limit`](Self::set_h1_connection_limit); opening a new
171    /// connection does, because the protocol is not known until the
172    /// connection is established. A value of zero disables the limit.
173    /// The default is 16.
174    pub fn set_h2_connection_limit(mut self, limit: usize) -> Self {
175        self.h2_limit = limit;
176        self
177    }
178
179    #[must_use]
180    /// Sets the maximum number of concurrent requests per HTTP/2 connection.
181    ///
182    /// The peer's `SETTINGS_MAX_CONCURRENT_STREAMS` also applies; the lower of
183    /// the two is used. A value of zero uses only the peer's setting.
184    /// The default is 100.
185    pub fn set_h2_max_streams(mut self, limit: u32) -> Self {
186        self.h2_max_streams = limit;
187        self
188    }
189
190    #[must_use]
191    /// Sets the keep-alive period for idle HTTP/2 connections.
192    ///
193    /// An HTTP/2 connection is idle when it has no in-flight requests; the
194    /// period is measured from the completion of its last request, including
195    /// the response payload. An idle connection older than this period is
196    /// closed when a connection for the same host is next requested.
197    /// A zero duration disables the idle check. The default is 60 seconds.
198    pub fn set_h2_keepalive<T: Into<Seconds>>(mut self, dur: T) -> Self {
199        self.h2_keep_alive = dur.into().into();
200        self
201    }
202
203    #[must_use]
204    /// Sets the maximum lifetime of an HTTP/2 connection.
205    ///
206    /// An HTTP/2 connection older than this period is not used for new
207    /// requests and is closed gracefully, after its in-flight requests
208    /// complete. This is checked when a connection for the same host is next
209    /// requested. A zero duration disables the limit. The default is 1 hour.
210    pub fn set_h2_lifetime<T: Into<Seconds>>(mut self, dur: T) -> Self {
211        self.h2_lifetime = dur.into().into();
212        self
213    }
214
215    #[must_use]
216    /// Sets the response-header timeout.
217    ///
218    /// The timeout covers receiving the response head after the request has
219    /// been sent. Connecting and sending the request are not included. A zero
220    /// duration disables the timeout. The default is 5 seconds.
221    pub fn set_response_timeout<T: Into<Millis>>(mut self, timeout: T) -> Self {
222        self.timeout = timeout.into();
223        self
224    }
225
226    #[must_use]
227    /// Disables the response-header timeout.
228    ///
229    /// This is the same as `set_response_timeout(Millis::ZERO)`.
230    pub fn disable_timeout(mut self) -> Self {
231        self.timeout = Millis::ZERO;
232        self
233    }
234
235    #[must_use]
236    /// Sets the maximum size of a buffered response payload.
237    ///
238    /// The default is 256 KiB. A value of zero disables the limit.
239    pub fn set_response_payload_limit(mut self, limit: usize) -> Self {
240        self.pl_limit = limit;
241        self
242    }
243
244    #[must_use]
245    /// Sets the timeout for reading a complete response payload.
246    ///
247    /// The default is 10 seconds. A zero duration disables the timeout.
248    pub fn set_response_payload_timeout<T: Into<Millis>>(mut self, timeout: T) -> Self {
249        self.pl_timeout = timeout.into();
250        self
251    }
252
253    /// Adds a header to every request.
254    ///
255    /// A request-specific header with the same name takes precedence.
256    pub fn set_header<K, V>(mut self, key: K, value: V) -> Result<Self, HttpError>
257    where
258        HeaderName: TryFrom<K>,
259        HeaderValue: TryFrom<V>,
260        <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
261        <HeaderValue as TryFrom<V>>::Error: Into<HttpError>,
262    {
263        let key = HeaderName::try_from(key).map_err(Into::into)?;
264        let value = HeaderValue::try_from(value).map_err(Into::into)?;
265        self.headers.append(key, value);
266        Ok(self)
267    }
268
269    fn insert_header(mut self, key: HeaderName, value: String) -> Result<Self, HttpError> {
270        let value = HeaderValue::try_from(value).map_err(HttpError::from)?;
271        self.headers.insert(key, value);
272        Ok(self)
273    }
274
275    /// Sets a client-wide HTTP Basic authentication header.
276    ///
277    /// Replaces any previously configured `Authorization` header.
278    pub fn set_basic_auth<U>(self, username: U, password: Option<&str>) -> Result<Self, HttpError>
279    where
280        U: fmt::Display,
281    {
282        let auth = match password {
283            Some(password) => format!("{username}:{password}"),
284            None => format!("{username}:"),
285        };
286        self.insert_header(
287            header::AUTHORIZATION,
288            format!("Basic {}", base64.encode(auth)),
289        )
290    }
291
292    /// Sets a client-wide HTTP Bearer authentication header.
293    ///
294    /// Replaces any previously configured `Authorization` header.
295    pub fn set_bearer_auth<T>(self, token: T) -> Result<Self, HttpError>
296    where
297        T: fmt::Display,
298    {
299        self.insert_header(header::AUTHORIZATION, format!("Bearer {token}"))
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn basics() {
309        let cfg = ClientConfig::new().disable_timeout();
310        assert_eq!(cfg.timeout, Millis::ZERO);
311    }
312
313    #[test]
314    fn h2_settings() {
315        let cfg = ClientConfig::new();
316        assert_eq!(cfg.h2_connection_limit(), 16);
317        assert_eq!(cfg.h2_max_streams(), 100);
318        assert_eq!(cfg.h2_keepalive(), Seconds(60));
319        assert_eq!(cfg.h2_lifetime(), Seconds(3600));
320
321        let cfg = cfg
322            .set_h2_connection_limit(2)
323            .set_h2_max_streams(10)
324            .set_h2_keepalive(Seconds(5))
325            .set_h2_lifetime(Seconds(50));
326        assert_eq!(cfg.h2_connection_limit(), 2);
327        assert_eq!(cfg.h2_max_streams(), 10);
328        assert_eq!(cfg.h2_keepalive(), Seconds(5));
329        assert_eq!(cfg.h2_lifetime(), Seconds(50));
330        // http/1 settings are not affected
331        assert_eq!(cfg.h1_connection_limit(), 8);
332        assert_eq!(cfg.h1_keep_alive, Duration::from_secs(15));
333        assert_eq!(cfg.h1_lifetime, Duration::from_secs(75));
334    }
335
336    #[test]
337    fn response_payload_limit() {
338        let cfg = ClientConfig::new();
339        assert_eq!(cfg.pl_limit, 262_144);
340
341        let cfg = cfg.set_response_payload_limit(10);
342        assert_eq!(cfg.pl_limit, 10);
343    }
344
345    #[test]
346    fn response_payload_timeout() {
347        let cfg = ClientConfig::default();
348        assert_eq!(cfg.pl_timeout, Millis(10_000));
349
350        let cfg = cfg.set_response_payload_timeout(Millis(10));
351        assert_eq!(cfg.pl_timeout, Millis(10));
352    }
353
354    #[test]
355    fn valid_header_name() {
356        let cfg = ClientConfig::new().set_header("Content-Length", 1).unwrap();
357        assert!(cfg.headers.contains_key("Content-Length"));
358    }
359
360    #[test]
361    fn invalid_header_name() {
362        let res = ClientConfig::new().set_header("no valid header name", 1);
363        assert!(res.is_err());
364    }
365
366    #[test]
367    fn valid_header_value() {
368        let valid_header_value = HeaderValue::from(1234);
369        let cfg = ClientConfig::new()
370            .set_header("Content-Length", &valid_header_value)
371            .unwrap();
372        assert_eq!(cfg.headers.get("Content-Length"), Some(&valid_header_value));
373    }
374
375    #[test]
376    fn invalid_header_value() {
377        let res = ClientConfig::new()
378            .set_header("Content-Length", "\n")
379            .is_err();
380        assert!(res);
381    }
382
383    #[test]
384    fn client_basic_auth() {
385        let cfg = ClientConfig::new()
386            .set_basic_auth("username", Some("password"))
387            .unwrap();
388        assert_eq!(
389            cfg.headers
390                .get(header::AUTHORIZATION)
391                .unwrap()
392                .to_str()
393                .unwrap(),
394            "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
395        );
396
397        let cfg = ClientConfig::new()
398            .set_basic_auth("username", None)
399            .unwrap();
400        assert_eq!(
401            cfg.headers
402                .get(header::AUTHORIZATION)
403                .unwrap()
404                .to_str()
405                .unwrap(),
406            "Basic dXNlcm5hbWU6"
407        );
408    }
409
410    #[test]
411    fn client_bearer_auth() {
412        let cfg = ClientConfig::new()
413            .set_bearer_auth("someS3cr3tAutht0k3n")
414            .unwrap();
415        assert_eq!(
416            cfg.headers
417                .get(header::AUTHORIZATION)
418                .unwrap()
419                .to_str()
420                .unwrap(),
421            "Bearer someS3cr3tAutht0k3n"
422        );
423    }
424}