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 v0.7.0 - Hardening

**Date:** 2026-10-26
**Compare:** `v0.6.0...v0.7.0`

**Attacked from every side, and one gap closed.** v0.7.0 is the hardening
release. Every fuzz target ran for 24 CPU-hours (458 million inputs, no
finding), the blocking server ran an 8-hour soak (191.7 million exchanges,
memory flat after warm-up), and every threat in the specification was
checked against its attack test. That audit found one real gap, now fixed:
the client switched protocols on any `101` with an `Upgrade` field, even to
a protocol it had not offered. A new response-side differential against
httparse found no disagreement. `docs/SECURITY.md` and
`docs/PLATFORM-NOTES.md` are complete. There are still no dependencies and
no `unsafe` code.

## 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.6.0

### Security fix: a `101` must name a protocol the client offered

`ClientConn` now switches on a `101 Switching Protocols` only when every
protocol the response's `Upgrade` field names is one the request offered
with `Request::upgrade` (case-insensitively; a bare name matches a
versioned one either way). Anything else, `h2c` included, is
`UnexpectedUpgrade`. Before, any non-empty `Upgrade` was accepted, so a
back end answering a `websocket` request could take the connection over
for another protocol (threat T-21; RFC 9110 section 7.8). The offered
protocols are kept in a reused buffer only for upgrade requests; ordinary
traffic still allocates nothing.

### Every threat has its attack test

An audit compared each of the 42 threats in the specification's section 10
with the test code: the cited tests exist, the counts are right, and they
attack what the threat describes. It found the T-21 gap above and 18
missing attack tests, now written:

- non-101 answers and protocol-less `101`s to an upgrade request, and a
  server `101` sent before the request body is read;
- HTTP/0.9 responses to the client;
- a chunk line over the line budget (whole, at every split, exactly at the
  budget, and as a 10 MiB stream);
- 100,000 pipelined requests offered with no answer: everything taken stays
  within the buffer capacity;
- the lingering close's byte cap against a client writing 8 MiB unpaced;
- a slow body answered `408`;
- wiping on `reset`;
- injection through request targets, methods, and field values, and
  reserved request fields in every spelling;
- `Transfer-Encoding` split across two fields;
- redaction of every owned and stateless view;
- `from_error` for every error variant, and error answers never reflecting
  the refused bytes;
- client trailers that cannot reframe the next pipelined response;
- the accept loop's back-off on persistent accept errors (T-41).

### The fuzz campaign

Each of the eight targets ran one libFuzzer process for 24 hours with
AddressSanitizer: 458 million executions in total, no crash, no sanitizer
report, no oracle failure. Per target:

| Target | Executions |
|---|---:|
| `server_conn` | 87.9 M |
| `client_conn` | 50.2 M |
| `chunk_line` | 17.2 M |
| `reference_diff` | 43.0 M |
| `httparse_diff` | 80.2 M |
| `builders` | 46.2 M |
| `stateless_parse` | 98.2 M |
| `chunked_decoder` | 35.3 M |

It ran on Windows x86_64 with the MSVC sanitizer runtime, on a machine
shared with other builds; three inputs libFuzzer flagged as slow re-run in
16 to 92 ms (scheduling stalls, not slow paths).

### The soak

`examples/soak.rs` (new) runs the blocking server with client threads
exchanging a mixed workload: keep-alive runs with chunked uploads and
96 KiB vectored bodies, pipelined bursts, one-shot requests, and malformed
requests. Over 8 hours: 191.7 million exchanges, 467,445 malformed
requests answered `400`, memory flat after a 12-minute warm-up (private
bytes +3.4% from one hour to eight, under the 5% bound). The five failures
were on the client side (three were Windows refusing a `connect` while
ports sat in TIME_WAIT); none was a server error.

### The response-side differential

`tests/prop_httparse_response.rs` compares the client's response parsing
with httparse on twelve seed responses, 47,249 single-byte mutations, and
24,576 property cases. On every head both accept, they agree on the
length, status, version, reason, and fields; where they differ, http-net
refused (bare LF, invalid `Content-Length` or `Transfer-Encoding`, a status
outside 100 to 599) or read HTTP/1.2 to 1.9 as 1.1. This was 0.7's defence
idea; it held.

### Documentation

- `docs/SECURITY.md`: complete, with the hardening record (audit, campaign,
  soak) and the response-side comparison.
- `docs/PLATFORM-NOTES.md` (new): timeout error kinds per OS, the lingering
  close and why it exists, ports and TIME_WAIT, measuring memory without
  `unsafe`, `no_std` targets, fuzzing on Linux and Windows.

### Not in 0.7

HTTP Garden runs need Docker, which the development machine does not have;
they move to a manually triggered CI job (`dev/PLAN.md` D-58). http-net
itself never uses Docker.

## Testing

354 tests plus 345 doctests, all passing (`cargo test --all-features`):
109 unit tests and 245 integration tests in 20 suites.

## Performance

No hot path changed except the `101` check, which runs only on an upgrade
response. The 0.6 numbers in `docs/PERFORMANCE.md` stand.

## Migration from v0.6.0

See `docs/MIGRATION.md`. If you upgrade with `ClientConn`, a server must
answer with a protocol you offered; one answering with another spelling of
the same protocol needs that spelling offered. Nothing else changes.

## Stability commitment

Pre-1.0: the API may still change in the 0.x series, and every change is
listed in `CHANGELOG.md` and `docs/MIGRATION.md`. The API is frozen at
1.0.0 until 2.0. 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 +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.8.0, alpha:** the first consumers (serve-net's engine adapter, the
  WASP gateway, fetch-net, LexerSketch Studio) run end to end against
  http-net, with every gap they report closed; HTTP Garden runs in CI.

## Installation

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

MSRV: Rust 1.85.

**Full Changelog:** https://github.com/jamesgober/http-net/compare/v0.6.0...v0.7.0