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