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