Skip to main content

trillium_http/
http_config.rs

1use fieldwork::Fieldwork;
2
3/// # Performance and security parameters for trillium-http.
4///
5/// Trillium's http implementation is built with sensible defaults, but applications differ in usage
6/// and this escape hatch allows an application to be tuned. It is best to tune these parameters in
7/// context of realistic benchmarks for your application.
8///
9/// Long term, trillium may export several standard defaults for different constraints and
10/// application types. In the distant future, these may turn into initial values and trillium will
11/// tune itself based on values seen at runtime.
12///
13/// ## HTTP version dispatch
14///
15/// trillium accepts HTTP/1.x, HTTP/2, and HTTP/3 connections on the same listener without
16/// any per-version configuration. The version a given connection speaks is decided at accept
17/// time based on ALPN (for TLS listeners with ALPN), or by peeking the first 24 bytes for
18/// the HTTP/2 client preface (for cleartext listeners and TLS listeners where ALPN was
19/// absent or returned an unrecognized value):
20///
21/// | Listener | ALPN result | First bytes | Protocol |
22/// |---|---|---|---|
23/// | TCP + TLS | `h2` | — | HTTP/2 |
24/// | TCP + TLS | `http/1.1` | — | HTTP/1.1 |
25/// | TCP + TLS | absent / other | match HTTP/2 preface | HTTP/2 (TLS prior-knowledge) |
26/// | TCP + TLS | absent / other | anything else | HTTP/1.1 |
27/// | TCP, cleartext | — | match HTTP/2 preface | HTTP/2 prior-knowledge (h2c) |
28/// | TCP, cleartext | — | anything else | HTTP/1.x |
29/// | QUIC (via `trillium-quinn`) | — | — | HTTP/3 |
30///
31/// h2c via the HTTP/1.1 `Upgrade` mechanism (RFC 7540 §3.2, removed in RFC 9113) is not
32/// supported. The `h2_*` fields on this struct tune the HTTP/2 advertised settings + recv
33/// windows; the `h3_*` fields tune HTTP/3. None of them affect HTTP/1.x.
34///
35/// ```
36/// # use trillium_http::HttpConfig;
37/// // Accept body bytes eagerly (65535-byte initial window) instead of the default 100-
38/// // continue-like lazy window. Good for "always accept uploads" workloads.
39/// let config = HttpConfig::default().with_h2_initial_stream_window_size(65_535);
40/// assert_eq!(config.h2_initial_stream_window_size(), 65_535);
41/// ```
42#[derive(Clone, Copy, Debug, Fieldwork)]
43#[fieldwork(get, get_mut, set, with, without)]
44// `HttpConfig` is a user-facing tuning struct with documented per-field setters; the natural
45// shape is one field per knob. Bundling bools into an enum or bitflags would make the getter/
46// setter surface worse for callers.
47#[allow(clippy::struct_excessive_bools)]
48pub struct HttpConfig {
49    /// The maximum length allowed before the http body begins for a given request.
50    ///
51    /// **Default**: `8kb` in bytes
52    ///
53    /// **Unit**: Byte count
54    pub(crate) head_max_len: usize,
55
56    /// The maximum length of a received body
57    ///
58    /// This limit applies regardless of whether the body is read all at once or streamed
59    /// incrementally, and regardless of transfer encoding (chunked or fixed-length). The correct
60    /// value will be application dependent.
61    ///
62    /// **Default**: `10mb` in bytes
63    ///
64    /// **Unit**: Byte count
65    pub(crate) received_body_max_len: u64,
66
67    #[cfg(not(feature = "parse"))]
68    #[field = false] // this one is private for now
69    pub(crate) max_headers: usize,
70
71    /// The initial buffer allocated for the response.
72    ///
73    /// Ideally this would be exactly the length of the combined response headers and body, if the
74    /// body is short. If the value is shorter than the headers plus the body, multiple transport
75    /// writes will be performed, and if the value is longer, unnecessary memory will be allocated
76    /// for each conn. Although a tcp packet can be up to 64kb, it is probably better to use a
77    /// value less than 1.5kb.
78    ///
79    /// **Default**: `512`
80    ///
81    /// **Unit**: byte count
82    pub(crate) response_buffer_len: usize,
83
84    /// Maximum size the response buffer may grow to absorb backpressure.
85    ///
86    /// When the transport cannot accept data as fast as the response body is produced, the buffer
87    /// absorbs the remainder up to this limit. Once the limit is reached, writes apply
88    /// backpressure to the body source. This prevents a slow client from causing unbounded memory
89    /// growth.
90    ///
91    /// **Default**: `2mb` in bytes
92    ///
93    /// **Unit**: byte count
94    pub(crate) response_buffer_max_len: usize,
95
96    /// The initial buffer allocated for the request headers.
97    ///
98    /// Ideally this is the length of the request headers. It will grow nonlinearly until
99    /// `max_head_len` or the end of the headers are reached, whichever happens first.
100    ///
101    /// **Default**: `128`
102    ///
103    /// **Unit**: byte count
104    pub(crate) request_buffer_initial_len: usize,
105
106    /// The number of response headers to allocate space for on conn creation.
107    ///
108    /// Headers will grow on insertion when they reach this size.
109    ///
110    /// **Default**: `16`
111    ///
112    /// **Unit**: Header count
113    pub(crate) response_header_initial_capacity: usize,
114
115    /// A sort of cooperative task yielding knob.
116    ///
117    /// Decreasing this number will improve tail latencies at a slight cost to total throughput for
118    /// fast clients. This will have more of an impact on servers that spend a lot of time in IO
119    /// compared to app handlers.
120    ///
121    /// **Default**: `16`
122    ///
123    /// **Unit**: the number of consecutive `Poll::Ready` async writes to perform before yielding
124    /// the task back to the runtime.
125    pub(crate) copy_loops_per_yield: usize,
126
127    /// The initial buffer capacity allocated when reading a chunked http body to bytes or string.
128    ///
129    /// Ideally this would be the size of the http body, which is highly application dependent. As
130    /// with other initial buffer lengths, further allocation will be performed until the necessary
131    /// length is achieved. A smaller number will result in more vec resizing, and a larger number
132    /// will result in unnecessary allocation.
133    ///
134    /// **Default**: `128`
135    ///
136    /// **Unit**: byte count
137    pub(crate) received_body_initial_len: usize,
138
139    /// Maximum size to pre-allocate based on content-length for buffering a complete request body
140    ///
141    /// When we receive a fixed-length (not chunked-encoding) body that is smaller than this size,
142    /// we can allocate a buffer with exactly the right size before we receive the body.  However,
143    /// if this is unbounded, malicious clients can issue headers with large content-length and
144    /// then keep the connection open without sending any bytes, allowing them to allocate
145    /// memory faster than their bandwidth usage. This does not limit the ability to receive
146    /// fixed-length bodies larger than this, but the memory allocation will grow as with
147    /// chunked bodies. Note that this has no impact on chunked bodies. If this is set higher
148    /// than the `received_body_max_len`, this parameter has no effect. This parameter only
149    /// impacts [`ReceivedBody::read_string`](crate::ReceivedBody::read_string) and
150    /// [`ReceivedBody::read_bytes`](crate::ReceivedBody::read_bytes).
151    ///
152    /// **Default**: `1mb` in bytes
153    ///
154    /// **Unit**: Byte count
155    pub(crate) received_body_max_preallocate: usize,
156
157    /// The maximum cumulative size of a header block the peer may send.
158    ///
159    /// Advertised in SETTINGS as `SETTINGS_MAX_HEADER_LIST_SIZE` on HTTP/2 (RFC 9113 §6.5.2)
160    /// and `SETTINGS_MAX_FIELD_SECTION_SIZE` on HTTP/3 (RFC 9114 §7.2.4.1). Guards against
161    /// pathological header lists inflating memory per stream during HPACK/QPACK decode.
162    /// Currently advertised only — the peer is expected to self-police.
163    ///
164    /// **Default**: `32 KiB`
165    ///
166    /// **Unit**: byte count
167    pub(crate) max_header_list_size: u64,
168
169    /// Maximum capacity of the dynamic header-compression table.
170    ///
171    /// Advertised to peers as `SETTINGS_HEADER_TABLE_SIZE` (HPACK / RFC 7541 §6.5.2) and
172    /// `SETTINGS_QPACK_MAX_TABLE_CAPACITY` (QPACK / RFC 9204 §5). Bounds both the decoder's
173    /// inbound table and our encoder's outbound table; set to `0` to disable dynamic-table
174    /// compression entirely (encoder reduces to static-or-literal).
175    ///
176    /// **Default**: 4096 bytes
177    ///
178    /// **Unit**: Byte count
179    pub(crate) dynamic_table_capacity: usize,
180
181    /// Maximum number of HTTP/3 request streams that may be blocked waiting for dynamic table
182    /// updates.
183    ///
184    /// Advertised to peers as `SETTINGS_QPACK_BLOCKED_STREAMS`. A value of `0` prevents peers
185    /// from sending header blocks that reference table entries not yet seen by this decoder.
186    ///
187    /// **Default**: 100
188    ///
189    /// **Unit**: Stream count
190    pub(crate) h3_blocked_streams: usize,
191
192    /// Per-connection ring size for the header encoder's recently-seen-pair predictor.
193    ///
194    /// Applies to both HPACK (HTTP/2) and QPACK (HTTP/3). The predictor lets the encoder
195    /// defer dynamic-table inserts until a `(name, value)` pair has been seen at least
196    /// once on the connection — first sighting emits a literal, subsequent sightings
197    /// within the ring's retention window invest in an insert so future sections can
198    /// index it. A larger ring catches repetitions across more intervening header lines
199    /// (good for header-heavy reverse proxies); a smaller ring forgets faster (fine for
200    /// tiny APIs). A cross-connection observer short-circuits this for already-known-hot
201    /// pairs.
202    ///
203    /// The predictor is consulted once per emitted header line via a u32 hash compare;
204    /// cost grows linearly with `size` but is dominated by the per-line hash, so
205    /// oversizing here is cheap.
206    ///
207    /// **Default**: 64
208    ///
209    /// **Unit**: Pair count
210    pub(crate) recent_pairs_size: usize,
211
212    /// Initial HTTP/2 stream flow-control window advertised to peers as
213    /// `SETTINGS_INITIAL_WINDOW_SIZE`.
214    ///
215    /// Controls how many request-body bytes the peer may send on a newly-opened stream before
216    /// waiting for a `WINDOW_UPDATE`. The default of `0` implements a lazy / 100-continue-like
217    /// pattern: the peer cannot send any body bytes until the handler calls `read` on the
218    /// request body, at which point the driver emits a `WINDOW_UPDATE` topping the window up
219    /// to [`h2_max_stream_recv_window_size`][Self::h2_max_stream_recv_window_size]. A handler
220    /// that returns an error from its header-level checks never pays the bandwidth cost of
221    /// reading the body.
222    ///
223    /// Set to `65_535` (the RFC 9113 baseline) to match nginx / Apache / hyper behavior — body
224    /// bytes arrive eagerly at the cost of 1 RTT less latency on the first DATA frame and the
225    /// possible waste of up to this many bytes on requests the handler rejects.
226    ///
227    /// Must not exceed `2^31 - 1`.
228    ///
229    /// **Default**: `0` (lazy-WU)
230    ///
231    /// **Unit**: byte count
232    pub(crate) h2_initial_stream_window_size: u32,
233
234    /// Per-stream recv window target — how high the driver keeps the peer's stream window
235    /// topped up as the handler consumes request-body bytes.
236    ///
237    /// After the handler signals intent to read (first `poll_read` on the request body), the
238    /// driver emits `WINDOW_UPDATE` frames to keep the effective peer window near this target.
239    /// Also serves as the hard per-stream buffer cap — a peer that sends past this amount of
240    /// unconsumed DATA on a single stream earns a connection-level `FLOW_CONTROL_ERROR`.
241    ///
242    /// **Default**: `1 MiB`
243    ///
244    /// **Unit**: byte count
245    pub(crate) h2_max_stream_recv_window_size: u32,
246
247    /// Connection-level recv window target — how high the driver keeps the peer's
248    /// connection-level window topped up as handlers consume bytes.
249    ///
250    /// Raised via an initial `WINDOW_UPDATE(stream_id=0)` right after SETTINGS (RFC 9113
251    /// §6.9.2 forbids SETTINGS from altering the connection window), then refilled on
252    /// consumption. Bounds total concurrent in-flight request-body bytes across all streams on
253    /// a single HTTP/2 connection. Leaving at the RFC baseline of `65_535` would cap bulk
254    /// uploads at ~5 Mbit/s × RTT.
255    ///
256    /// **Default**: `2 MiB`
257    ///
258    /// **Unit**: byte count
259    pub(crate) h2_initial_connection_window_size: u32,
260
261    /// HTTP/2 `SETTINGS_MAX_CONCURRENT_STREAMS` — the maximum number of concurrent
262    /// peer-initiated streams the server will accept.
263    ///
264    /// Peer-opened streams beyond this count get `RST_STREAM(RefusedStream)` per RFC 9113
265    /// §5.1.2. A value in the 100–250 range is the post-Rapid-Reset (CVE-2023-44487)
266    /// consensus; lower values cap parallelism, higher values need per-connection reset-rate
267    /// limiting to avoid `DoS` exposure.
268    ///
269    /// **Default**: `100`
270    ///
271    /// **Unit**: stream count
272    pub(crate) h2_max_concurrent_streams: u32,
273
274    /// HTTP/2 `SETTINGS_MAX_FRAME_SIZE` — the largest frame payload the server will accept.
275    ///
276    /// Peer frames whose payload exceeds this get `FRAME_SIZE_ERROR` per RFC 9113 §4.2. The
277    /// RFC floor is `16_384`; the ceiling is `16_777_215`. Larger values amortize per-frame
278    /// overhead on bulk transfers but increase the upper bound on a single read.
279    ///
280    /// **Default**: `16_384`
281    ///
282    /// **Unit**: byte count
283    pub(crate) h2_max_frame_size: u32,
284
285    /// whether [datagrams](https://www.rfc-editor.org/rfc/rfc9297.html) are enabled for HTTP/3
286    ///
287    /// This is a protocol-level setting and is communicated to the peer as well as enforced.
288    ///
289    /// **Default**: false
290    pub(crate) h3_datagrams_enabled: bool,
291
292    /// whether [webtransport](https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3)
293    /// (`draft-ietf-webtrans-http3`) is enabled for HTTP/3
294    ///
295    /// This is a protocol-level setting and is communicated to the peer. You do not need to
296    /// manually configure this if using
297    /// [`trillium-webtransport`](https://docs.rs/trillium-webtransport)
298    ///
299    /// **Default**: false
300    pub(crate) webtransport_enabled: bool,
301
302    /// `SETTINGS_ENABLE_CONNECT_PROTOCOL` — advertises that the server accepts extended
303    /// CONNECT requests, enabling protocols layered on top of HTTP that bootstrap via a
304    /// CONNECT with a `:protocol` pseudo-header. The same identifier (0x08) is used by
305    /// HTTP/2 (RFC 8441 §3) and HTTP/3 (RFC 9220 §3).
306    ///
307    /// Use cases include WebSocket-over-h2 (RFC 8441), WebSocket-over-h3 (RFC 9220),
308    /// and WebTransport (`draft-ietf-webtrans-http2` and `draft-ietf-webtrans-http3`).
309    ///
310    /// When set, the server's initial SETTINGS frame includes
311    /// `SETTINGS_ENABLE_CONNECT_PROTOCOL = 1` (on both HTTP/2 and HTTP/3) and the runtime
312    /// accepts CONNECT requests carrying a `:protocol` pseudo-header. Without it, clients
313    /// won't attempt extended CONNECT, which is the correct default — handlers that don't
314    /// expect extended CONNECT shouldn't see those requests.
315    ///
316    /// You don't need to set this manually if using a handler that requires it (e.g. an
317    /// h2 websocket handler will flip it from `Handler::init`, the same way the
318    /// trillium-webtransport handler flips `webtransport_enabled`).
319    ///
320    /// **Default**: false
321    pub(crate) extended_connect_enabled: bool,
322
323    /// whether to panic when a response header with an invalid value (containing `\r`, `\n`, or
324    /// `\0`) is encountered.
325    ///
326    /// Invalid header values are always skipped to prevent header injection. When this is `true`,
327    /// Trillium will additionally panic, surfacing the bug loudly. When `false`, the skip is only
328    /// logged (to the `log` backend) at error level.
329    ///
330    /// **Default**: `true` when compiled with `debug_assertions` (i.e. debug builds), `false` in
331    /// release builds. Override to `true` in release if you want strict production behavior, or to
332    /// `false` in debug if you prefer not to panic during development.
333    pub(crate) panic_on_invalid_response_headers: bool,
334}
335
336impl HttpConfig {
337    /// Default Config
338    pub const DEFAULT: Self = HttpConfig {
339        response_buffer_len: 512,
340        response_buffer_max_len: 2 * 1024 * 1024,
341        request_buffer_initial_len: 128,
342        head_max_len: 8 * 1024,
343        #[cfg(not(feature = "parse"))]
344        max_headers: 128,
345        response_header_initial_capacity: 16,
346        copy_loops_per_yield: 16,
347        received_body_max_len: 10 * 1024 * 1024,
348        received_body_initial_len: 128,
349        received_body_max_preallocate: 1024 * 1024,
350        max_header_list_size: 32 * 1024,
351        dynamic_table_capacity: 4096,
352        h3_blocked_streams: 100,
353        recent_pairs_size: 64,
354        h3_datagrams_enabled: false,
355        h2_initial_stream_window_size: 0,
356        h2_max_stream_recv_window_size: 1 << 20,
357        h2_initial_connection_window_size: 2 << 20,
358        h2_max_concurrent_streams: 100,
359        h2_max_frame_size: 16_384,
360        webtransport_enabled: false,
361        extended_connect_enabled: false,
362        panic_on_invalid_response_headers: cfg!(debug_assertions),
363    };
364}
365
366impl Default for HttpConfig {
367    fn default() -> Self {
368        HttpConfig::DEFAULT
369    }
370}