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 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    /// The response head and as much of the body as fits are coalesced into a single transport
37    /// write, so a response up to a few KiB is sent in one write regardless of this value. It
38    /// matters only for large response bodies, where a larger buffer batches more bytes per write
39    /// and so reduces the number of write syscalls on a bulk transfer — at the cost of that much
40    /// initial memory per connection. On the common path the buffer flushes when full rather than
41    /// growing; `response_buffer_max_len` bounds only the separate backpressure-absorption path.
42    ///
43    /// **Default**: `512`
44    ///
45    /// **Unit**: byte count
46    pub(crate) response_buffer_len: usize,
47
48    /// Maximum size the response buffer may grow to absorb backpressure.
49    ///
50    /// When the transport cannot accept data as fast as the response body is produced, the buffer
51    /// absorbs the remainder up to this limit. Once the limit is reached, writes apply
52    /// backpressure to the body source. This prevents a slow client from causing unbounded memory
53    /// growth.
54    ///
55    /// **Default**: `2mb` in bytes
56    ///
57    /// **Unit**: byte count
58    pub(crate) response_buffer_max_len: usize,
59
60    /// The initial buffer allocated for the request headers.
61    ///
62    /// Ideally this is the length of the request headers. It will grow nonlinearly until
63    /// `head_max_len` or the end of the headers are reached, whichever happens first.
64    ///
65    /// **Default**: `1024`
66    ///
67    /// **Unit**: byte count
68    pub(crate) request_buffer_initial_len: usize,
69
70    /// The expected number of response headers, used to size the response header map on conn
71    /// creation.
72    ///
73    /// The map grows on insertion beyond this. The value is split evenly across two internal
74    /// stores, so prefer to overestimate — an undersized map reallocates as it fills.
75    ///
76    /// **Default**: `32`
77    ///
78    /// **Unit**: Header count
79    pub(crate) response_header_initial_capacity: usize,
80
81    /// The expected number of request headers, used to size the request header map while parsing.
82    ///
83    /// The map grows on insertion beyond this. The value is split evenly across two internal
84    /// stores, so prefer to overestimate — an undersized map reallocates as it fills, which is the
85    /// dominant cost of building the header map for header-heavy requests.
86    ///
87    /// **Default**: `32`
88    ///
89    /// **Unit**: Header count
90    pub(crate) request_header_initial_capacity: usize,
91
92    /// Cooperative task-yielding knob.
93    ///
94    /// Decreasing this number will improve tail latencies at a slight cost to total throughput for
95    /// fast clients. This will have more of an impact on servers that spend a lot of time in IO
96    /// compared to app handlers.
97    ///
98    /// **Default**: `16`
99    ///
100    /// **Unit**: the number of consecutive `Poll::Ready` async writes to perform before yielding
101    /// the task back to the runtime.
102    pub(crate) copy_loops_per_yield: usize,
103
104    /// The initial buffer capacity allocated when reading a chunked http body to bytes or string.
105    ///
106    /// Ideally this would be the size of the http body, which is highly application dependent. As
107    /// with other initial buffer lengths, further allocation will be performed until the necessary
108    /// length is achieved. A smaller number will result in more vec resizing, and a larger number
109    /// will result in unnecessary allocation.
110    ///
111    /// **Default**: `128`
112    ///
113    /// **Unit**: byte count
114    pub(crate) received_body_initial_len: usize,
115
116    /// Maximum size to pre-allocate based on content-length for buffering a complete request body
117    ///
118    /// When we receive a fixed-length (not chunked-encoding) body that is smaller than this size,
119    /// we can allocate a buffer with exactly the right size before we receive the body.  However,
120    /// if this is unbounded, malicious clients can issue headers with large content-length and
121    /// then keep the connection open without sending any bytes, allowing them to allocate
122    /// memory faster than their bandwidth usage. This does not limit the ability to receive
123    /// fixed-length bodies larger than this, but the memory allocation will grow as with
124    /// chunked bodies. Note that this has no impact on chunked bodies. If this is set higher
125    /// than the `received_body_max_len`, this parameter has no effect. This parameter only
126    /// impacts [`ReceivedBody::read_string`](crate::ReceivedBody::read_string) and
127    /// [`ReceivedBody::read_bytes`](crate::ReceivedBody::read_bytes).
128    ///
129    /// **Default**: `1mb` in bytes
130    ///
131    /// **Unit**: Byte count
132    pub(crate) received_body_max_preallocate: usize,
133
134    /// The maximum cumulative size of a header block the peer may send.
135    ///
136    /// Advertised in SETTINGS as `SETTINGS_MAX_HEADER_LIST_SIZE` on HTTP/2 (RFC 9113) and
137    /// `SETTINGS_MAX_FIELD_SECTION_SIZE` on HTTP/3 (RFC 9114). Guards against pathological
138    /// header lists inflating memory per stream during HPACK/QPACK decode.
139    ///
140    /// On HTTP/2 this also bounds the cumulative compressed bytes of a header block
141    /// accumulated across HEADERS + CONTINUATION frames: a block exceeding this limit closes
142    /// the connection with `ENHANCE_YOUR_CALM`, mitigating the CONTINUATION-flood `DoS`
143    /// (CVE-2024-27316 class). Otherwise the peer is expected to self-police.
144    ///
145    /// **Default**: `32 KiB` in bytes
146    ///
147    /// **Unit**: byte count
148    pub(crate) max_header_list_size: u64,
149
150    /// Maximum capacity of the dynamic header-compression table.
151    ///
152    /// Advertised to peers as `SETTINGS_HEADER_TABLE_SIZE` (HPACK / RFC 7541) and
153    /// `SETTINGS_QPACK_MAX_TABLE_CAPACITY` (QPACK / RFC 9204). Bounds both the decoder's
154    /// inbound table and our encoder's outbound table; set to `0` to disable dynamic-table
155    /// compression entirely (encoder reduces to static-or-literal).
156    ///
157    /// **Default**: `4 KiB` in bytes
158    ///
159    /// **Unit**: Byte count
160    pub(crate) dynamic_table_capacity: usize,
161
162    /// Maximum number of HTTP/3 request streams that may be blocked waiting for dynamic table
163    /// updates.
164    ///
165    /// Advertised to peers as `SETTINGS_QPACK_BLOCKED_STREAMS`. A value of `0` prevents peers
166    /// from sending header blocks that reference table entries not yet seen by this decoder.
167    ///
168    /// **Default**: 100
169    ///
170    /// **Unit**: Stream count
171    pub(crate) h3_blocked_streams: usize,
172
173    /// Per-connection ring size for the header encoder's recently-seen-pair predictor.
174    ///
175    /// Applies to both HPACK (HTTP/2) and QPACK (HTTP/3). The predictor lets the encoder
176    /// defer dynamic-table inserts until a `(name, value)` pair has been seen at least
177    /// once on the connection — first sighting emits a literal, subsequent sightings
178    /// within the ring's retention window invest in an insert so future sections can
179    /// index it. A larger ring catches repetitions across more intervening header lines
180    /// (good for header-heavy reverse proxies); a smaller ring forgets faster (fine for
181    /// tiny APIs). A cross-connection observer short-circuits this for already-known-hot
182    /// pairs.
183    ///
184    /// The predictor is consulted once per emitted header line via a u32 hash compare;
185    /// cost grows linearly with `size` but is dominated by the per-line hash, so
186    /// oversizing here is cheap.
187    ///
188    /// **Default**: 64
189    ///
190    /// **Unit**: Pair count
191    pub(crate) recent_pairs_size: usize,
192
193    /// Initial HTTP/2 stream flow-control window advertised to peers as
194    /// `SETTINGS_INITIAL_WINDOW_SIZE` — the lower tier of the two-tier per-stream window.
195    ///
196    /// Controls how many request-body bytes the peer may send on a newly-opened stream before the
197    /// handler starts reading. Once the handler signals intent to read (first `poll_read` on the
198    /// request body), the window is promoted to `h2_max_stream_recv_window_size`; a stream whose
199    /// handler never reads the body stays at this initial.
200    ///
201    /// Must not exceed `2^31 - 1`.
202    ///
203    /// **Default**: `256 KiB`
204    ///
205    /// **Unit**: byte count
206    pub(crate) h2_initial_stream_window_size: u32,
207
208    /// Per-stream recv window target — the upper tier of the two-tier window. A stream opens at
209    /// `h2_initial_stream_window_size` and is promoted to this value once the handler signals
210    /// intent to read the request body (first `poll_read`); the driver then tops the peer's window
211    /// back up to it via `WINDOW_UPDATE` as the handler drains. Because strict flow control bounds
212    /// the recv buffer to the granted window, this is also the per-stream buffer bound — a peer
213    /// that sends past the window earns a connection-level `FLOW_CONTROL_ERROR`.
214    ///
215    /// Must be `>= h2_initial_stream_window_size`; a smaller value is clamped up to the initial
216    /// (with a one-time log warning), since the window is only ever promoted upward.
217    ///
218    /// **Default**: `1 MiB` in bytes
219    ///
220    /// **Unit**: byte count
221    pub(crate) h2_max_stream_recv_window_size: u32,
222
223    /// Connection-level recv window target — how high the driver keeps the peer's
224    /// connection-level window topped up as handlers consume bytes.
225    ///
226    /// Raised via an initial `WINDOW_UPDATE(stream_id=0)` right after SETTINGS (RFC 9113
227    /// forbids SETTINGS from altering the connection window), then refilled on consumption.
228    /// Bounds total concurrent in-flight request-body bytes across all streams on a single
229    /// HTTP/2 connection. Leaving at the RFC baseline of `65_535` would cap bulk uploads at
230    /// ~5 Mbit/s × RTT.
231    ///
232    /// **Default**: `2 MiB` in bytes
233    ///
234    /// **Unit**: byte count
235    pub(crate) h2_initial_connection_window_size: u32,
236
237    /// HTTP/2 `SETTINGS_MAX_CONCURRENT_STREAMS` — the maximum number of concurrent
238    /// peer-initiated streams the server will accept.
239    ///
240    /// Peer-opened streams beyond this count get `RST_STREAM(RefusedStream)` per RFC 9113.
241    /// A value in the 100–250 range is the post-Rapid-Reset (CVE-2023-44487) consensus;
242    /// lower values cap parallelism, higher values need per-connection reset-rate limiting
243    /// to avoid `DoS` exposure.
244    ///
245    /// **Default**: `100`
246    ///
247    /// **Unit**: stream count
248    pub(crate) h2_max_concurrent_streams: u32,
249
250    /// HTTP/2 `SETTINGS_MAX_FRAME_SIZE` — the largest frame payload the server will accept.
251    ///
252    /// Peer frames whose payload exceeds this get `FRAME_SIZE_ERROR` per RFC 9113. The RFC
253    /// floor is `16_384`; the ceiling is `16_777_215`. Larger values amortize per-frame
254    /// overhead on bulk transfers but increase the upper bound on a single read.
255    ///
256    /// **Default**: `16 KiB` in bytes
257    ///
258    /// **Unit**: byte count
259    pub(crate) h2_max_frame_size: u32,
260
261    /// whether [datagrams](https://www.rfc-editor.org/rfc/rfc9297.html) are enabled for HTTP/3
262    ///
263    /// This is a protocol-level setting and is communicated to the peer as well as enforced.
264    ///
265    /// **Default**: false
266    pub(crate) h3_datagrams_enabled: bool,
267
268    /// whether [webtransport](https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3)
269    /// (`draft-ietf-webtrans-http3`) is enabled for HTTP/3
270    ///
271    /// This is a protocol-level setting and is communicated to the peer. You do not need to
272    /// manually configure this if using
273    /// [`trillium-webtransport`](https://docs.rs/trillium-webtransport)
274    ///
275    /// **Default**: false
276    pub(crate) webtransport_enabled: bool,
277
278    /// `SETTINGS_ENABLE_CONNECT_PROTOCOL` — advertises that the server accepts extended
279    /// CONNECT requests, enabling protocols layered on top of HTTP that bootstrap via a
280    /// CONNECT with a `:protocol` pseudo-header.
281    ///
282    /// You likely don't need to set this directly if using a trillium handler that uses extended
283    /// connect.
284    ///
285    /// **Default**: false
286    pub(crate) extended_connect_enabled: bool,
287
288    /// whether to panic when an outbound (app-controlled) header with an invalid value (containing
289    /// `\r`, `\n`, or `\0`) is encountered.
290    ///
291    /// Invalid header values are always skipped to prevent header injection. When this is `true`,
292    /// Trillium will additionally panic, surfacing the bug loudly. When `false`, the skip is only
293    /// logged (to the `log` backend) at error level.
294    ///
295    /// **Default**: `true` when compiled with `debug_assertions` (i.e. debug builds), `false` in
296    /// release builds. Override to `true` in release if you want strict production behavior, or to
297    /// `false` in debug if you prefer not to panic during development.
298    pub(crate) panic_on_invalid_response_headers: bool,
299}
300
301const KB: u32 = 1024;
302const MB: u32 = 1024 * KB;
303
304impl HttpConfig {
305    /// Default Config
306    pub const DEFAULT: Self = HttpConfig {
307        response_buffer_len: 512,
308        response_buffer_max_len: 2 * MB as usize,
309        request_buffer_initial_len: 1024,
310        head_max_len: 8 * KB as usize,
311        response_header_initial_capacity: 32,
312        request_header_initial_capacity: 32,
313        copy_loops_per_yield: 16,
314        received_body_max_len: 10 * MB as u64,
315        received_body_initial_len: 128,
316        received_body_max_preallocate: MB as usize,
317        max_header_list_size: 32 * KB as u64,
318        dynamic_table_capacity: 4 * KB as usize,
319        h3_blocked_streams: 100,
320        recent_pairs_size: 64,
321        h3_datagrams_enabled: false,
322        h2_initial_stream_window_size: 256 * KB,
323        h2_max_stream_recv_window_size: MB,
324        h2_initial_connection_window_size: 2 * MB,
325        h2_max_concurrent_streams: 100,
326        h2_max_frame_size: 16 * KB,
327        webtransport_enabled: false,
328        extended_connect_enabled: false,
329        panic_on_invalid_response_headers: cfg!(debug_assertions),
330    };
331}
332
333impl Default for HttpConfig {
334    fn default() -> Self {
335        HttpConfig::DEFAULT
336    }
337}