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///
9/// The `h2_*` fields tune HTTP/2 advertised settings and recv windows; `h3_*` fields tune HTTP/3.
10/// None affect HTTP/1.x. See the [crate-level docs][crate] for the protocol-dispatch table.
11///
12/// ```
13/// # use trillium_http::HttpConfig;
14/// // Accept body bytes eagerly (65535-byte initial window) instead of the default 100-
15/// // continue-like lazy window. Good for "always accept uploads" workloads.
16/// let config = HttpConfig::default().with_h2_initial_stream_window_size(65_535);
17/// assert_eq!(config.h2_initial_stream_window_size(), 65_535);
18/// ```
19#[derive(Clone, Copy, Debug, Fieldwork)]
20#[fieldwork(get, get_mut, set, with, without)]
21// `HttpConfig` is a user-facing tuning struct with documented per-field setters; the natural
22// shape is one field per knob. Bundling bools into an enum or bitflags would make the getter/
23// setter surface worse for callers.
24#[allow(clippy::struct_excessive_bools)]
25pub struct HttpConfig {
26 /// The maximum length allowed before the http body begins for a given request.
27 ///
28 /// **Default**: `8kb` in bytes
29 ///
30 /// **Unit**: Byte count
31 pub(crate) head_max_len: usize,
32
33 /// The maximum length of a received body
34 ///
35 /// This limit applies regardless of whether the body is read all at once or streamed
36 /// incrementally, and regardless of transfer encoding (chunked or fixed-length). The correct
37 /// value will be application dependent.
38 ///
39 /// **Default**: `10mb` in bytes
40 ///
41 /// **Unit**: Byte count
42 pub(crate) received_body_max_len: u64,
43
44 #[cfg(not(feature = "parse"))]
45 #[field = false] // this one is private for now
46 pub(crate) max_headers: usize,
47
48 /// The initial buffer allocated for the response.
49 ///
50 /// Ideally this would be exactly the length of the combined response headers and body, if the
51 /// body is short. If the value is shorter than the headers plus the body, multiple transport
52 /// writes will be performed, and if the value is longer, unnecessary memory will be allocated
53 /// for each conn. Although a tcp packet can be up to 64kb, it is probably better to use a
54 /// value less than 1.5kb.
55 ///
56 /// **Default**: `512`
57 ///
58 /// **Unit**: byte count
59 pub(crate) response_buffer_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 /// `max_head_len` or the end of the headers are reached, whichever happens first.
77 ///
78 /// **Default**: `128`
79 ///
80 /// **Unit**: byte count
81 pub(crate) request_buffer_initial_len: usize,
82
83 /// The number of response headers to allocate space for on conn creation.
84 ///
85 /// Headers will grow on insertion when they reach this size.
86 ///
87 /// **Default**: `16`
88 ///
89 /// **Unit**: Header count
90 pub(crate) response_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`
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**: 4096 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`.
195 ///
196 /// Controls how many request-body bytes the peer may send on a newly-opened stream before
197 /// waiting for a `WINDOW_UPDATE`. The default of `0` implements a lazy / 100-continue-like
198 /// pattern: the peer cannot send any body bytes until the handler calls `read` on the
199 /// request body, at which point the driver emits a `WINDOW_UPDATE` topping the window up
200 /// to [`h2_max_stream_recv_window_size`][Self::h2_max_stream_recv_window_size]. A handler
201 /// that returns an error from its header-level checks never pays the bandwidth cost of
202 /// reading the body.
203 ///
204 /// Set to `65_535` (the RFC 9113 baseline) to match nginx / Apache / hyper behavior — body
205 /// bytes arrive eagerly at the cost of 1 RTT less latency on the first DATA frame and the
206 /// possible waste of up to this many bytes on requests the handler rejects.
207 ///
208 /// Must not exceed `2^31 - 1`.
209 ///
210 /// **Default**: `0` (lazy-WU)
211 ///
212 /// **Unit**: byte count
213 pub(crate) h2_initial_stream_window_size: u32,
214
215 /// Per-stream recv window target — how high the driver keeps the peer's stream window
216 /// topped up as the handler consumes request-body bytes.
217 ///
218 /// After the handler signals intent to read (first `poll_read` on the request body), the
219 /// driver emits `WINDOW_UPDATE` frames to keep the effective peer window near this target.
220 /// Also serves as the hard per-stream buffer cap — a peer that sends past this amount of
221 /// unconsumed DATA on a single stream earns a connection-level `FLOW_CONTROL_ERROR`.
222 ///
223 /// **Default**: `1 MiB`
224 ///
225 /// **Unit**: byte count
226 pub(crate) h2_max_stream_recv_window_size: u32,
227
228 /// Connection-level recv window target — how high the driver keeps the peer's
229 /// connection-level window topped up as handlers consume bytes.
230 ///
231 /// Raised via an initial `WINDOW_UPDATE(stream_id=0)` right after SETTINGS (RFC 9113
232 /// forbids SETTINGS from altering the connection window), then refilled on consumption.
233 /// Bounds total concurrent in-flight request-body bytes across all streams on a single
234 /// HTTP/2 connection. Leaving at the RFC baseline of `65_535` would cap bulk uploads at
235 /// ~5 Mbit/s × RTT.
236 ///
237 /// **Default**: `2 MiB`
238 ///
239 /// **Unit**: byte count
240 pub(crate) h2_initial_connection_window_size: u32,
241
242 /// HTTP/2 `SETTINGS_MAX_CONCURRENT_STREAMS` — the maximum number of concurrent
243 /// peer-initiated streams the server will accept.
244 ///
245 /// Peer-opened streams beyond this count get `RST_STREAM(RefusedStream)` per RFC 9113.
246 /// A value in the 100–250 range is the post-Rapid-Reset (CVE-2023-44487) consensus;
247 /// lower values cap parallelism, higher values need per-connection reset-rate limiting
248 /// to avoid `DoS` exposure.
249 ///
250 /// **Default**: `100`
251 ///
252 /// **Unit**: stream count
253 pub(crate) h2_max_concurrent_streams: u32,
254
255 /// HTTP/2 `SETTINGS_MAX_FRAME_SIZE` — the largest frame payload the server will accept.
256 ///
257 /// Peer frames whose payload exceeds this get `FRAME_SIZE_ERROR` per RFC 9113. The RFC
258 /// floor is `16_384`; the ceiling is `16_777_215`. Larger values amortize per-frame
259 /// overhead on bulk transfers but increase the upper bound on a single read.
260 ///
261 /// **Default**: `16_384`
262 ///
263 /// **Unit**: byte count
264 pub(crate) h2_max_frame_size: u32,
265
266 /// whether [datagrams](https://www.rfc-editor.org/rfc/rfc9297.html) are enabled for HTTP/3
267 ///
268 /// This is a protocol-level setting and is communicated to the peer as well as enforced.
269 ///
270 /// **Default**: false
271 pub(crate) h3_datagrams_enabled: bool,
272
273 /// whether [webtransport](https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3)
274 /// (`draft-ietf-webtrans-http3`) is enabled for HTTP/3
275 ///
276 /// This is a protocol-level setting and is communicated to the peer. You do not need to
277 /// manually configure this if using
278 /// [`trillium-webtransport`](https://docs.rs/trillium-webtransport)
279 ///
280 /// **Default**: false
281 pub(crate) webtransport_enabled: bool,
282
283 /// `SETTINGS_ENABLE_CONNECT_PROTOCOL` — advertises that the server accepts extended
284 /// CONNECT requests, enabling protocols layered on top of HTTP that bootstrap via a
285 /// CONNECT with a `:protocol` pseudo-header. The same identifier (0x08) is used by
286 /// HTTP/2 (RFC 8441) and HTTP/3 (RFC 9220).
287 ///
288 /// Use cases include WebSocket-over-h2 (RFC 8441), WebSocket-over-h3 (RFC 9220),
289 /// and WebTransport (`draft-ietf-webtrans-http2` and `draft-ietf-webtrans-http3`).
290 ///
291 /// When set, the server's initial SETTINGS frame includes
292 /// `SETTINGS_ENABLE_CONNECT_PROTOCOL = 1` (on both HTTP/2 and HTTP/3) and the runtime
293 /// accepts CONNECT requests carrying a `:protocol` pseudo-header. Without it, clients
294 /// won't attempt extended CONNECT, which is the correct default — handlers that don't
295 /// expect extended CONNECT shouldn't see those requests.
296 ///
297 /// You don't need to set this manually if using a handler that requires it; such handlers
298 /// flip it from `Handler::init`.
299 ///
300 /// **Default**: false
301 pub(crate) extended_connect_enabled: bool,
302
303 /// whether to panic when a response header with an invalid value (containing `\r`, `\n`, or
304 /// `\0`) is encountered.
305 ///
306 /// Invalid header values are always skipped to prevent header injection. When this is `true`,
307 /// Trillium will additionally panic, surfacing the bug loudly. When `false`, the skip is only
308 /// logged (to the `log` backend) at error level.
309 ///
310 /// **Default**: `true` when compiled with `debug_assertions` (i.e. debug builds), `false` in
311 /// release builds. Override to `true` in release if you want strict production behavior, or to
312 /// `false` in debug if you prefer not to panic during development.
313 pub(crate) panic_on_invalid_response_headers: bool,
314}
315
316impl HttpConfig {
317 /// Default Config
318 pub const DEFAULT: Self = HttpConfig {
319 response_buffer_len: 512,
320 response_buffer_max_len: 2 * 1024 * 1024,
321 request_buffer_initial_len: 128,
322 head_max_len: 8 * 1024,
323 #[cfg(not(feature = "parse"))]
324 max_headers: 128,
325 response_header_initial_capacity: 16,
326 copy_loops_per_yield: 16,
327 received_body_max_len: 10 * 1024 * 1024,
328 received_body_initial_len: 128,
329 received_body_max_preallocate: 1024 * 1024,
330 max_header_list_size: 32 * 1024,
331 dynamic_table_capacity: 4096,
332 h3_blocked_streams: 100,
333 recent_pairs_size: 64,
334 h3_datagrams_enabled: false,
335 h2_initial_stream_window_size: 0,
336 h2_max_stream_recv_window_size: 1 << 20,
337 h2_initial_connection_window_size: 2 << 20,
338 h2_max_concurrent_streams: 100,
339 h2_max_frame_size: 16_384,
340 webtransport_enabled: false,
341 extended_connect_enabled: false,
342 panic_on_invalid_response_headers: cfg!(debug_assertions),
343 };
344}
345
346impl Default for HttpConfig {
347 fn default() -> Self {
348 HttpConfig::DEFAULT
349 }
350}