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
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
<h1 align="center">
    <img width="90px" height="auto" src="https://raw.githubusercontent.com/jamesgober/jamesgober/main/media/icons/hexagon-3.svg" alt="Triple Hexagon">
    <br><b>CHANGELOG</b>
</h1>
<p>
  All notable changes to <code>http-net</code> will be documented in this file. The format is based on <a href="https://keepachangelog.com/en/1.1.0/">Keep a Changelog</a>,
  and this project adheres to <a href="https://semver.org/spec/v2.0.0.html">Semantic Versioning</a>.
</p>

---

## [Unreleased]

---

## [0.9.2] - 2026-11-02

Fixes from the documentation audit of 0.9.1, which checked every factual
claim against the build. No API change (`cargo semver-checks` against
0.9.1: no change).

### Fixed

- `ClientConn::outstanding()` left out the request whose response failed
  (a parse error, the peer closing mid-response), so a driver retrying
  from the count dropped one idempotent request.
- At `ResponseEnd`, `is_reusable()` could say yes while `send_request`
  refused anything but a pipelined idempotent request, and `progress()`
  said `Idle` with a pipelined request outstanding. When the connection
  stays open and nothing else is outstanding, `send_request` now accepts
  any request without a further `poll`; with a request outstanding,
  `is_reusable()` is false and `progress()` is `AwaitingResponse`.
- The 0.9.1 `HEAD` fix covered a failure in the body only. Once a `HEAD`
  request line has been read, every error answer has no content,
  including a failure later in the head and the blocking adapter's `408`
  to a head that never finished (RFC 9110 section 9.3.2).

### Documentation

- `docs/STABILITY-1.0.md` scoped the semver promise to items "marked
  exists", which `docs/API.md` defines as items from 0.2.0. It now covers
  every public item in `docs/API.md` apart from those planned after 1.0.
- 18 corrections in all (`dev/PLAN.md` D-75), among them: framing refusals
  do not apply to a response with no body; `UnexpectedUpgrade` also covers
  a `101` naming a protocol not offered; `BareCr` is not raised in a chunk
  line; `blocking::Client` opens one connection per request; `close` and
  `set_keep_alive(false)` do nothing on a tunnel; the client deadline does
  not bound name resolution; the migration notes list every noticeable
  change.
- `docs/PERFORMANCE.md`: aarch64 numbers from the manual benchmark
  workflow.

### Testing

- `tests/prop_split.rs`: the review's split-invariance properties
  (the lenient client, tiny budgets with reporting on, the pipelining
  client, the stateless parser against the connection) and every budget
  at its limit and one over, at every split.
- Regression tests for each fix (`tests/pipelining_reuse.rs`,
  `tests/rfc_cases.rs`, `tests/localhost.rs`).

---

## [0.9.1] - 2026-11-02

Fixes from the 0.9.x polish pass: a second, independent security review
of the frozen code, a performance pass against every release since 0.6
and against the competitors, and a pass over every open item. No API
change (`cargo semver-checks` against 0.9.0: no change).

### Fixed

- An error answer to a `HEAD` request that failed in its body (a bad
  chunk, the peer closing mid-body) carried the error text as content; it
  is now an answer to `HEAD`, without content (RFC 9110 section 9.3.2).
  The blocking adapter's `408` to a `HEAD` likewise has no content. The
  answer always closed the connection, so only a front end ignoring
  `connection: close` could misread it.
- The pipelining `ClientConn` kept accepting requests after the head of
  the response being read said the connection ends with it
  (`Connection: close`, HTTP/1.0 without keep-alive, a body read until
  close). `can_send` is now false and `send_request` refuses with
  `InvalidState` (RFC 9112 section 9.6).
- **Security (T-35):** `Debug` of `ServerEvent::Body`, `ClientEvent::Body`,
  `blocking::Incoming`, and the `leftover` of `blocking::Upgraded` printed
  their bytes, which can carry credentials (a form login; a tunnelled
  request with its `Authorization` field). They print lengths now, like
  `blocking::Connection` and `ClientResponse`.
- `blocking::Server::serve` returned on persistent accept errors without
  waiting for live connections; it now stops them and waits, as a
  shutdown does.

### Documentation

- `Request::forward_from`: every name `Connection` lists is dropped,
  end-to-end names included, so a client can strip fields a proxy relies
  on; check `connection_options()` first. `commit(n)` clamps an `n`
  larger than `read_buf`.
- `docs/PERFORMANCE.md`: the 0.9.x pass. No regression against 0.6.0,
  0.7.0, or 0.8.0 (interleaved, nine rounds); what the response-write
  benchmarks contain (about 62 ns of their time is reading the request
  they answer); the measured cost of `set_wipe` (+2.1% on the browser
  GET, replacing an estimate); the competitor ratios on 0.9.0.
- `docs/SECURITY.md`: the review record, measured wipe cost, and the T-40
  byte cap now told apart from the time cap by a test.
- `dev/PLAN.md` D-71 to D-73; the module table matches the code.

### Testing

- Regression tests for each fix except the wait in `serve`, which needs a
  failing listener (`tests/rfc_cases.rs`, `tests/pipelining_reuse.rs`,
  `tests/redaction.rs`, `tests/localhost.rs`), and a unit test for the
  lingering-close byte cap.
- The reference parser behind the `reference_diff` fuzz target and
  `tests/prop_reference.rs` overflowed on a chunk size near `u64::MAX`
  (found by the fuzz rerun; the crate itself refuses such a size). Fixed,
  with `test_huge_chunk_sizes_agree`.
- Every fuzz target ran again for 15 minutes, and the HTTP Garden ran
  again, on this code: no finding in the crate.
- A `wipe` group in `benches/bench.rs`; a manual benchmark workflow for
  aarch64 (`.github/workflows/bench.yml`).

---

## [0.9.0] - 2026-10-29

Beta: the API is frozen as the 1.0 candidate; `0.9.x` releases contain
fixes only. A documentation audit against the build found and fixed four
defects, two of them `Debug` outputs that printed credentials.

### Added

- `docs/STABILITY-1.0.md`: what 1.0 commits to, which types may grow in
  1.x, the behaviour guarantees, and the 1.0 audit record.
- `docs/PARITY.md`: the feature matrix against hyper, httparse, h11,
  llhttp, picohttpparser, and the `http` crate, every cell checked.
- An HTTP Garden differential job (`.github/workflows/garden.yml`, manual
  runs only) with its origin in `garden/` (not part of the package). Its
  first run compared http-net with hyper, nginx, h2o, and gunicorn over
  679 cases and found no defect (`docs/SECURITY.md`).

### Changed

- `blocking::Upgraded` is `#[non_exhaustive]`, so a 1.x release can add a
  field; its fields are unchanged. A pattern naming every field needs `..`
  (`docs/MIGRATION.md`).

### Fixed

- **Security (T-35):** `ResponseHeadRef` and `blocking::Connection`
  printed credential values in their `Debug` output (`ResponseHeadRef`
  derived it; `Connection` kept the last request body and serialized
  response and printed both). `ResponseHeadRef` now redacts
  `authorization`, `proxy-authorization`, `cookie`, and `set-cookie` like
  every other type, and `Connection` prints its buffers as lengths only.
  Both are in `tests/redaction.rs`.
- `ServerConn::progress` reported `Closed` for a keep-alive request with no
  body left to read that was answered before `RequestEnd` was polled (for
  example a `GET` answered at `ServerEvent::Request`). It now reports
  `Idle`, so a driver that closes on `Closed` keeps the connection.
- `blocking::Connection` reported a missed request deadline as
  `WouldBlock` on Unix; it is `TimedOut` on every platform, as documented.
- The `Display` text of `HttpError::InterimNotAllowed` contained a run of
  spaces (a missing line continuation).
- The `Display` text of `HttpError::InvalidHeaderValue` also names the
  other case it covers: surrounding whitespace in an outgoing value.

### Documentation

- Every factual claim in the README, `docs/`, the changelog, and the
  rustdoc re-checked against the build and corrected where it was wrong
  (among them the `Transfer-Encoding` rule, the buffer bound, the sizes of
  `HttpError` and `RequestHead`, pipelining in the `ClientConn` docs, the
  `ChunkedDecoder` errors, and the `OwnedRequestHead::from_parts` check,
  which is structural and not an integrity check).

---

## [0.8.0] - 2026-10-26

Alpha: the shared RFC 9110 field grammar comes from the first-party crate
`parse-net`, and the first consumers (serve-net, LexerSketch Studio) run
end to end against this release with no change. Nothing a 0.7 user can
notice changes.

### Changed

- The generic RFC 9110 field grammar now comes from the first-party crate
  `parse-net` 0.7 (published version, `default-features = false`, itself
  dependency-free and `no_std` without `alloc`): tokens and `tchar`, OWS
  trimming, the field-value check, `qdtext`, hex digits, decimal parsing,
  and the calendar behind `HttpDate`. Behaviour is unchanged byte for byte,
  checked by equality tests against the 0.7 implementation; the public API
  is unchanged. HTTP/1.1 framing (request-target and `Host` checks, the
  reason phrase, the head's byte scanners, the `#list` walker, the number
  writers) stays in this crate. http-net still has no third-party runtime
  dependency.

### Added

- Equality tests for every item taken from `parse-net`: each classifier
  over every byte, the field-value check at every position of a 17-byte
  value and on every pair of refused bytes, OWS trimming at both ends,
  decimal parsing on 400,000 generated cases, and the `Date` output for
  every day from 1970 to 9999, each against the 0.7 implementation.

---

## [0.7.0] - 2026-10-26

Hardening: a 24-CPU-hour fuzz campaign per target, an 8-hour soak of the
blocking server, an attack test for every threat in the specification
(which found and fixed one client upgrade gap), a response-side
differential against httparse, and the completed `docs/SECURITY.md` and
`docs/PLATFORM-NOTES.md`. See `docs/MIGRATION.md` for the one change a 0.6
user can notice.

### Security

- The client switches protocols on a `101` only when every protocol its
  `Upgrade` field names is one the request offered (RFC 9110 section
  7.8); otherwise `UnexpectedUpgrade` (threat T-21). Before, any non-empty
  `Upgrade` was accepted, so a server answering a `websocket` upgrade with
  another protocol took over the connection.

### Added

- `examples/soak.rs`: the soak test of the blocking server (mixed
  workload, one status line per minute, resident memory on Linux).
- An attack test for the accept loop's back-off on persistent accept
  errors (threat T-41).
- Attack tests for every gap the 0.7 threat audit found: non-101 answers
  and protocol-less 101s to an upgrade, a server 101 before the body is
  read, HTTP/0.9 responses, over-long chunk lines (and a 10 MiB stream of
  one), an unanswered pipelining flood, the lingering-close byte cap, a
  slow body answered `408`, wiping on reset, builder injection through
  targets, methods, and values, reserved request fields, `Transfer-Encoding`
  split across fields, redaction of every owned and stateless view,
  `from_error` for every variant, and client trailers that cannot reframe.
- `tests/prop_httparse_response.rs`: the response-side differential against
  httparse (47,249 single-byte mutations and three property tests).
- `docs/PLATFORM-NOTES.md`: timeouts, lingering close, ports and
  TIME_WAIT, memory measurement, `no_std` targets, and fuzzing per
  platform. `docs/SECURITY.md` complete, with the 0.7 hardening record.

---

## [0.6.0] - 2026-10-22

Optimisation: one-pass parsing of the common field and chunk lines,
compile-time status lines, `respond_head_only` with vectored writes, the
comparison repository `http-net-bench`, and `docs/PERFORMANCE.md`. Nothing
a 0.5 user can notice changes except speed (see `docs/MIGRATION.md`).

### Added

- `ServerConn::respond_head_only` (A-224): writes only the head of a
  response and returns the `BodyPlan`, so a driver sends a full body after
  the head with one vectored write instead of copying it into the output
  buffer. The head plus the body are exactly what `respond` writes.
- The blocking server sends full bodies over 4 KiB after the head with one
  `write_vectored`, without copying them.
- `docs/PERFORMANCE.md`: method, numbers, the comparison with httparse and
  hyper, and the decisions behind the optimisations.
- The comparison repository
  [`http-net-bench`]https://github.com/jamesgober/http-net-bench
  (workloads H1 to H5 against httparse and hyper on identical bytes).
- `scripts/bench_ab.sh`: interleaved A/B benchmarks of the working tree
  against a tag, pinned to one core, for the 5% regression gate.
- Tests: the linearity guard (`tests/linearity.rs`: a head fed one byte at
  a time costs time linear in its size); `respond_head_only` equivalence
  with `respond` over 2,048 generated cases (`tests/head_only.rs`) and over
  real sockets; the fast paths against the general paths on 300,000
  generated field lines and 200,000 chunk lines; `find_byte` at every
  position among near-miss bytes; the status-line table against the parts
  for every code.

### Changed

- Faster head parsing: the common field line (token name, colon, spaces,
  printable value, CR LF) is parsed in one pass instead of three; every
  other shape takes the general path, so what is accepted and refused does
  not change. About 20% faster on the 12-field request head, 15% on
  client response parsing, 12% on pipelined requests.
- Faster chunked bodies: the common chunk line (hex digits, CR LF) is
  parsed in one pass. About 9% faster on a 1 MiB chunked body, 36% on
  `ChunkedDecoder` framing.
- Faster responses: status lines come from a compile-time table built
  from the status registry. About 10% faster on response serialization.

---

## [0.5.0] - 2026-10-21

Feature complete: client pipelining, connection reuse and buffer pooling,
optional wiping of consumed input, and the allocation-free Tier-3 surface
(spans and copied heads, stateless parsers, the `Output` writer, a
standalone chunked decoder, request forwarding, HTTP/2 preface detection),
plus opt-in client tolerance for folded response fields. See
`docs/MIGRATION.md` for what a 0.4 user can notice.

### Added

- Client pipelining: `Limits::with_pipeline_depth` / `pipeline_depth`
  (1 to 32, default 1), `ClientConn::outstanding` and `ClientConn::can_send`.
  Idempotent requests with full bodies are sent before earlier responses
  arrive and matched to them in order from a fixed array.
- Connection reuse and buffer pooling: `reset`, `with_buffer`,
  `into_buffer`, and `shrink_idle` on both connections.
- `ServerConn::set_wipe`: zero each finished request's bytes, the stale
  copies compaction leaves, and the whole buffer on reset, release, and
  drop (threat T-36).
- `Request::forward_from`: a request for an upstream built from a received
  head, with the hop-by-hop fields (RFC 9110 section 7.6.1) removed and the
  framing mode kept; `is_hop_by_hop` and `RequestHead::connection_options`.
- `is_h2_preface`, and the error `HttpError::Http2Preface` (505).
- `ClientLeniency` and `ClientConn::set_leniency`: an opt-in tolerance for
  obsolete line folding in response heads, replaced by spaces in place
  (RFC 9112 section 5.2); every other refusal stays.
- Example `proxy_forward`.
- Example `pipelined_client`: four requests in flight on one connection.
- `docs/ARCHITECTURE.md` (modules, data flow, state machines, framing,
  memory) and the `docs/SECURITY.md` draft (threat model T-1 to T-42 with
  each defence, test, and cost; the deliberate strictness choices).
- Tests: pipelining, reuse, and wiping (`tests/pipelining_reuse.rs`, with a
  property test and control runs for the wipe); allocation tests for
  pipelined responses, wiping, and reset; the `client_conn` fuzz target
  runs with fold leniency and checks that leniency changes nothing on
  unfolded input.
- Spans and raw heads (Tier 3): `Span` and `FieldSpan`, public `u32`
  offsets into a head's bytes (`Span::get` returns an empty slice for a
  span out of range and never panics); `RequestHead::raw` and
  `ResponseHead::raw` return a `RawHead` with the head bytes and the spans
  of the start line, method, target, version, status, and every field.
- The head passport (OD-12): `RequestHead::copy_into` copies the head
  bytes and field spans into caller-owned, reused vectors and returns
  `HeadFacts`; `OwnedRequestHead::from_parts` rebuilds a view from them
  after checking every span and the line structure (`InvalidState` for a
  changed buffer), `view` reads it, and `into_parts` gives the vectors
  back. `RequestHead::to_owned` makes an owned copy in one call. Once the
  vectors have grown, the copy allocates nothing.
- Stateless head parsers (Tier 3): `parse_request_head` and
  `parse_response_head` read one head from a caller slice into a caller
  array of field spans, with the same scanner and rules as the
  connections, and no allocation. They return `Parsed::Complete` with
  `ParsedRequest` / `ParsedResponse` and the head length, or
  `Parsed::Partial`; a head with more fields than the array is
  `TooManyHeaders`. `Asked` tells the response parser whether the request
  was `HEAD` or `CONNECT`.
- Example `passport`: a reader parses pipelined requests with the
  stateless parser and passes copied heads through a passport pool to a
  worker thread.
- Tests: unit tests for every `from_parts` refusal; property tests that
  the stateless parsers agree with `ServerConn` and `ClientConn` and that
  a copied head rebuilds the same view; allocation tests for the
  stateless parsers and `copy_into` into grown vectors (zero
  allocations); fuzz target `stateless_parse` (oracle: equal to
  `ServerConn` and `ClientConn`, copies rebuild) in the CI fuzz matrix.
- `Output`: where serialized bytes go, a `Vec<u8>` or a caller type (a
  zeroizing buffer, a slab, a counter). `put` cannot fail; `reserve` is a
  hint. `&mut O` is an `Output` for any `O: Output`.
- The borrowed writer: `ServerConn::write_head` writes a response head from
  a `ResponseHeadRef` (borrowed fields, an `OutgoingBody`, `close`, an
  optional upgrade protocol) to any `Output` and returns a `BodyPlan`. It
  writes the bytes `respond` writes for the equivalent `Response` and fails
  the same way, and allocates nothing with a reused `Vec`.
- `ServerConn::respond_into`, `send_body_into`, `end_body_into`:
  `respond_with_date`, `send_body`, `end_body_with_trailers` over any
  `Output`.
- `Response::clear`: reset a response for reuse, keeping its field
  allocation.
- `ChunkedDecoder` and `ChunkStep`: a standalone chunked-body decoder over
  caller-owned bytes, with explicit byte accounting and the connection's
  grammar and budgets. It shares the connection's decoder and allocates
  nothing.

### Changed

- A connection that starts with the HTTP/2 connection preface is refused
  with `Http2Preface` instead of `UnsupportedVersion` (both answer 505). The
  check runs only before a connection's first request.
- `respond`, `send_body`, `end_body`, and `end_body_with_trailers` now run
  through the generic `Output` code; their behaviour and output are
  unchanged.

---

## [0.4.0] - 2026-10-21

Extended: trailers and interim responses both ways, the client side of
`Expect: 100-continue`, keep-alive control and progress for drivers, a
cached `Date`, per-line and trailer budgets, and total deadlines in the
blocking adapter. See `docs/MIGRATION.md` for what a 0.3 user can notice.

### Added

- Trailers both ways. `ServerConn::set_report_trailers` and
  `ClientConn::set_report_trailers` report a non-empty trailer section as
  `ServerEvent::Trailers` / `ClientEvent::Trailers` before the end event;
  `ServerConn::end_body_with_trailers` and
  `ClientConn::end_body_with_trailers` send one after a chunked body,
  refusing the fields RFC 9110 section 6.5.1 keeps out of trailers with the
  new `HttpError::TrailerNotAllowed`.
- Interim responses both ways. `ServerConn::send_informational` sends a
  1xx other than 100 and 101 (for example `103 Early Hints`) before the
  final response; `ClientConn::set_report_informational` reports them as
  `ClientEvent::Informational`; `ResponseHead::is_informational`.
- The client side of `Expect: 100-continue`: `Request::expect_continue`,
  `ClientConn::is_waiting_for_continue`, `ClientEvent::Continue`. A final
  status instead of `100` refuses the promised body and closes the
  connection after the response.
- Driver controls: `ServerConn::set_keep_alive` and `keep_alive`,
  `requests_served`, and `progress` with the new `Progress` enum (also
  `ClientConn::progress`, `ClientConn::is_reusable`).
- `HttpDate` and `DateCache` (IMF-fixdate, formatted at most once per
  second; the caller supplies the time) and
  `ServerConn::respond_with_date`.
- `RequestHead::header_list`, `ResponseHead::header_list`, and the
  `ListElements` iterator; `RequestHead::upgrade_protocols` (the offered
  protocols, `h2c` left out); `RequestHead::has_unknown_expectation`.
- `status_allows_body`.
- `Limits::with_max_field_line` / `max_field_line` (8 KiB by default),
  `with_max_trailer_bytes` / `max_trailer_bytes`,
  `with_max_trailer_fields` / `max_trailer_fields` (both following the
  header budgets until set), `with_max_interim` / `max_interim` (16).
- Errors: `FieldLineTooLong` (431), `TooManyInterim`, `InterimNotAllowed`,
  `TrailerNotAllowed`.
- Blocking adapter: `blocking::serve` (bind and serve in one call),
  `blocking::post`, `blocking::now_date` (a per-thread `DateCache` over the
  system clock), `Client::request` (any `Request` to an `http://` URL),
  `Client::with_deadline` (one total deadline per exchange),
  `ServerConfig::with_response_deadline` (60 s to write one response) and
  `with_max_requests_per_connection`, `Connection::with_response_deadline`,
  and `Connection::send_informational`.
- Examples `early_hints` and `trailers`.

### Changed

- One field line may be at most 8 KiB by default (`FieldLineTooLong`); in
  0.3 a single line could use the whole 16 KiB header section. Raise it
  with `Limits::with_max_field_line`.
- `ServerConn::send_continue` reports `InterimNotAllowed` (was
  `InvalidState`) for an HTTP/1.0 request, after the body began or a
  response started, and when sent twice.
- The client stops after `max_interim` interim responses to one request
  (`TooManyInterim`), as well as at the header byte budget.
- Outgoing messages may no longer set `trailer`, `keep-alive`, or
  `proxy-connection`, and responses may not set `te` (`ReservedHeader`):
  they belong to the connection (RFC 9110 sections 7.6.1, 10.1.4).
- `Limits::buffer_capacity` accounts for a trailer budget larger than the
  header budget; with the defaults it is unchanged (64 KiB).
- The blocking server answers a request with an expectation other than
  `100-continue` with `417 Expectation Failed` (and closes) instead of
  ignoring it; `date` comes from a per-second cache instead of being
  formatted for every response.

### Security

- The blocking server's total response deadline (60 s by default) stops a
  client that reads one byte at a time from holding a worker thread during
  a large response (threat T-26); before, only each write had a timeout.
- The blocking client's `with_deadline` bounds a whole exchange, so a slow
  server cannot hold a caller past it.
- `dev/PLAN.md` records the 0.4 decisions (D-13 to D-20), among them
  keeping the shared grammar internal until `parse-net` is published.

### Fixed

- List fields are split with a quote-aware walker: a comma inside a quoted
  string no longer ends an element (the behaviour of the shared grammar
  crate `parse-net`). No message 0.3 accepted reads differently; this
  removes a latent differential between the two crates.

---

## [0.3.0] - 2026-10-21

Core, with the design lock revisited: `docs/API.md` now describes the
whole 1.0 surface, each item marked with the version it ships in, and was
approved before this release's code changes. This release closes a
credential leak in 0.2.0's `Debug` output, refuses the `h2c` upgrade and
switches to unoffered protocols, tightens `Host` validation, fills in the
method and status registries, and adds fuzzing, an allocation-counting
test, a differential test against `httparse`, and an RFC conformance table.
See `docs/MIGRATION.md` for what a 0.2.0 user can notice.

### Added

- `VERSION`: the crate version string.
- `HttpError::is_peer_error` and `HttpError::is_usage_error`, and the
  variant `HttpError::UpgradeNotOffered`.
- `Limits::buffer_capacity`: the input buffer's hard capacity, for planning
  memory per connection.
- `RequestHead::target_form` and the `TargetForm` enum (`Origin`,
  `Absolute`, `Authority`, `Asterisk`).
- `RequestHead::header_all`, `ResponseHead::header_all`, and the
  `HeaderValues` iterator: every field with one name, in arrival order
  (each `Set-Cookie` separately).
- `RequestHead::content_length` and `ResponseHead::content_length`: the
  declared `Content-Length`, telling `Some(0)` from an absent field.
- `Method::is_safe`, `Method::is_idempotent`, `Method::index`, and
  `MethodSet`, a bit set of methods with `write_allow` for the `Allow` field
  of a `405`.
- `StatusClass`, `status_class`, and the `status` module: a named `u16`
  constant for every registered status code.
- `Response::from_error`: the response for a request parse error (status,
  short `text/plain` body, `close`); the blocking server uses it.
- `Request::head`, `Request::put`, `Request::delete`, `Request::options`.
- `docs/MIGRATION.md`, `dev/PLAN.md` (design and decision log), and
  `.editorconfig`.
- Tests: `tests/alloc.rs` (a counting allocator: 10,000 keep-alive requests
  on one `ServerConn`, and 10,000 responses on one `ClientConn`, allocate
  nothing after warm-up; 10 MiB hostile streams never grow a connection
  past its buffer capacity), `tests/prop_httparse.rs` (differential against
  `httparse` 1.10 on every head both accept, and a classified count of the
  heads only `httparse` accepts), `tests/conformance.rs` (RFC 9112 and RFC
  9110 statements as a table, one check each), `tests/redaction.rs`, and
  new attack vectors for chunk-extension line ends (the Funky Chunks
  classes), 0.CL and `Expect`-based desync, `h2c` and unoffered upgrades,
  and `Host` escapes. The `docs/API.md` and `docs/MIGRATION.md` examples
  run as doctests.
- Fuzzing: six cargo-fuzz targets in `fuzz/` (`server_conn`, `client_conn`,
  `chunk_line`, `reference_diff`, `httparse_diff`, `builders`), each run
  for 60 s in CI.
- CI: a `no_std` build for `thumbv7em-none-eabihf` and the fuzz smoke run.

### Changed

- `RequestHead::is_upgrade` is false for a request whose `Upgrade` offers
  only `h2c`; such a request is served as plain HTTP/1.1 (RFC 9113 section
  3.1).
- `ServerConn::respond` refuses a `101` naming a protocol the request did
  not offer with `UpgradeNotOffered` (RFC 9110 section 7.8). Names are
  compared ASCII case-insensitively, and a name without a version answers
  an offer of that name with any version.
- `Response::switching_protocols` and `Request::upgrade` accept a
  `name/version` product token and refuse `h2c` with `UpgradeNotOffered`.
- The client accepts a status line without the space after the code
  (`HTTP/1.1 200`); `reason()` is then empty.
- `reason_phrase` covers every code in the IANA registry: `102`, `103`,
  `207`, `208`, `226`, `305`, `402`, `407`, `423`, `424`, `425`, `451`,
  `506`, `507`, `508`, `510`, and `511` were added; `306` and `418` stay
  empty as reserved.
- `Method` and the status registry moved to their own modules; the public
  paths (`http_net::Method`, `http_net::reason_phrase`) are unchanged.
- `dev/ROADMAP.md` follows the library specification's version plan: fuzz
  targets moved from 0.9 to 0.3, the client `Expect`, interim responses,
  and blocking deadlines to 0.4, client pipelining to 0.5; the async
  adapter is withdrawn (no crate in this family takes an async runtime).

### Security

- `Debug` for `RequestHead`, `ResponseHead`, `Headers`, `HeaderValues`,
  `ServerEvent`, `ClientEvent`, `Request`, `Response`, and
  `blocking::ClientResponse` replaces the values of `authorization`,
  `proxy-authorization`, `cookie`, and `set-cookie` with
  `<redacted N bytes>`. In 0.2.0 a `{:?}` of a request printed session
  cookies and bearer tokens in full.
- `Host` values with an empty host before a port (`:80`) or a `%` not
  followed by two hex digits are refused (`InvalidHost`), as are such
  authorities in `CONNECT` and absolute-form targets (`InvalidTarget`).
- The `h2c` upgrade is never honoured (h2c smuggling), and a server cannot
  switch to a protocol the client did not offer.

---

## [0.2.0] - 2026-10-12

The foundation: a sans-IO HTTP/1.1 engine for servers and clients, strict
per RFC 9112, with request smuggling defences, bounded buffers, keep-alive,
chunked bodies both ways, the protocol-upgrade hand-off for WebSocket,
`Expect: 100-continue` on the server, and a small blocking adapter over
`std::net`. No dependencies.

### Added

- `ServerConn` and `ServerEvent`: the server-side state machine. Input in
  through `feed` or `read_buf` + `commit`, events out through `poll`
  (`Request`, `Body`, `RequestEnd`, `NeedInput`, `AwaitingResponse`,
  `Upgraded`, `Closed`), responses out through `respond`, `send_body`, and
  `end_body`. Keep-alive is automatic; pipelined requests are served in
  order. A request without a body may be answered at `Request` and stays
  keep-alive; one with unread body bytes is closed afterwards.
- `ServerConn::send_continue` and `RequestHead::expects_continue`: the
  `100 Continue` answer RFC 9110 section 10.1.1 requires before a server
  waits for an expected body (HTTP/1.1 requests with a body only; never to
  HTTP/1.0 clients).
- `ClientConn` and `ClientEvent`: the client-side state machine
  (`send_request`, streamed request bodies, response parsing framed by the
  request method, interim 1xx responses skipped, `Idle` when reusable).
- `RequestHead`, `ResponseHead`, and `Headers`: zero-copy views of parsed
  heads. For an absolute-form target, `RequestHead::host` is the target's
  authority and `path` / `query` are its path and query (RFC 9112 section
  3.2.2: the server ignores `Host` then).
- `Request` and `Response` builders that validate as they go and own no
  framing headers; `Response::switching_protocols` and `Request::upgrade`
  for protocol upgrades.
- `Method`, `Version`, `BodyFraming`, `reason_phrase`.
- `Limits`: request/status/chunk line (8 KiB), header section (16 KiB),
  field count (100), and body (1 MiB) budgets, each checked before the
  bytes it governs are buffered. The header budget also caps, per message,
  the total of chunk extensions and of interim 1xx heads.
- `HttpError` (`#[non_exhaustive]`): one variant per refusal, with
  `status()` giving the status code to answer with.
- `blocking` module (feature `std`): `Server` (bounded thread per
  connection, connection cap with 503, read/write timeouts, a per-request
  deadline answered with 408, automatic `100 Continue`, `date` header,
  lingering close bounded to 1 s, 500 for a streamed or invalid handler
  response, upgrade hook, `ShutdownHandle`), `ServerConfig` (including
  `with_request_timeout`, 30 s by default), `Connection` for custom accept
  loops and upgrades (`with_request_timeout`), `Client` and `get` for
  `http://` URLs.
- Tests: RFC 9112 accept/reject/response cases as data, a smuggling suite,
  an adversarial suite (target forms and the authority, `Expect`, chunk and
  trailer edges, cumulative budgets, integer edges), property tests (round
  trip including absolute-form targets and keep-alive, split invariance,
  no panics on arbitrary bytes from the first byte and behind valid heads),
  a differential test against an independent reference parser, a
  100,000-request scale test, and localhost socket tests (including
  slowloris heads and bodies, a refused client trickling data, lingering
  close, and `Expect: 100-continue`).
- Criterion benchmarks (`benches/bench.rs`) and examples (`localhost`,
  `sans_io`, `serve`, `get`).

### Security

- Refused outright, each with its own `HttpError`: `Content-Length` with
  `Transfer-Encoding`; a duplicate (even equal) or list-valued
  `Content-Length`, or one with a sign, space, or hex; any
  `Transfer-Encoding` other than a single final `chunked`, in requests and
  responses alike; a transfer coding other than `chunked` (501);
  `Transfer-Encoding` in HTTP/1.0; obsolete line folding; a bare CR
  anywhere in a head; a bare LF as a line end; whitespace before a colon;
  control bytes in field values and non-token field names; a missing,
  duplicate, or malformed `Host`; request targets outside RFC 3986, in a
  form the method does not allow, or in absolute form without
  `//authority`, with userinfo, or with an empty host; chunk sizes over 16
  hex digits, malformed chunk extensions, chunk data without its CRLF; and
  more than the header budget of chunk extensions in one message (the
  CVE-2024-22019 pattern). After a parse error the connection parses
  nothing further.
- Trailer fields are validated, counted, and discarded: framing fields in a
  trailer cannot reframe the message or the next one.
- A read-until-close response body is delivered up to `max_body` and
  refused at the first byte past it, so the events do not depend on how
  the input was split.
- The client refuses bytes that arrive after a complete response with no
  request outstanding (`UnexpectedData`) rather than treating them as the
  next response, and stops parsing interim 1xx responses once they exceed
  the header budget.
- Outgoing messages cannot contain CR/LF in fields or set framing headers.
- The blocking server cannot be pinned by slow peers: a request trickled
  inside the read timeout is cut off by the request deadline (408); a
  refused connection is answered without the accept thread ever waiting on
  the peer; lingering close stops after 1 s and 256 KiB.

---

## [0.1.0] - 2026-10-08

Initial scaffold and repository bootstrap. No domain logic yet; this release establishes the structure, tooling, and quality gates the implementation will be built on.

### Added

- `Cargo.toml` with crate metadata, Rust 2024 edition, MSRV 1.85.
- Dual `Apache-2.0 OR MIT` license files.
- `README.md`, `CHANGELOG.md`, and a documentation skeleton.
- `REPS.md` compliance baseline.
- `.github/workflows/ci.yml` CI matrix; `deny.toml`, `clippy.toml`, `rustfmt.toml`.
- `dev/DIRECTIVES.md` and `dev/ROADMAP.md` (committed engineering standards + plan).

[Unreleased]: https://github.com/jamesgober/http-net/compare/v0.9.2...HEAD
[0.9.2]: https://github.com/jamesgober/http-net/compare/v0.9.1...v0.9.2
[0.9.1]: https://github.com/jamesgober/http-net/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/jamesgober/http-net/compare/v0.8.0...v0.9.0
[0.8.0]: https://github.com/jamesgober/http-net/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/jamesgober/http-net/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/jamesgober/http-net/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/jamesgober/http-net/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/jamesgober/http-net/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/jamesgober/http-net/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/jamesgober/http-net/compare/1f3458c...v0.2.0
[0.1.0]: https://github.com/jamesgober/http-net/commit/1f3458c