http-net 0.2.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 dependencies.
Documentation
# http-net - Roadmap


> Path from scaffold to a stable 1.0. Hard parts are front-loaded; each phase has hard exit criteria.
> Master plan: C:/Dev/libraries/Rust/lang-collection/_lexersketch/ROADMAP.md and NEW-LIBS.md
>
> **Anti-deferral rule:** no listed hard task moves to a later phase unless this file records the move and the reason.

## v0.1.0 - Scaffold (DONE)

Compiles, CI green, structure correct, no domain logic.
- [x] Manifest, README, CHANGELOG, REPS, dual license, CI, deny, clippy, rustfmt, DIRECTIVES, ROADMAP.

## v0.2.0 - Foundation (DONE, 2026-10-12)

- [x] Sans-IO request/response parser and serializer, keep-alive, chunked bodies, server and client state machines, a blocking std adapter; smuggling and fuzz-style property tests.
- [x] Upgrade hand-off for ws-net (moved forward from v0.5.0: LexerSketch Studio's
  WebSocket needs it, and it is a small, self-contained state transition).
- [x] `Expect: 100-continue` on the server (moved forward from v0.5.0 during the
  adversarial review: RFC 9110 section 10.1.1 makes answering the expectation a
  MUST, and the blocking server, which reads the whole body before calling the
  handler, otherwise stalled every expecting client).
- [x] Adversarial review against RFC 9110 / 9112 (2026-10-12): findings fixed with
  a failing test first; see "Review fixes" below.

Delivered:
- `ServerConn` / `ServerEvent` and `ClientConn` / `ClientEvent`: sans-IO state
  machines over one bounded input buffer each (`feed` or `read_buf` + `commit`,
  `poll`, `feed_eof`); responses and requests serialized into a caller-owned
  `Vec<u8>`; streamed bodies through `send_body` / `end_body`.
- Strict RFC 9112 parsing: request line forms checked against the method,
  versions (1.2-1.9 processed as 1.1), field syntax, `Host` rules, and framing
  per section 6.3 (HEAD, 1xx, 204, 304, CONNECT 2xx, until-close responses).
- Smuggling defences: CL+TE, duplicate or list CL, any TE other than a single
  final `chunked`, TE in HTTP/1.0, obs-fold, bare CR, bare LF, whitespace
  before colon, control bytes, malformed chunk lines and extensions. After a
  parse error the connection parses nothing further.
- `Limits` (8 KiB line, 16 KiB headers, 100 fields, 1 MiB body by default),
  each checked before the bytes it governs are buffered; the input buffer is
  capped at `2 * (line + headers) + 16 KiB`; head scanning resumes where it
  stopped, so parsing is linear however the input is split.
- Keep-alive (HTTP/1.1 default, HTTP/1.0 opt-in), in-order serving of pipelined
  requests, early responses (close afterwards when body bytes are unread; a
  bodiless request stays keep-alive), HTTP/1.0 chunked fallback.
- Request targets: origin, absolute (`scheme://authority[path][?query]`, no
  userinfo, non-empty host; its authority is `host()` and `Host` is ignored per
  RFC 9112 section 3.2.2), authority (`CONNECT`), asterisk (`OPTIONS`).
- `Expect: 100-continue`: `RequestHead::expects_continue`,
  `ServerConn::send_continue`; the blocking server answers it itself.
- Cumulative budgets: chunk extensions per message and interim 1xx heads per
  response count against `max_header_bytes`; a read-until-close body is
  delivered up to `max_body` and refused past it.
- Upgrade hand-off: `RequestHead::is_upgrade`, `Response::switching_protocols`,
  `ServerEvent::Upgraded` + `remaining_input`; client side `Request::upgrade`,
  `ClientEvent::Upgraded`; CONNECT 2xx tunnels.
- `blocking`: bounded thread-per-connection `Server` (503 over the cap without
  the accept thread waiting on the peer, read/write timeouts, a per-request
  deadline answered with 408, automatic `100 Continue`, `date`, lingering close
  bounded to 1 s, 500 for streamed or invalid handler responses, upgrade hook,
  shutdown), `Connection` (with `with_request_timeout`), `Client` / `get`.
- Tests: RFC cases as data, smuggling suite, adversarial suite, property tests
  (round trip, split invariance, no panics, from the first byte and behind valid
  heads), a differential test against an independent reference parser, a
  100k-request scale test, localhost socket tests (including slow peers).
  Criterion benches.

Review fixes (adversarial review, 2026-10-12; each has a test that failed first):
- Blocking server: lingering close waited 250 ms *per read* with no total, and
  the 503 refusal ran it on the accept thread, so one refused client trickling
  bytes stalled every new connection (measured 2.8 s; unbounded in general).
  Now the refusal never waits on the peer and the drain stops after 1 s total.
- Blocking server: only a per-read timeout, so a client sending a byte every
  few seconds held a connection forever (slowloris). Added the per-request
  deadline (`ServerConfig::with_request_timeout`, 30 s) answered with 408.
- Blocking server: `Expect: 100-continue` was never answered (RFC 9110 MUST).
- Absolute-form targets: `host()` returned the `Host` field rather than the
  target's authority, `path()` returned the whole URI, and userinfo, an empty
  host, or no `//authority` at all were accepted.
- Chunk extensions were bounded per line but not per message.
- The client skipped interim 1xx responses without any budget.
- A response sent at `ServerEvent::Request` to a bodiless request closed the
  connection needlessly.
- A read-until-close body over `max_body` produced different events depending
  on how the input was split (found by the new behind-the-head property test).
- A handler returning a streamed or invalid `Response` left the client with a
  half-written response or none; it now gets 500.

Dependency wiring (recorded per the anti-deferral rule):
- **None wired.** No first-party `-lang` crate covers HTTP syntax; the crate is
  dependency-free (runtime and build). `criterion` and `proptest` are
  dev-dependencies only.
- **Fuzz target (DIRECTIVES section 3 item 6):** planned for v0.9.0 Hardening,
  as this roadmap already lists. In this release the property tests that feed
  arbitrary and dictionary-built bytes to both connections (whole, split, and
  with tiny limits) cover the same ground without nightly tooling.

Remaining in v0.5.0 (as originally planned): interim 1xx responses other than
`100 Continue` from the server (for example `103 Early Hints`); sending
`Expect: 100-continue` from the client and waiting for it; a client with
several outstanding pipelined requests (the server already serves pipelined
requests in order); an async adapter.

## v0.5.0 - Implementation

- [ ] Client-side pipelining (several outstanding requests), client-side Expect: 100-continue, server interim 1xx responses other than 100, an async adapter.
- [ ] Blocking client: a total deadline per response (today its timeouts apply to each connect, read, and write).
- [ ] Blocking server: a total deadline per response write (today the write timeout applies to each write, so a client that reads very slowly can hold a worker thread during a large response). Found in the 0.2.0 adversarial review.
- [ ] Interop: accept a status line without a reason phrase (`HTTP/1.1 200`) on the client side; check percent-escapes in `Host` and refuse an empty host (`:80`).

## v0.9.0 - Hardening

- [ ] Fuzzing (`cargo fuzz` targets for `ServerConn` and `ClientConn`); conformance tests.

## v1.0.0 - Stable

- [ ] Frozen after LexerSketch Studio and ls-publish use it (D18).