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