http-net 0.9.2

Sans-IO HTTP/1.1 server and client: strict RFC 9112 parsing, smuggling defences, bounded buffers, keep-alive, chunked bodies, upgrades, a blocking std adapter, no third-party dependencies.
Documentation
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
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# Migration guide

How to move between `http-net` versions. Each section lists every change a
user of the previous version can notice, why it was made, and what to do.
Additions that change no existing behaviour are listed at the end of each
section.

## 0.9.1 to 0.9.2

Fixes only; no signature changes. Three behaviours can be noticed.

### An error answer to `HEAD` has no content, in the head too

**What changed.** Once a `HEAD` request line has been read, any error
response is an answer to `HEAD`, also when the failure comes later in the
head (an invalid `Content-Length`, no `Host`). The blocking adapter's
`408` to a `HEAD` whose head never finished has no content either. A
request line over its budget was never read, and is answered as before.

**Why.** RFC 9110 section 9.3.2; 0.9.1 covered a failure in the body only.

**What to do.** Nothing.

### `outstanding()` counts the response that failed

**What changed.** When a response fails partway (a parse error, the peer
closing mid-response), `ClientConn::outstanding()` counts that request as
well as those queued behind it. A clean close is unchanged.

**Why.** The documented use is to retry that many requests on a new
connection; one was being dropped.

**What to do.** Nothing if you retry from the count. If you added one to
work around this, stop.

### The connection is reusable at `ResponseEnd`

**What changed.** When a response ends, the connection stays open, and
nothing else is outstanding, `send_request` accepts any request without a
further `poll`, and `can_send()` says so. With a pipelined request still
outstanding, `is_reusable()` is false and `progress()` is
`AwaitingResponse` (it was `Idle`).

**Why.** `is_reusable()` said yes while `send_request` refused anything
but a pipelined idempotent request, and `Idle` stopped a driver's response
deadline with a request outstanding.

**What to do.** Nothing. A driver that polls to `Idle` first still works.

## 0.9.0 to 0.9.1

Fixes only; no signature changes. Four behaviours can be noticed.

### An error answer to `HEAD` has no content

**What changed.** When a `HEAD` request fails in its body (a bad chunk,
the peer closing mid-body), the one error response `respond` writes is an
answer to `HEAD`: the head of `Response::from_error`, without the error
text. The blocking adapter's `408` to a `HEAD` likewise has no content
once its head is complete. A failure in the head is answered as before
(until 0.9.2).

**Why.** RFC 9110 section 9.3.2: a response to `HEAD` carries no content.
A front end that reused the connection would have read the text as the
next response.

**What to do.** Nothing.

### The pipelining client stops after a response that closes

**What changed.** Once the head of the response being read says the
connection ends with it (`Connection: close`, HTTP/1.0 without
keep-alive, a body read until close), `ClientConn::can_send` is false and
`send_request` refuses another request with `InvalidState` instead of
writing it.

**Why.** RFC 9112 section 9.6: the client must stop sending on that
connection. The request written before was never answered.

**What to do.** Send the next request on a new connection, as for any
`Closed`; `outstanding()` still counts what was sent before the response
arrived.

### Body bytes print as lengths in `Debug`

**What changed.** `ServerEvent::Body`, `ClientEvent::Body`,
`blocking::Incoming`, and the `leftover` of `blocking::Upgraded` print
`N bytes` instead of the bytes.

**Why.** A body or a tunnelled request can carry credentials no field
redaction sees (T-35).

**What to do.** Log the bytes yourself if you need them; the fields and
accessors are unchanged.

### `blocking::Server::serve` waits on persistent accept errors

**What changed.** After more than 1,000 accept errors in a row, `serve`
stops live connections and waits for them before it returns the error, as
it does on shutdown. It returned at once before.

**Why.** Connection threads kept running after `serve` had returned.

**What to do.** Nothing, unless you relied on `serve` returning before
those connections finished.

## 0.8 to 0.9

0.9 freezes the API as the 1.0 candidate. One change needs a code change;
the fixes in the changelog's 0.9.0 **Fixed** section can also be noticed:
`Debug` of `ResponseHeadRef` and `blocking::Connection`, `progress()`
reporting `Idle` where it reported `Closed`, a missed request deadline
being `TimedOut` on Unix, and two `Display` texts.

### `blocking::Upgraded` is `#[non_exhaustive]`

**What changed.** The struct the blocking server hands to the upgrade hook
is marked `#[non_exhaustive]`. Its fields (`stream`, `leftover`, `target`)
are unchanged and still public.

**Why.** The caller receives it and never builds it. Marking it now lets a
1.x release add a field without breaking anyone.

**What to do.** Nothing if you read its fields or move them out with
`let Upgraded { stream, leftover, .. } = up;`. A pattern that names every
field without `..` needs the `..`; code outside the crate can no longer
build an `Upgraded` with a struct literal.

## 0.7 to 0.8

Nothing a 0.7 user can notice changes. The generic field grammar (tokens,
whitespace, the field-value check, decimal and hex digits, the calendar
behind `Date`) now comes from the first-party crate `parse-net` 0.7; every
input is accepted or refused exactly as before, with the same errors, and
every serialized byte is the same. `parse-net` has no dependencies of its
own and builds without `std` or `alloc`, so `no_std` builds are unaffected.

## 0.6 to 0.7

### A `101` must name a protocol the client offered

**What changed.** `ClientConn` now switches on a `101 Switching Protocols`
only when every protocol in the response's `Upgrade` field is one the
request offered with `Request::upgrade` (names compare case-insensitively,
and a bare name matches a versioned one either way). Any other `101` is
`UnexpectedUpgrade`, as a `101` nobody asked for already was.

**Why.** RFC 9110 section 7.8 lets a server switch only to a protocol the
client listed. Accepting any protocol let a back end answering a
`websocket` request take the connection over for something else (threat
T-21).

**What to do.** Nothing for a server that answers with the protocol it was
offered. If you talk to a server that answers with a different spelling of
the same protocol, offer that spelling.

## 0.5 to 0.6

Nothing a 0.5 user can notice changes: every message is accepted or
refused exactly as before, with the same errors, and every serialized byte
is the same. 0.6 is faster (the common field line and chunk line are
parsed in one pass, status lines come from a compile-time table); the
general paths still decide every other input.

The blocking server now writes full bodies over 4 KiB after the head with
one vectored write instead of copying them; the bytes on the wire are the
same.

### Added in 0.6

- `ServerConn::respond_head_only`: the head only, for a vectored send of
  head and body.

## 0.4 to 0.5

### The HTTP/2 preface has its own error

**What changed.** A connection that starts with the HTTP/2 connection
preface (`PRI * HTTP/2.0` CRLF CRLF `SM` CRLF CRLF, RFC 9113 section 3.4) is
refused with the new `HttpError::Http2Preface` instead of
`UnsupportedVersion`. Both map to status 505, so an answer built with
`Response::from_error` is unchanged. The check runs only before a
connection's first request; an `HTTP/2.0` request line later on a
connection is still `UnsupportedVersion`. While the first bytes of a
connection could still be the preface, the connection waits for more
input instead of parsing them.

**What to do.** Nothing, unless you matched `UnsupportedVersion` to detect
prior-knowledge HTTP/2: match `Http2Preface` (or call `is_h2_preface`
before feeding the connection) and route to an HTTP/2 engine.

### Added in 0.5

- Client pipelining: `Limits::with_pipeline_depth`, `ClientConn::outstanding`,
  `ClientConn::can_send` (the default depth of 1 keeps 0.4 behaviour).
- Reuse and pooling on both connections: `reset`, `with_buffer`,
  `into_buffer`, `shrink_idle`; `ServerConn::set_wipe` (off by default).
- Spans and copied heads: `Span`, `FieldSpan`, `RawHead`, `RequestHead::raw`,
  `ResponseHead::raw`, `RequestHead::copy_into`, `RequestHead::to_owned`,
  `OwnedRequestHead`, `HeadFacts`.
- Stateless parsers: `parse_request_head`, `parse_response_head`, `Parsed`,
  `ParsedRequest`, `ParsedResponse`, `Asked`.
- The Tier-3 writer: `Output`, `ResponseHeadRef`, `OutgoingBody`, `BodyPlan`,
  `ServerConn::write_head`, `respond_into`, `send_body_into`,
  `end_body_into`; `Response::clear`.
- `ChunkedDecoder` and `ChunkStep`.
- `Request::forward_from`, `is_hop_by_hop`, `RequestHead::connection_options`.
- `is_h2_preface`.
- `ClientLeniency`, `ClientConn::set_leniency` (strict by default).

## 0.3 to 0.4

Update the dependency:

```toml
[dependencies]
http-net = "0.4"
```

Most code builds unchanged. `ServerEvent` and `ClientEvent` gained
variants, but both are `#[non_exhaustive]`, so every `match` on them
already has a wildcard arm; the new variants appear only when you opt in.

### One field line is at most 8 KiB by default

**What changed.** `Limits` has a per-line budget, `max_field_line`, 8 KiB by
default. A longer field line (in headers or trailers) is refused with the
new `HttpError::FieldLineTooLong`, answered 431. In 0.3 one line could use
the whole 16 KiB header section.

**Why.** A single huge line is how a header section budget gets spent in
one go; 8 KiB matches common proxy limits.

**What to do.** If you accept very long cookies or tokens, raise it:

```rust
let limits = http_net::Limits::new().with_max_field_line(16 * 1024);
assert_eq!(limits.max_field_line(), 16 * 1024);
```

### `send_continue` reports `InterimNotAllowed`

**What changed.** `ServerConn::send_continue` returns the new
`HttpError::InterimNotAllowed` (was `InvalidState`) for an HTTP/1.0
request, after a body byte was read or a response started, when called
twice, and for a request with no body left to read. With no current request
(none arrived, or the connection failed, closed, or upgraded) it is still
`InvalidState`.

**What to do.** Match on `InterimNotAllowed` if you matched on the old
error; both mean "skip the interim response".

### The client counts interim responses

**What changed.** A client stops after `Limits::max_interim` (16 by default)
interim responses to one request, with the new `HttpError::TooManyInterim`;
0.3 stopped only at the header byte budget (`HeadersTooLarge`).

**What to do.** Nothing for real servers; raise `max_interim` if a server
legitimately sends more `102 Processing` responses.

### More reserved outgoing fields

**What changed.** `Response::header` and `Request::header` refuse
`trailer`, `keep-alive`, and `proxy-connection`, and `Response::header`
refuses `te`, with `ReservedHeader`.

**Why.** They are hop-by-hop fields that belong to the connection (RFC 9110
section 7.6.1); `te` is a request field (section 10.1.4). A caller-set
`keep-alive: timeout=5` could contradict what the connection does.

**What to do.** Remove them from your builders; requests may still send
`te: trailers`.

### Blocking server: 417 and a response deadline

**What changed.** `blocking::Server` answers a request whose `Expect`
holds anything but `100-continue` with `417 Expectation Failed` and closes,
without calling the handler (RFC 9110 section 10.1.1 allows it); 0.3
ignored such expectations. Writing one response may now take at most
`ServerConfig::response_deadline` (60 s by default) in total; a client
that reads slower than that is cut off.

**What to do.** Raise or disable the deadline with
`with_response_deadline` for very large downloads to slow clients.

### Lists split around quoted strings

**What changed.** A comma inside a quoted string no longer splits a list
element, in the fields the crate reads (`Connection`, `Transfer-Encoding`,
`Expect`, `Upgrade`) and in the new `header_list`.

**What to do.** Nothing: no message 0.3 accepted reads differently.

### Added in 0.4

These change no existing behaviour:

- Trailers both ways: `set_report_trailers` and `ServerEvent::Trailers` /
  `ClientEvent::Trailers`; `end_body_with_trailers` on both connections;
  `HttpError::TrailerNotAllowed`.
- Interim responses both ways: `ServerConn::send_informational`,
  `ClientConn::set_report_informational`, `ClientEvent::Informational`,
  `ResponseHead::is_informational`.
- The client side of `Expect: 100-continue`: `Request::expect_continue`,
  `ClientConn::is_waiting_for_continue`, `ClientEvent::Continue`.
- `ServerConn::set_keep_alive`, `keep_alive`, `requests_served`,
  `progress`; `ClientConn::progress`, `is_reusable`; the `Progress` enum.
- `HttpDate`, `DateCache`, `ServerConn::respond_with_date`.
- `header_list` on both views and `ListElements`;
  `RequestHead::upgrade_protocols`, `has_unknown_expectation`.
- `status_allows_body`.
- `Limits` trailer budgets (`max_trailer_bytes`, `max_trailer_fields`) and
  `max_interim`.
- Blocking: `serve`, `post`, `now_date`, `Client::request`,
  `Client::with_deadline`, `ServerConfig::with_max_requests_per_connection`,
  `Connection::with_response_deadline`, `Connection::send_informational`.

## 0.2 to 0.3

Update the dependency:

```toml
[dependencies]
http-net = "0.3"
```

Most code builds unchanged. The changes below are behavioural: they refuse
or hide things 0.2.0 accepted or printed. Each one closes a smuggling or
leak path, so none of them has an opt-out.

### `Debug` output hides credential values

**What changed.** Every `Debug` implementation in the crate now replaces the
values of `authorization`, `proxy-authorization`, `cookie`, and `set-cookie`
with `<redacted N bytes>`, where `N` is the value's length. This covers
`RequestHead`, `ResponseHead`, `Headers`, `ServerEvent` and `ClientEvent`
(which print the views they carry), `Request`, `Response`, and
`blocking::ClientResponse`. Field names are matched ASCII
case-insensitively. In 0.2.0 these values were printed in full.

**Why.** A `{:?}` of a request in a log line printed session cookies and
bearer tokens (threat T-35).

**What to do.** Nothing, unless a test or a log parser compared `Debug`
output that contained one of these values. To read a value, use the
accessors, which are unchanged: `header`, `header_str`, `headers`,
`header_all`, and `header_value` on the builders still return the real
bytes.

```rust
use http_net::{ServerConn, ServerEvent};

let mut conn = ServerConn::new();
let _ = conn.feed(b"GET / HTTP/1.1\r\nHost: a\r\nAuthorization: Bearer t0k3n\r\n\r\n");
let ServerEvent::Request(req) = conn.poll()? else { unreachable!() };
assert!(!format!("{req:?}").contains("t0k3n"));
assert_eq!(req.header("authorization"), Some(&b"Bearer t0k3n"[..]));
# Ok::<(), http_net::HttpError>(())
```

### `h2c` is no longer an upgrade offer

**What changed.** A request whose `Upgrade` field offers only `h2c` now
reports `is_upgrade() == false` and is served as plain HTTP/1.1.
`Upgrade: h2c, websocket` is still an upgrade offer (for `websocket`).
`Response::switching_protocols("h2c")` and `Request::upgrade("h2c")` are
refused with the new `HttpError::UpgradeNotOffered` (when the response is
written, or when the request is sent).

**Why.** RFC 9113 section 3.1 deprecates the `h2c` upgrade, and proxies that
forward `Upgrade: h2c` blindly are the h2c smuggling attack (threat T-20).
RFC 9110 section 7.8 lets a server ignore `Upgrade`, so ignoring `h2c` breaks
no client: it falls back to HTTP/1.1.

**What to do.** If you answered `h2c` upgrades, stop: route prior-knowledge
HTTP/2 to an HTTP/2 engine instead (from 0.5, `is_h2_preface` and
`HttpError::Http2Preface` detect it).

### A `101` must name a protocol the client offered

**What changed.** `ServerConn::respond` with `Response::switching_protocols(p)`
now fails with `HttpError::UpgradeNotOffered` unless the request's `Upgrade`
list offered `p`. Names are compared ASCII case-insensitively
(`WebSocket` matches `websocket`), and a `101` naming only a protocol name
matches an offer of that name with or without a version (`websocket`
matches `websocket/13`). In 0.2.0 any token was accepted.

**Why.** RFC 9110 section 7.8: a server MUST NOT switch to a protocol the
client did not indicate. A proxy that believes an upgrade succeeded while
the origin switched to something else is the upgrade smuggling shape
(threat T-21).

**What to do.** Answer with a protocol taken from the request's `Upgrade`
field (`req.header("upgrade")`), or send a normal response.

### New error variant `HttpError::UpgradeNotOffered`

**What changed.** A usage error for the two cases above. `status()` is
`None`; `is_usage_error()` is true.

**What to do.** `HttpError` has been `#[non_exhaustive]` since it was introduced, so every
`match` on it already has a wildcard arm and keeps compiling. Add an arm if
you want to handle it explicitly.

### Stricter `Host` and authority validation

**What changed.** An empty host before a port (`Host: :80`) and a `%` not
followed by two hex digits (`Host: a%zz`, `Host: a%4`) are now refused:
`InvalidHost` for the `Host` field, `InvalidTarget` for the authority of a
`CONNECT` target or an absolute-form target, and `InvalidHost` from
`Request::header("host", ...)`. Valid escapes (`a%41`), IPvFuture literals made of hex digits, `.` and
`:` (such as `[v1.fe80::a]`),
an empty port (`example.com:`), and an empty `Host` (a target with no
authority) are still accepted. A port above 65535 is still accepted by
syntax, as RFC 3986 does not bound it.

**Why.** Threat T-18: hosts that different components
parse differently are a routing and cache-key confusion vector, and RFC 9112
section 3.2 requires 400 for an invalid `Host`.

**What to do.** Nothing for well-formed clients. Such requests are now
answered 400 by the blocking server.

### The client accepts a status line without the space after the code

**What changed.** `ClientConn` now accepts `HTTP/1.1 200` CRLF (no space and
no reason phrase); `ResponseHead::reason()` is then `""`. In 0.2.0 it was
`InvalidStartLine`. The server side is unaffected.

**Why.** Real servers send it, and it cannot change framing.

**What to do.** Nothing. If you relied on the error to reject such servers,
check `reason()` yourself.

### `reason_phrase` covers the whole status registry

**What changed.** `reason_phrase` now returns the registered phrase for
`102`, `103`, `207`, `208`, `226`, `305`, `402`, `407`, `423`, `424`, `425`,
`451`, `506`, `507`, `508`, `510`, and `511`, which returned `""` in 0.2.0.
`306` and `418` return `""`: the registry lists them as unused (RFC 9110
sections 15.4.7 and 15.5.19). `ServerConn::respond` writes these phrases in
the status line, so a response with one of these codes now carries its
phrase.

**What to do.** Nothing, unless a test compared a status line byte for byte.

### Upgrade protocols may carry a version

**What changed.** `Response::switching_protocols` and `Request::upgrade`
accept a `name/version` product token (`foo/2`) as RFC 9110 section 7.8
defines a protocol; 0.2.0 refused anything but a plain token with
`InvalidHeaderValue`.

**What to do.** Nothing.

### Added in 0.3

These change no existing behaviour:

- `VERSION`: the crate version string.
- `HttpError::is_peer_error` and `HttpError::is_usage_error`.
- `Limits::buffer_capacity`: the input buffer's hard capacity, for memory
  planning.
- `RequestHead::target_form` and the `TargetForm` enum.
- `RequestHead::header_all`, `ResponseHead::header_all`, and the
  `HeaderValues` iterator: every field with one name, in order (each
  `Set-Cookie` separately).
- `RequestHead::content_length` and `ResponseHead::content_length`: the
  declared `Content-Length`, so `Content-Length: 0` (`Some(0)`) can be told
  apart from an absent one; `framing()` still reports `BodyFraming::None`
  for both.
- `Method::is_safe`, `Method::is_idempotent`, `Method::index`, and the
  `MethodSet` bit set (with `write_allow` for the `Allow` field of a 405).
- `StatusClass`, `status_class`, and the `status` module of named status
  code constants.
- `Response::from_error`: the response for a request parse error (the
  blocking server now builds its error answers with it; the bytes it sends
  are the same as in 0.2.0).
- `Request::head`, `Request::put`, `Request::delete`, `Request::options`.