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
# http-net - Stability (1.0)

> What http-net 1.0.0 commits to, what a 1.x release may change, what only a
> 2.0 may change, and the audit that 1.0.0 is released on. **Draft (0.9.2):**
> the API is frozen as the 1.0 candidate; the audit record at the end is
> filled in as each part is done, and 1.0.0 ships only when every part is
> recorded and James approves the release.

## What 1.0 means here

From 1.0.0, every public item in [`docs/API.md`](API.md), apart from those
under "Planned after 1.0", is under semantic versioning:

- **Patch (`1.0.x`):** fixes only. No API change. A fix may refuse a message
  that was accepted before when accepting it is a security defect (a message
  two implementations could frame differently, a limit that was not
  enforced); every such change is listed under **Security** in
  `CHANGELOG.md`.
- **Minor (`1.x.0`):** additions only: new functions, methods, types,
  `HttpError` variants, `Limits` settings, enum variants of the
  `#[non_exhaustive]` enums, and the items `docs/API.md` lists as "after
  1.0". Existing items keep their signatures, their defaults, and their
  behaviour.
- **Major (`2.0.0`):** anything else: a removed or renamed item, a changed
  signature, a changed default limit, a message accepted in 1.x that a
  2.0 refuses for a reason other than security, a raised MSRV.

A deprecated item stays for the whole 1.x series and names its replacement.

## The frozen surface

The complete list, with signatures, errors, and examples, is
[`docs/API.md`](API.md). In summary:

| Area | Items |
|---|---|
| Connections | `ServerConn`, `ServerEvent`, `Progress`; `ClientConn`, `ClientEvent`, `ClientLeniency` |
| Views | `RequestHead`, `ResponseHead`, `Headers`, `HeaderValues`, `ListElements`, `Version`, `BodyFraming`, `TargetForm`, `OwnedRequestHead` |
| Builders | `Request`, `Response` |
| Allocation-free tier | `RawHead`, `Span`, `FieldSpan`, `HeadFacts`, `parse_request_head`, `parse_response_head`, `Asked`, `Parsed`, `ParsedRequest`, `ParsedResponse`, `ChunkedDecoder`, `ChunkStep`, `Output`, `ResponseHeadRef`, `OutgoingBody`, `BodyPlan` |
| Vocabulary | `Method`, `MethodSet`, `status` (constants, `StatusClass`, `reason_phrase`, `status_class`, `status_allows_body`), `HttpDate`, `DateCache`, `is_hop_by_hop`, `is_h2_preface`, `VERSION` |
| Configuration and errors | `Limits` (every setting and its default), `HttpError` (every variant, `is_peer_error`, `is_usage_error`, `status`) |
| `blocking` (feature `std`) | `Server`, `ServerConfig`, `ShutdownHandle`, `Incoming`, `Connection`, `Upgraded`, `Client`, `ClientResponse`, `Error`, `get`, `post`, `serve`, `now_date` |

Which types may grow in 1.x is decided at the freeze (`dev/PLAN.md` D-63):

- Every public enum is `#[non_exhaustive]`: a match needs a wildcard arm,
  and a new variant is a minor release.
- `ClientLeniency` and `blocking::Upgraded` are `#[non_exhaustive]` structs:
  a new field is a minor release.
- `Asked`, `ResponseHeadRef`, `Span`, and `FieldSpan` are built by callers
  with struct literals and stay exactly as they are for all of 1.x.
- Every other public struct has private fields; new methods are a minor
  release.

## Behaviour guarantees

- **Strict framing.** A message that two conforming implementations could
  frame differently is refused with a specific `HttpError`, never repaired.
  The table of such refusals is in [`docs/SECURITY.md`]SECURITY.md. A 1.x
  release never starts accepting one of them.
- **Bounded memory.** Every buffer is bounded by a `Limits` setting, checked
  before the bytes it governs are buffered. Defaults change only in 2.0.
- **No panics on input.** No byte sequence from a peer makes the crate
  panic; refused input is an `Err`. Checked by eight fuzz targets (24
  CPU-hours each in 0.7, 60 seconds each on every CI run).
- **No `unsafe` code** (`#![forbid(unsafe_code)]`) in the crate, for all of
  1.x.
- **Allocation.** The core allocates nothing per request after warm-up on a
  keep-alive connection, and the allocation-free tier allocates nothing of
  its own: a caller's reused buffer grows only until it fits. Both are proved by counting-allocator tests (`tests/alloc.rs`).
- **Output bytes.** The bytes the crate writes for a given response or
  request are stable within a minor series; a minor release may change
  them only in ways RFC 9112 makes equivalent (field order of the crate's
  own fields, for example), and says so in `CHANGELOG.md`.

## Dependencies and platforms

- **Runtime dependencies:** the first-party crate `parse-net` only, by
  published version, with no features. A third-party runtime dependency is
  a 2.0 change, except the optional `http-types` feature planned after 1.0,
  which is never a default.
- **`no_std`:** the crate builds without `std` (it needs `alloc`), checked
  in CI on `thumbv7em-none-eabihf`. The `blocking` module needs `std`.
- **Platforms:** tested in CI on Linux x86_64, Windows x86_64, and macOS
  (the runners' aarch64), stable and the MSRV. The core is portable Rust
  with no platform code; only `blocking` touches the operating system,
  through `std::net`.

## MSRV policy

The minimum supported Rust version is 1.85. It is tested in CI on every
platform. Raising it is a 2.0 change.

## Support

**Open, for James's decision at 1.0:** how long a 1.x minor release
receives security fixes after its successor ships, and the private channel
for reporting a security problem (for example GitHub's private
vulnerability reporting on the repository). Until then, fixes go to the
latest release only.

## How this contract is checked

- `cargo semver-checks` against the previous release before every release
  from 0.9.1 on; a patch release must report no semver change.
- CI builds and tests every feature mode on Rust stable and 1.85 on Linux,
  macOS, and Windows, and the `no_std` target.
- `docs/API.md` is checked against the build before every release.

## The 1.0 audit

Each part records what was checked, how, and the result. 1.0.0 ships when
every part says **done**, the 0.9 exit criteria hold (two consecutive weeks
without an API change after 0.9.0, D-66; the full documentation set; local
gate and CI green on every platform and the MSRV), and James approves.

### API

- Frozen at 0.9.0 (D-63). One change at the freeze: `blocking::Upgraded`
  became `#[non_exhaustive]`.
- Every public item has rustdoc with an example, compiled, and run where
  it needs no server, and an entry in `docs/API.md`.
- `cargo semver-checks` to 0.9.1 and to 0.9.2: no change (196 checks).
  Run it with a toolchain whose rustdoc format it reads (Rust 1.95 for
  cargo-semver-checks 0.47).
- **Status:** freeze done; two-week clock running from the 0.9.0 release.

### Documentation

- Every factual claim in the README, `docs/`, the changelog, and the
  rustdoc was re-checked against the build for 0.9.0. The audit found four
  code defects (two `Debug` outputs that printed credentials, a wrong
  `progress()` value, a platform-dependent timeout kind, a `Display` text),
  fixed in 0.9.0 with regression tests, and corrected the documentation
  everywhere else (`dev/PLAN.md` D-69).
- Repeated on 0.9.1, with the same method (D-75): three code defects
  (`outstanding()` after a failed response, the reuse queries at
  `ResponseEnd`, and `HEAD` errors in the head), fixed in 0.9.2 with
  tests, and 18 documentation corrections.
- **Status:** done for 0.9.2. Any code change before 1.0 repeats it for
  what changed.

### Tests

- Unit, integration, property, differential (an independent reference
  parser, httparse on requests and responses), counting-allocator, and
  socket tests; every threat T-1 to T-42 has an attack test
  (`docs/SECURITY.md`).
- The 0.9.x reviews add a regression test for each fix except the wait
  in `serve` after persistent accept errors, which needs a failing
  listener; a unit test that tells the lingering-close byte cap from its
  time cap; and `tests/prop_split.rs` (split invariance where the default
  drivers do not reach, and every budget exact at every split).
- **Status:** green at 0.9.2 (386 tests and 346 doctests).

### Benchmarks

- Measured numbers, method, and hardware in
  [`docs/PERFORMANCE.md`]PERFORMANCE.md (0.6.0); the comparison
  repository is [`http-net-bench`]https://github.com/jamesgober/http-net-bench.
  0.7 and 0.8 showed no measurable change against 0.6 (`dev/PLAN.md` D-61).
- Head parsing is at parity with httparse, not 20% ahead: the shortfall and
  its cause are published (D-54).
- The 0.9.x pass (D-72): no regression against 0.6.0, 0.7.0, or 0.8.0
  (interleaved, nine rounds); the competitor ratios of 0.6 hold on 0.9.0;
  the unmet targets decomposed in `docs/PERFORMANCE.md` (the response-write
  benches include reading the request; `write_head` validates each
  borrowed field per call).
- **Status:** done for the frozen code; the H1 comparison is rerun on a
  quiet machine before 1.0 (the 0.9.x run shared the machine with
  fuzzers, so only its ratios are used).

### Security

- Fuzz campaign (24 CPU-hours per target, no finding), 8-hour soak (+3.4%
  memory after warm-up), threat audit (T-21 fixed), response-side
  differential: `docs/SECURITY.md`, hardening record.
- HTTP Garden: a manually triggered job (`.github/workflows/garden.yml`,
  D-68) compares http-net with hyper, nginx, h2o, and gunicorn, directly
  and through haproxy. The first run (2026-10-29, 679 cases) found no
  defect: each of the 156 differences is a documented refusal, an input
  the RFCs allow, or a limit of the harness (`docs/SECURITY.md`, D-70).
- A second, independent review of the frozen code (0.9.x polish pass,
  D-73) found no smuggling, desync, panic, or memory defect; it found an
  error answer to `HEAD` with content, pipelining after a response that
  closes, and body bytes in `Debug`, all fixed in 0.9.1 with tests.
  Every fuzz target ran again for 15 minutes on the 0.9.1 code with no
  finding in the crate (one overflow in a test oracle, fixed, D-74), and
  the HTTP Garden ran again with the same result as its first run.
- **Status:** done.

### Parity with other implementations

- Every cell compared with hyper, httparse, h11, llhttp, picohttpparser,
  and the `http` crate, by running them where possible and from source
  otherwise: [`docs/PARITY.md`]PARITY.md (D-67).
- **Status:** done.

### Consumers

- serve-net and LexerSketch Studio run end to end against 0.8 with no
  change (D-62), and again against the 0.9.2 code: `ls-studio`'s 42 tests
  and serve-net's whole suite pass with no source change (the crate moved
  to 0.9 on its own). The WASP gateway skeleton and the first fetch-net do not
  exist yet. The specification also asks for LexerSketch Studio's publish
  step to use the API end to end before 1.0.
- **Status:** open.

## Open before 1.0

What is still open after the 0.9.x polish pass, and who decides it.
Everything else in the audit above is done.

**For James:**

- The security-fix support window for 1.x minors and the private channel
  for reporting a vulnerability ([Support]#support); a root
  `SECURITY.md` follows from the channel.
- The third consumer (D-62): serve-net and LexerSketch Studio run end to
  end; the WASP gateway and the first fetch-net do not exist yet. Wait for
  one, or ship 1.0 on two.
- Whether LexerSketch Studio's publish step must use the API end to end
  before 1.0, as the specification asks.
- The 1.0 go or no-go itself, no earlier than 2026-11-12 (two weeks after
  0.9.0 with no API change, D-66).

**Engineering, before the 1.0 tag** (no API effect):

- An H1 competitor run on a quiet machine (D-72).
- H6 (plaintext over sockets, through serve-net) and the report-only C
  harnesses (picohttpparser, llhttp) in `http-net-bench`; the
  specification lists them, none is run yet.

**Targets not met, with the decision on record** (`docs/PERFORMANCE.md`):
the latency targets for the browser GET, pipelined requests, the 1 MiB
bodies, the response write, `write_head`, and the client parse; and 20%
ahead of httparse on head parsing (D-54: parity, kept safe).