http-net 0.10.0

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 v0.9.0 - Beta

**Date:** 2026-10-29
**Compare:** `v0.8.0...v0.9.0`

**The API is frozen as the 1.0 candidate.** From this release, `0.9.x`
releases contain fixes only, and 1.0.0 follows once two weeks pass with no
API change and the 1.0 audit is complete. The one API change at the
freeze: `blocking::Upgraded` is `#[non_exhaustive]`. A line-by-line audit
of the documentation against the build found and fixed four defects,
two of them `Debug` outputs that printed credentials. The parity matrix
against hyper, httparse, h11, llhttp, picohttpparser, and the `http` crate
is checked cell by cell, and HTTP Garden runs as a manual CI job; its
first run found no defect.

## What is http-net?

An HTTP/1.1 engine that does no I/O. Feed it the bytes a socket delivered;
it hands back requests or responses as borrowed views of those bytes,
decodes their bodies, and serializes what you send back. Parsing is strict:
a message two implementations could frame differently is refused with a
specific error, never repaired. Every buffer is bounded by configuration,
and each limit is checked before the bytes it governs are buffered.

## What changed since v0.8.0

### The API freeze

`docs/STABILITY-1.0.md` sets out what 1.0 commits to: what a patch, a
minor, and a major release may change, which types may grow in 1.x, the
behaviour guarantees (strict framing, bounded memory, no panics on input,
no `unsafe`), the dependency and MSRV policy, and the audit 1.0.0 is
released on. `cargo semver-checks` against 0.8.0 reports one change, the
intended one:

- `blocking::Upgraded` is `#[non_exhaustive]`. The upgrade hook receives
  it and never builds it, so a 1.x release can add a field. Its fields are
  unchanged; a pattern that names every field needs `..`
  (`docs/MIGRATION.md`).

`Asked`, `ResponseHeadRef`, `Span`, and `FieldSpan` stay exactly as they
are: callers build them with struct literals.

### Fixed

- **Credentials in `Debug` (T-35).** `ResponseHeadRef` derived `Debug` and
  printed every field value, and `blocking::Connection` printed the last
  request body and the last serialized response. `ResponseHeadRef` now
  redacts `authorization`, `proxy-authorization`, `cookie`, and
  `set-cookie` like every other type, and `Connection` prints its buffers
  as lengths. Both are covered by `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, such as a `GET` answered at `ServerEvent::Request`. A driver that
  closes on `Closed` dropped a healthy connection. It now reports `Idle`.
- **Request deadline on Unix.** `blocking::Connection` reported a missed
  request deadline as `WouldBlock` on Unix; it is `TimedOut` on every
  platform, as documented.
- **`Display` of `InterimNotAllowed`** contained a run of spaces.

### Documentation

Every factual claim in the README, `docs/`, the changelog, and the rustdoc
was checked against the build. Besides the four defects above, the
corrections include the `Transfer-Encoding` rule in the README (a coding
before `chunked` is 501; anything else that does not end in a single
`chunked` is 400), the buffer bound (`head + max(head, max_trailer_bytes)
+ 16 KiB`), the sizes of `HttpError` and `RequestHead`, pipelining in the
`ClientConn` docs, the `ChunkedDecoder` error list, and the
`OwnedRequestHead::from_parts` check (structural, not an integrity check:
a passport that crosses a trust boundary needs its own MAC).

### Parity

`docs/PARITY.md` is the feature matrix for the 1.0 candidate. Every cell
was checked: hyper 1.12.0 and httparse 1.10.1 by running them on the attack
inputs, llhttp 9.3.0 through Node.js's own parser, h11 0.16.0,
picohttpparser, and the `http` crate 1.5.0 from source. Every claim in the
http-net column holds. Among the findings: hyper lets `Transfer-Encoding`
win over `Content-Length`, collapses duplicate equal lengths, sends the
body at once despite `Expect: 100-continue`, and accepts a `101` to a
protocol the request did not offer; h11 accepts bare LF and obs-fold.

### HTTP Garden

`.github/workflows/garden.yml` runs the HTTP Garden on a GitHub runner,
on demand: http-net as an origin (`garden/`, not part of the package)
compared with hyper, nginx, h2o, and gunicorn, directly and through
haproxy.

The first run, on the merged 0.9.0 code, sent 133 seeds directly, 47
through haproxy, and 499 seeded mutations. http-net's result differs from
at least one other server's in 156 cases, and none is a defect:

- 102 are refusals another server does not make, each a documented choice
  (bare LF, `Content-Length` with `Transfer-Encoding`, duplicate lengths,
  a space after a chunk size, two `Host` fields, and the like).
- 50 are inputs http-net accepts and another server refuses, each one the
  RFCs allow: empty lines before the request line, a lowercase method, a
  trailer section, `Transfer-Encoding: ,chunked`, `HTTP/1.2` read as 1.1,
  an empty `Host`. hyper accepts all of them except `HTTP/1.2`.
- 4 are requests every server accepts whose echo differs, such as an
  absolute-form target that nginx and h2o rewrite.

`docs/SECURITY.md` has the full triage. The first dispatch of the job
stopped inside the Garden's own mutator, before any server saw the input
(a mutant's chunk length overflowed its regular expression); the job now
skips such mutants and counts them. That fix is in `garden/` only and
changes nothing in the crate.

## Testing

365 tests plus 346 doctests, all passing (`cargo test --all-features`):
116 unit tests and 249 integration tests (4 new: the `Debug` of
`ResponseHeadRef` and `blocking::Connection`, `progress` after an answer
at `ServerEvent::Request`, and the request deadline's error kind). The
Garden origin has its own tests (`garden/tests/echo.rs`).

## Migration from v0.8.0

Only for code that destructures `blocking::Upgraded` naming every field:
add `..`. See `docs/MIGRATION.md`.

## Stability commitment

The API is frozen as the 1.0 candidate: `0.9.x` releases contain fixes
only, and `docs/STABILITY-1.0.md` describes what 1.0 commits to. 1.0.0
ships after the full audit, at the earliest two weeks after this release
with no API change in between. The MSRV is Rust 1.85; raising it after 1.0
is a major change.

## Verification

Run on Windows x86_64 and in WSL Ubuntu (Linux x86_64), each on Rust
stable and the 1.85 MSRV toolchain:

```bash
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --all-targets --no-default-features -- -D warnings
cargo test
cargo test --all-features
cargo test --no-default-features
cargo build --examples
cargo build --lib --no-default-features --target thumbv7em-none-eabihf
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --no-default-features
cargo package
cargo deny check
cargo audit
cargo semver-checks --baseline-version 0.8.0 --all-features   # one intended change
cargo +nightly fuzz run --target x86_64-unknown-linux-gnu <target> <corpus outside the repo> -- -max_total_time=60   # each of the eight targets, WSL
```

CI runs the same checks on Linux, macOS, and Windows, stable and 1.85,
plus the `no_std` build and the 60 s fuzz smoke run per target.

## What's next

- **0.9.x:** fixes only; another HTTP Garden run before 1.0.
- **1.0.0:** after the full audit, on James's decision.

## Installation

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

MSRV: Rust 1.85.

**Full Changelog:** https://github.com/jamesgober/http-net/compare/v0.8.0...v0.9.0