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#[derive(Clone, Copy, Debug, Fieldwork)]
9#[fieldwork(get, get_mut, set, with, without)]
10// `HttpConfig` is a user-facing tuning struct with documented per-field setters; the natural
11// shape is one field per knob. Bundling bools into an enum or bitflags would make the getter/
12// setter surface worse for callers.
13#[allow(clippy::struct_excessive_bools)]
14pub struct HttpConfig {
15    /// The maximum length of an http head — request headers server-side, response headers
16    /// client-side.
17    ///
18    /// **Default**: `8kb` in bytes
19    ///
20    /// **Unit**: Byte count
21    pub(crate) head_max_len: usize,
22
23    /// The maximum length of a received body
24    ///
25    /// This limit applies regardless of whether the body is read all at once or streamed
26    /// incrementally, and regardless of transfer encoding (chunked or fixed-length). The correct
27    /// value will be application dependent.
28    ///
29    /// **Default**: `10mb` in bytes
30    ///
31    /// **Unit**: Byte count
32    pub(crate) received_body_max_len: u64,
33
34    /// The initial capacity of the buffer that serializes the response head and batches body
35    /// writes.
36    ///
37    /// Sizing it to fit a typical response head avoids one buffer growth per response. Body
38    /// write batching is governed by `body_write_chunk_len`, not this value;
39    /// `response_buffer_max_len` bounds only the separate backpressure-absorption path.
40    ///
41    /// **Default**: `512`
42    ///
43    /// **Unit**: byte count
44    pub(crate) response_buffer_len: usize,
45
46    /// The maximum bytes of a streaming body read and framed per iteration on the h1 and h3
47    /// send paths, and the buffer fill level that triggers a write to the transport during
48    /// body streaming.
49    ///
50    /// Streaming body content is read directly into the send buffer in slices of at most this
51    /// size, each becoming one h1 chunk or h3 DATA frame; once the buffer holds this many
52    /// bytes it is written through. Larger values batch more bytes per write syscall and
53    /// per-frame overhead at the cost of that much buffer memory per in-flight streaming
54    /// body. Bodies already in memory ignore this and are written whole. Values below `16`
55    /// are treated as `16`.
56    ///
57    /// **Default**: `8kb` in bytes
58    ///
59    /// **Unit**: byte count
60    pub(crate) body_write_chunk_len: usize,
61
62    /// Maximum size the response buffer may grow to absorb backpressure.
63    ///
64    /// When the transport cannot accept data as fast as the response body is produced, the buffer
65    /// absorbs the remainder up to this limit. Once the limit is reached, writes apply
66    /// backpressure to the body source. This prevents a slow client from causing unbounded memory
67    /// growth.
68    ///
69    /// **Default**: `2mb` in bytes
70    ///
71    /// **Unit**: byte count
72    pub(crate) response_buffer_max_len: usize,
73
74    /// The initial read length for an http head — request headers server-side, response headers
75    /// client-side.
76    ///
77    /// Ideally this is the length of the headers. It will grow nonlinearly until `head_max_len`
78    /// or the end of the headers are reached, whichever happens first.
79    ///
80    /// **Default**: `1024`
81    ///
82    /// **Unit**: byte count
83    pub(crate) request_buffer_initial_len: usize,
84
85    /// The expected number of response headers, used to size the response header map on conn
86    /// creation.
87    ///
88    /// The map grows on insertion beyond this. The value is split evenly across two internal
89    /// stores, so prefer to overestimate — an undersized map reallocates as it fills.
90    ///
91    /// **Default**: `32`
92    ///
93    /// **Unit**: Header count
94    pub(crate) response_header_initial_capacity: usize,
95
96    /// The expected number of request headers, used to size the request header map while parsing.
97    ///
98    /// The map grows on insertion beyond this. The value is split evenly across two internal
99    /// stores, so prefer to overestimate — an undersized map reallocates as it fills, which is the
100    /// dominant cost of building the header map for header-heavy requests.
101    ///
102    /// **Default**: `32`
103    ///
104    /// **Unit**: Header count
105    pub(crate) request_header_initial_capacity: usize,
106
107    /// Cooperative task-yielding knob.
108    ///
109    /// Decreasing this number will improve tail latencies at a slight cost to total throughput for
110    /// fast clients. This will have more of an impact on servers that spend a lot of time in IO
111    /// compared to app handlers.
112    ///
113    /// **Default**: `16`
114    ///
115    /// **Unit**: the number of consecutive units of work a task does without waiting before it
116    /// yields back to the runtime. When copying a body, each write that completes immediately is
117    /// one unit; on an HTTP/2 connection, each round of reading and writing frames is one,
118    /// including a round that hands a new request to its handler.
119    pub(crate) copy_loops_per_yield: usize,
120
121    /// The initial buffer capacity allocated when reading a chunked http body to bytes or string.
122    ///
123    /// Ideally this would be the size of the http body, which is highly application dependent. As
124    /// with other initial buffer lengths, further allocation will be performed until the necessary
125    /// length is achieved. A smaller number will result in more vec resizing, and a larger number
126    /// will result in unnecessary allocation.
127    ///
128    /// **Default**: `128`
129    ///
130    /// **Unit**: byte count
131    pub(crate) received_body_initial_len: usize,
132
133    /// Maximum size to pre-allocate based on content-length for buffering a complete request body
134    ///
135    /// When we receive a fixed-length (not chunked-encoding) body that is smaller than this size,
136    /// we can allocate a buffer with exactly the right size before we receive the body.  However,
137    /// if this is unbounded, malicious clients can issue headers with large content-length and
138    /// then keep the connection open without sending any bytes, allowing them to allocate
139    /// memory faster than their bandwidth usage. This does not limit the ability to receive
140    /// fixed-length bodies larger than this, but the memory allocation will grow as with
141    /// chunked bodies. Note that this has no impact on chunked bodies. If this is set higher
142    /// than the `received_body_max_len`, this parameter has no effect. This parameter only
143    /// impacts [`ReceivedBody::read_string`](crate::ReceivedBody::read_string) and
144    /// [`ReceivedBody::read_bytes`](crate::ReceivedBody::read_bytes).
145    ///
146    /// **Default**: `1mb` in bytes
147    ///
148    /// **Unit**: Byte count
149    pub(crate) received_body_max_preallocate: usize,
150
151    /// The maximum cumulative size of a header block the peer may send.
152    ///
153    /// Advertised in SETTINGS as `SETTINGS_MAX_HEADER_LIST_SIZE` on HTTP/2 (RFC 9113) and
154    /// `SETTINGS_MAX_FIELD_SECTION_SIZE` on HTTP/3 (RFC 9114). Guards against pathological
155    /// header lists inflating memory per stream during HPACK/QPACK decode.
156    ///
157    /// On HTTP/2 this also bounds the cumulative compressed bytes of a header block
158    /// accumulated across HEADERS + CONTINUATION frames: a block exceeding this limit closes
159    /// the connection with `ENHANCE_YOUR_CALM`, mitigating the CONTINUATION-flood `DoS`
160    /// (CVE-2024-27316 class). Otherwise the peer is expected to self-police.
161    ///
162    /// On HTTP/1.1 this bounds the trailer-section accumulated after a chunked body's
163    /// last-chunk marker: a peer that streams trailer bytes without ever sending the
164    /// terminator errors out at this limit rather than growing the connection buffer
165    /// without bound.
166    ///
167    /// **Default**: `32 KiB` in bytes
168    ///
169    /// **Unit**: byte count
170    pub(crate) max_header_list_size: u64,
171
172    /// Maximum capacity of the dynamic header-compression table.
173    ///
174    /// Advertised to peers as `SETTINGS_HEADER_TABLE_SIZE` (HPACK / RFC 7541) and
175    /// `SETTINGS_QPACK_MAX_TABLE_CAPACITY` (QPACK / RFC 9204). Bounds both the decoder's
176    /// inbound table and our encoder's outbound table; set to `0` to disable dynamic-table
177    /// compression entirely (encoder reduces to static-or-literal).
178    ///
179    /// **Default**: `4 KiB` in bytes
180    ///
181    /// **Unit**: Byte count
182    pub(crate) dynamic_table_capacity: usize,
183
184    /// Maximum number of HTTP/3 request streams that may be blocked waiting for dynamic table
185    /// updates.
186    ///
187    /// Advertised to peers as `SETTINGS_QPACK_BLOCKED_STREAMS`. A value of `0` prevents peers
188    /// from sending header blocks that reference table entries not yet seen by this decoder.
189    ///
190    /// **Default**: 100
191    ///
192    /// **Unit**: Stream count
193    pub(crate) h3_blocked_streams: usize,
194
195    /// Per-connection ring size for the header encoder's recently-seen-pair predictor.
196    ///
197    /// Applies to both HPACK (HTTP/2) and QPACK (HTTP/3). The predictor lets the encoder
198    /// defer dynamic-table inserts until a `(name, value)` pair has repeated on the
199    /// connection — early sightings emit literals, repetition within the ring's retention
200    /// window invests in an insert so future sections can index it. A cross-connection
201    /// observer short-circuits this for already-known-hot pairs.
202    ///
203    /// **Ignored while `recent_pairs_auto` is `true` (the default)**, in which case each
204    /// connection derives its ring size from the negotiated dynamic-table capacity.
205    /// Setting this through [`set_recent_pairs_size`](Self::set_recent_pairs_size) or
206    /// [`with_recent_pairs_size`](Self::with_recent_pairs_size) disables
207    /// `recent_pairs_auto`. The window should track what the table can retain: an
208    /// oversized ring approves inserts whose repetition the table has already evicted,
209    /// churning the table for no reference savings.
210    ///
211    /// **Default**: 64, inert behind `recent_pairs_auto`
212    ///
213    /// **Unit**: Pair count
214    #[field(set = false, with = false)]
215    pub(crate) recent_pairs_size: usize,
216
217    /// Derive header-encoder insert-prediction tuning from each connection's negotiated
218    /// dynamic-table capacity.
219    ///
220    /// When `true` (the default), the recently-seen-pair ring is sized to roughly twice
221    /// the entry count the table can hold, and the insert threshold requires a third
222    /// sighting on tables of 512 bytes or larger (second sighting on smaller tables).
223    /// These track the negotiated capacity, so they adapt per connection. QPACK
224    /// connections additionally warm-insert a name-only `(name, "")` entry for a
225    /// static-table-miss name on its third sighting (tracked by a second ring sized like
226    /// the pair ring), so names whose values never repeat — request ids and the like —
227    /// still earn a cheap dynamic name reference. HPACK has no name-only analog: its
228    /// inserts are inline field lines, where an empty-valued entry would be a bogus
229    /// header.
230    ///
231    /// When `false`, the ring uses `recent_pairs_size` verbatim, inserts trigger on the
232    /// second sighting at every capacity, and no name-only warming occurs. Setting
233    /// `recent_pairs_size` explicitly switches this off; mutating it through
234    /// [`recent_pairs_size_mut`](Self::recent_pairs_size_mut) does *not*.
235    ///
236    /// **Default**: `true`
237    pub(crate) recent_pairs_auto: bool,
238
239    /// Initial HTTP/2 stream flow-control window advertised to peers as
240    /// `SETTINGS_INITIAL_WINDOW_SIZE` — the lower tier of the two-tier per-stream window.
241    ///
242    /// Controls how many request-body bytes the peer may send on a newly-opened stream before the
243    /// handler starts reading. Once the handler signals intent to read (first `poll_read` on the
244    /// request body), the window is promoted to `h2_max_stream_recv_window_size`; a stream whose
245    /// handler never reads the body stays at this initial.
246    ///
247    /// Must not exceed `2^31 - 1`.
248    ///
249    /// **Default**: `256 KiB`
250    ///
251    /// **Unit**: byte count
252    pub(crate) h2_initial_stream_window_size: u32,
253
254    /// Per-stream recv window target — the upper tier of the two-tier window. A stream opens at
255    /// `h2_initial_stream_window_size` and is promoted to this value once the handler signals
256    /// intent to read the request body (first `poll_read`); the driver then tops the peer's window
257    /// back up to it via `WINDOW_UPDATE` as the handler drains. Because strict flow control bounds
258    /// the recv buffer to the granted window, this is also the per-stream buffer bound — a peer
259    /// that sends past the window earns a connection-level `FLOW_CONTROL_ERROR`.
260    ///
261    /// Must be `>= h2_initial_stream_window_size`; a smaller value is clamped up to the initial
262    /// (with a one-time log warning), since the window is only ever promoted upward.
263    ///
264    /// **Default**: `1 MiB` in bytes
265    ///
266    /// **Unit**: byte count
267    pub(crate) h2_max_stream_recv_window_size: u32,
268
269    /// Connection-level recv window target — how high the driver keeps the peer's
270    /// connection-level window topped up as handlers consume bytes.
271    ///
272    /// Raised via an initial `WINDOW_UPDATE(stream_id=0)` right after SETTINGS (RFC 9113
273    /// forbids SETTINGS from altering the connection window), then refilled on consumption.
274    /// Bounds total concurrent in-flight request-body bytes across all streams on a single
275    /// HTTP/2 connection. Leaving at the RFC baseline of `65_535` would cap bulk uploads at
276    /// ~5 Mbit/s × RTT.
277    ///
278    /// **Default**: `2 MiB` in bytes
279    ///
280    /// **Unit**: byte count
281    pub(crate) h2_initial_connection_window_size: u32,
282
283    /// HTTP/2 `SETTINGS_MAX_CONCURRENT_STREAMS` — the maximum number of concurrent
284    /// peer-initiated streams the server will accept.
285    ///
286    /// Peer-opened streams beyond this count get `RST_STREAM(RefusedStream)` per RFC 9113.
287    /// A value in the 100–250 range is the post-Rapid-Reset (CVE-2023-44487) consensus;
288    /// lower values cap parallelism, higher values need per-connection reset-rate limiting
289    /// to avoid `DoS` exposure.
290    ///
291    /// **Default**: `100`
292    ///
293    /// **Unit**: stream count
294    pub(crate) h2_max_concurrent_streams: u32,
295
296    /// HTTP/2 `SETTINGS_MAX_FRAME_SIZE` — the largest frame payload the server will accept.
297    ///
298    /// Peer frames whose payload exceeds this get `FRAME_SIZE_ERROR` per RFC 9113. The RFC
299    /// floor is `16_384`; the ceiling is `16_777_215`. Larger values amortize per-frame
300    /// overhead on bulk transfers but increase the upper bound on a single read.
301    ///
302    /// **Default**: `16 KiB` in bytes
303    ///
304    /// **Unit**: byte count
305    pub(crate) h2_max_frame_size: u32,
306
307    /// whether [datagrams](https://www.rfc-editor.org/rfc/rfc9297.html) are enabled for HTTP/3
308    ///
309    /// This is a protocol-level setting and is communicated to the peer as well as enforced.
310    ///
311    /// **Default**: false
312    pub(crate) h3_datagrams_enabled: bool,
313
314    /// whether [webtransport](https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3)
315    /// (`draft-ietf-webtrans-http3`) is enabled for HTTP/3
316    ///
317    /// This is a protocol-level setting and is communicated to the peer. You do not need to
318    /// manually configure this if using
319    /// [`trillium-webtransport`](https://docs.rs/trillium-webtransport)
320    ///
321    /// **Default**: false
322    pub(crate) webtransport_enabled: bool,
323
324    /// `SETTINGS_ENABLE_CONNECT_PROTOCOL` — advertises that the server accepts extended
325    /// CONNECT requests, enabling protocols layered on top of HTTP that bootstrap via a
326    /// CONNECT with a `:protocol` pseudo-header.
327    ///
328    /// You likely don't need to set this directly if using a trillium handler that uses extended
329    /// connect.
330    ///
331    /// **Default**: false
332    pub(crate) extended_connect_enabled: bool,
333
334    /// whether to panic when an outbound (app-controlled) header with an invalid value (containing
335    /// `\r`, `\n`, or `\0`) is encountered.
336    ///
337    /// Invalid header values are always skipped to prevent header injection. When this is `true`,
338    /// Trillium will additionally panic, surfacing the bug loudly. When `false`, the skip is only
339    /// logged (to the `log` backend) at error level.
340    ///
341    /// **Default**: `true` when compiled with `debug_assertions` (i.e. debug builds), `false` in
342    /// release builds. Override to `true` in release if you want strict production behavior, or to
343    /// `false` in debug if you prefer not to panic during development.
344    pub(crate) panic_on_invalid_response_headers: bool,
345}
346
347const KB: u32 = 1024;
348const MB: u32 = 1024 * KB;
349
350impl HttpConfig {
351    /// Default Config
352    pub const DEFAULT: Self = HttpConfig {
353        response_buffer_len: 512,
354        body_write_chunk_len: 8 * KB as usize,
355        response_buffer_max_len: 2 * MB as usize,
356        request_buffer_initial_len: 1024,
357        head_max_len: 8 * KB as usize,
358        response_header_initial_capacity: 32,
359        request_header_initial_capacity: 32,
360        copy_loops_per_yield: 16,
361        received_body_max_len: 10 * MB as u64,
362        received_body_initial_len: 128,
363        received_body_max_preallocate: MB as usize,
364        max_header_list_size: 32 * KB as u64,
365        dynamic_table_capacity: 4 * KB as usize,
366        h3_blocked_streams: 100,
367        recent_pairs_size: 64,
368        recent_pairs_auto: true,
369        h3_datagrams_enabled: false,
370        h2_initial_stream_window_size: 256 * KB,
371        h2_max_stream_recv_window_size: MB,
372        h2_initial_connection_window_size: 2 * MB,
373        h2_max_concurrent_streams: 100,
374        h2_max_frame_size: 16 * KB,
375        webtransport_enabled: false,
376        extended_connect_enabled: false,
377        panic_on_invalid_response_headers: cfg!(debug_assertions),
378    };
379}
380
381// Hand-written (not fieldwork-derived) so an explicit size disables auto derivation —
382// same signatures fieldwork would generate.
383impl HttpConfig {
384    /// Sets recent pairs size and disables `recent_pairs_auto`, returning `&mut Self` for
385    /// chaining. See [`recent_pairs_size`](Self::recent_pairs_size).
386    pub fn set_recent_pairs_size(&mut self, recent_pairs_size: usize) -> &mut Self {
387        self.recent_pairs_size = recent_pairs_size;
388        self.recent_pairs_auto = false;
389        self
390    }
391
392    /// Owned chainable setter for recent pairs size; disables `recent_pairs_auto`. See
393    /// [`recent_pairs_size`](Self::recent_pairs_size).
394    #[must_use]
395    pub fn with_recent_pairs_size(mut self, recent_pairs_size: usize) -> Self {
396        self.recent_pairs_size = recent_pairs_size;
397        self.recent_pairs_auto = false;
398        self
399    }
400}
401
402impl Default for HttpConfig {
403    fn default() -> Self {
404        HttpConfig::DEFAULT
405    }
406}