# Kawa H1 — Session Workflow and Per-Stream Lifecycle
Reference document for maintainers of the HTTP/1.1 frontend / backend pairing
under `lib/src/protocol/kawa_h1/`. Companion to `lib/src/protocol/mux/LIFECYCLE.md`
(H2) and `lib/src/protocol/proxy_protocol/LIFECYCLE.md` (PROXY-v2 ingress).
Every claim is anchored to a concrete `file.rs:LINE`. Line numbers were last
refreshed against the `docs/feat-h2-mux-audit` branch tip on 2026-04-26 — when
a refactor moves them, please update the citations in the same changeset; stale
pointers here are treated as broken documentation.
Scope: the server-side Kawa H1 session is the primary subject. Where the
behaviour diverges between an H1 frontend and the H1 backend connection wired
underneath an H2 frontend (via the mux), the difference is called out.
---
## 1. Architecture Overview
### 1.1 Where Kawa H1 sits
Kawa H1 owns the HTTP/1.1 wire-level state machine plus the request/response
mutation pipeline. It is consumed by:
- the standalone H1 frontend (`lib/src/http.rs`, `lib/src/https.rs` after TLS
handshake completes with ALPN `http/1.1` or no ALPN);
- the H2 mux backend path (`lib/src/protocol/mux/h1.rs`), which speaks H1 to
origin servers regardless of the frontend protocol — Sōzu currently does
not speak H2 to backends.
### 1.2 Module layout
| `mod.rs` | `lib/src/protocol/kawa_h1/mod.rs` | `Http<Front, L>` session state + `SessionState` impl + readiness pumping |
| `editor.rs` | `lib/src/protocol/kawa_h1/editor.rs` | `HttpContext`, header rewrites (`Forwarded`, `X-Forwarded-*`, `Sozu-Id`), `log_context()` |
| `parser.rs` | `lib/src/protocol/kawa_h1/parser.rs` | `Method` enum, hostname/port helper, tolerant-vs-strict charset split |
| `answers.rs` | `lib/src/protocol/kawa_h1/answers.rs` | `Template`, `HttpAnswers`, `DefaultAnswerStream` for synthesised 4xx/5xx replies |
| `diagnostics.rs` | `lib/src/protocol/kawa_h1/diagnostics.rs` | Hex-dump + phase rendering for parse-failure access logs |
### 1.3 Key types
| `Http<Front, L>` | `lib/src/protocol/kawa_h1/mod.rs:189` | Top-level H1 session state |
| `DefaultAnswer` | `lib/src/protocol/kawa_h1/mod.rs:111` | Catalogue of synthesised replies (301/400/404/413/502/503/504/507) |
| `ResponseStream` | `lib/src/protocol/kawa_h1/mod.rs:183` | `BackendAnswer(Kawa<…>)` vs `DefaultAnswer(…)` split |
| `TimeoutStatus` | `lib/src/protocol/kawa_h1/mod.rs:176` | Used by the supervisor to attribute idle vs response timeouts |
| `HttpContext` | `lib/src/protocol/kawa_h1/editor.rs:116` | Per-request mutable state used by Kawa parser callbacks |
| `Method` | `lib/src/protocol/kawa_h1/parser.rs:39` | Owned-string-free method enum |
| `HttpAnswers` | `lib/src/protocol/kawa_h1/answers.rs:339` | Listener + cluster template registry |
| `DefaultAnswerStream` | `lib/src/protocol/kawa_h1/answers.rs:36` | `Kawa<SharedBuffer>` carrying a rendered default answer |
---
## 2. Per-Stream Lifecycle
### 2.1 Accept and session creation
A new H1 session is constructed by the proxy layer (`lib/src/http.rs`,
`lib/src/https.rs`) once a TCP/TLS connection has produced a Kawa pair of
front/back buffers and a `HttpContext`. `Http::new` (`lib/src/protocol/kawa_h1/mod.rs:228`)
seeds the session with:
- `frontend_readiness.interest = READABLE | HUP | ERROR`;
- `backend_readiness.interest = HUP | ERROR` (no backend yet);
- `request_stream` / `response_stream` Kawa parsers in their initial phase;
- the `container_frontend_timeout` driving idle disconnect.
`reset()` (`lib/src/protocol/kawa_h1/mod.rs:299`) is the keep-alive entry
point — it reuses the same `Http` instance across pipelined requests, clearing
the per-request fields while preserving connection-level state.
### 2.2 Request parsing
`Http::readable` (`lib/src/protocol/kawa_h1/mod.rs:355`) is invoked when the
event loop sees the frontend socket as readable. It:
1. Resets the frontend timeout (`mod.rs:357`); failure to reset is logged but
not fatal.
2. Refuses to read while a `DefaultAnswer` is already queued
(`mod.rs:367-376`) — the only legal next step is `writable` to flush the
synthesised reply.
3. Detects a full request buffer and either pushes the parser forward (if we
are mid-body) or escalates to a `DefaultAnswer::Answer413`
(`mod.rs:379-392`).
4. Calls `socket_read` and feeds the bytes to the Kawa request parser.
When the request transitions out of the parsing phase, the parser callbacks in
`HttpContext` (see §3) capture the `:method`, authority, path, and rewrite the
`Forwarded` / `X-Forwarded-*` header chain.
### 2.3 Routing — `cluster_id_from_request`
`cluster_id_from_request` (`lib/src/protocol/kawa_h1/mod.rs:1340`) extracts the
authority and path from the Kawa stream and calls back into the listener
(`L7ListenerHandler`) to resolve the request to a `cluster_id`. It is also the
gate that:
- enforces the served-cert-SAN-vs-`:authority` binding when `tls_server_name`
is present (`HttpContext::strict_sni_binding`, see `editor.rs:189`): the
routing layer matches the request authority against `HttpContext::tls_cert_names`
with RFC 6125 §6.4.3 wildcard handling, accepting H2/H1 connection coalescing
per RFC 7540 §9.1.1 / RFC 9113 §9.1.1 (the same behaviour Firefox / Chrome
implement). Misses yield a **421 Misdirected Request** default answer
(RFC 9110 §15.5.20) rather than a backend connection (CWE-346 / CWE-444);
- turns route-not-found into `DefaultAnswer::Answer404`.
### 2.4 Backend connect — `connect_to_backend`
`connect_to_backend` (`lib/src/protocol/kawa_h1/mod.rs:1462`) reuses the
existing backend when the cluster has not changed and the TCP connection is
healthy (`mod.rs:1485-1500`); otherwise it allocates a fresh socket via
`backend_from_request` (`mod.rs:1397`). The resulting `BackendConnectAction`
is bubbled back to the supervisor so it can register the new socket with mio.
### 2.5 Response streaming and keep-alive
After the backend handshake completes, `Http::backend_writable`
(`mod.rs:701`) flushes the request body and `Http::backend_readable`
(`mod.rs:773`) ingests the response. Once `response_stream.parsing_phase`
reaches the terminal phase and the body is fully consumed:
- if both sides set `Connection: keep-alive` (`HttpContext.keep_alive_frontend`
+ `keep_alive_backend`), `Http::reset` (`mod.rs:299`) is called and the
session waits for the next request on the same TCP/TLS connection;
- otherwise the session shuts down (see §6).
---
## 3. Editor — `HttpContext` and the parser callbacks
`HttpContext` (`lib/src/protocol/kawa_h1/editor.rs:116`) is the per-request
mutable companion to the Kawa parser. Its `kawa::h1::ParserCallbacks` impl
(`editor.rs:217`) fires:
- `on_headers` (`editor.rs:218`) — split between request and response by
`stream.kind`;
- `on_request_headers` (`editor.rs:287`) — captures the `:method`, authority,
path; copies `X-Forwarded-For` into `xff_chain` for the access log; appends
the configured `Forwarded`/`X-Forwarded-*` hop; injects the `Sozu-Id`
correlation header named by `sozu_id_header`;
- `on_response_headers` (`editor.rs:574`) — captures `:status`, `:reason`,
optionally rewrites `Set-Cookie` for sticky sessions.
`HttpContext::log_context` (`editor.rs:687`) is the canonical helper for
producing the `LogContext { session_id, request_id, cluster_id, backend_id }`
record consumed by every `log_context!` macro in this module — prefer it over
hand-rolling a struct literal (per repo `CLAUDE.md`).
Notable security-relevant fields on `HttpContext`:
- `tls_server_name` (`editor.rs:188`) — SNI captured at handshake (lowercased,
trailing dot stripped). Used for logging and as a fallback exact-match check
when `tls_cert_names` is unavailable.
- `tls_cert_names` (`editor.rs:200`) — `Option<Arc<Vec<String>>>` snapshot of
the SAN dNSName entries of the certificate Sōzu actually served on this TLS
session (RFC 6125 §6.4.4: when the SAN extension contains at least one
dNSName entry, those entries are authoritative and the Common Name is
ignored — Sōzu only honours CN as a fallback when SAN is absent or has no
dNSName entry). Captured once at handshake from the cert resolver, frozen
for the connection lifetime, `Arc`-shared across every per-stream
`HttpContext` so H2 fan-out allocates once. The routing layer matches
`:authority` against this set with RFC 6125 §6.4.3 wildcard handling,
accepting browser connection coalescing while preserving the
CWE-346 / CWE-444 trust boundary (operator-defined SAN scope). `None` when
the resolver fell back to the default cert — routing then fall-backs to
legacy SNI exact-match.
- `strict_sni_binding` (`editor.rs:195`) — mirrors
`HttpsListenerConfig::strict_sni_binding`; gates the `tls_cert_names` check
on/off. Defends against cross-tenant authority spoofing (CWE-346 / CWE-444).
- `xff_chain` (`editor.rs:147`) — verbatim upstream `X-Forwarded-For` snapshot
taken before Sōzu appends its own hop, so the access log records the
attested chain even when Sōzu mutates the live header.
- `x_request_id` (`editor.rs:141`) — universal correlation token; populated
unconditionally in `on_request_headers` so the access log always has a
cross-component join key.
---
## 4. Default Answers
Synthesised 4xx/5xx responses are not handcrafted byte buffers — they are
template-rendered Kawa streams. The relevant pieces:
- `DefaultAnswer` enum (`lib/src/protocol/kawa_h1/mod.rs:111`) lists every
variant Sōzu can emit (301 redirect, 400, 404, 413, 502, 503, 504, 507) and
carries the per-call diagnostic strings.
- `Template` (`answers.rs:78`), `Replacement` (`answers.rs:72`), and
`TemplateVariable` (`answers.rs:57`) drive the substitution engine:
variables can be plain (text) or typed (URL/path/integer) — typed
substitutions go through the corresponding sanitizer to avoid log /
header injection.
- `HttpAnswers` (`answers.rs:339`) holds the per-listener registry; the
ListenerAnswers / ClusterAnswers split (`answers.rs:307`/`:334`) allows a
cluster to override a listener-level template.
- `Http::set_answer` (`mod.rs:1055`) is the one chokepoint that promotes a
`ResponseStream::BackendAnswer` to `ResponseStream::DefaultAnswer` and
arms the readiness flags so `writable` flushes the synthesised reply
next.
Status mapping `DefaultAnswer → u16` lives at `mod.rs:157`.
---
## 5. Diagnostics and Error Envelope
`diagnostics::diagnostic_400_502` (`lib/src/protocol/kawa_h1/diagnostics.rs:49`)
and `diagnostic_413_507` (`diagnostics.rs:95`) render the parsing phase, the
charset rule (`diagnostics.rs:14-17`, switches on the `tolerant-http1-parser`
feature), and a hex-dump window into the offending buffer region. Their output
is what populates the `message` field on `DefaultAnswer::Answer400` and is
echoed to the access log so a parse failure is debuggable without a packet
capture.
`Http::log_request` (`mod.rs:992`) is the canonical access-log emitter; the
`log_request_success` / `log_default_answer_success` / `log_request_error`
shortcuts (`mod.rs:1036`/`:1041`/`:1044`) categorise the call site.
---
## 6. Invariants
These rules are load-bearing — break them and you risk a truncated response,
a wedged session, or a security regression.
1. **Never panic on network-facing input.** Parser failures, oversized bodies,
bad TLS handshake state, lost backends — all funnel into `DefaultAnswer` +
metric + log. `unwrap`/`expect`/`panic!` are reserved for hard internal
invariants. Per repo `CLAUDE.md`, the H1 parser, editor, default-answer,
and diagnostics paths are explicitly listed under
"no panic on network-facing input".
2. **Write-only shutdown on TLS frontends.** Closing the frontend socket with
`Shutdown::Both` discards any unread receive data and elicits a TCP RST,
truncating the already-queued response. The canonical write-up lives at
`lib/src/https.rs:650-655`. Backends speak plaintext H1 today, so
`Shutdown::Both` is permitted on the backend socket — but the
`// SAFETY (TLS-truncation invariant)` comment at
`lib/src/protocol/kawa_h1/mod.rs:1219-1225` flags the migration to
`Shutdown::Write` when backend TLS lands.
3. **Readiness lifecycle is owned by the editor, not the supervisor.** The
per-stream readiness pumping (`frontend_readiness.interest.insert(WRITABLE)`,
`backend_readiness.interest.insert(READABLE)`, etc.) is driven from
`Http::readable` / `writable` / `backend_readable` / `backend_writable`
(`mod.rs:355` / `:520` / `:773` / `:701`). The mux-style
`signal_pending_write` discipline does NOT apply to Kawa H1 — H1 sessions
own a single in-flight request at a time, and the readable/writable
methods toggle interest directly under the mio level-triggered semantics
the H1 listener uses.
4. **Default answer is one-shot per session.** Once `set_answer` promotes
`response_stream` to `ResponseStream::DefaultAnswer`, `readable` refuses to
accept further bytes (`mod.rs:367-376`). The next legal transition is
`writable` followed by either `reset` (keep-alive) or close.
5. **`tolerant-http1-parser` is opt-in.** The strict parser is the default;
the tolerant variant is enabled only via the `tolerant-http1-parser`
feature on `sozu-lib` and `sozu-bin` (`lib/Cargo.toml:86`,
`bin/Cargo.toml`). Tolerant mode relaxes hostname charset rules
(`parser.rs:110-123`) and switches the diagnostics charset label
(`diagnostics.rs:14-17`). It must not be enabled in security-sensitive
deployments without measuring the risk against the upstream backends'
strictness.
6. **`HttpContext` outlives a single request when keep-alive is in play.**
`reset` clears the per-request fields but preserves the per-connection
ULID (`session_id`), the SNI-derived TLS state, and the rendered
`sozu_id_header` label. The `request_id` (`HttpContext::id`,
`editor.rs:161`) IS rotated per request to keep the access log
correlatable.
---
## 7. Cross-References
- `lib/src/protocol/mux/LIFECYCLE.md` — H2 frontend lifecycle. The H2 mux
speaks H1 to the backend via this very module.
- `lib/src/protocol/proxy_protocol/LIFECYCLE.md` — PROXY-v2 ingress that
precedes H1 on PROXY-aware listeners.
- `bin/src/command/LIFECYCLE.md` — supervisor view; explains how listener
reloads (which can swap `HttpAnswers` templates) propagate into running
sessions.
- `doc/lifetime_of_a_session.md` — operator-facing prose introduction; this
document is the maintainer-facing deep dive it links into.
- `doc/configure.md` — listener / cluster / answer configuration reference.