Each task's tests were run against a reference at its end state; the end states were replayed from master in order with the gate at each step (650 to 762 tests); each skeleton compiles against its tests and fails them. The reference is kept off this machine. Lessons T27 (every wait in a test has a limit) and T28 (mutate the reference before hand-over) come from this work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
58 lines
2.8 KiB
Markdown
58 lines
2.8 KiB
Markdown
# M4a task 06: HTTP/1.1 requests
|
|
|
|
**Branch:** `m4a` (run `git switch m4a`; `git status --short` must be empty, otherwise stop)
|
|
**Commit subject:** `gatewayd: http, requests over a stream with size caps and rate-limit waits`
|
|
|
|
## Goal
|
|
|
|
Mattermost's REST calls are HTTP/1.1, one connection per request (`Connection: close`), over a
|
|
`net::Stream`. This module writes a request and reads the response. The server is not trusted: a
|
|
response head over 16 KiB or a body over 4 MiB is an error, and every limit is checked **before**
|
|
reading or allocating. The WebSocket handshake (task 07) reuses `read_head`, which therefore must
|
|
not read a byte past the blank line that ends the head: the WebSocket's first frame may follow at
|
|
once.
|
|
|
|
## Files
|
|
|
|
- Copy: `crates/gatewayd/tests/http.rs`, and the skeleton `crates/gatewayd/src/http.rs`
|
|
- Modify: `crates/gatewayd/src/lib.rs` (`pub mod http;`), `docs/implementer-log.md`
|
|
|
|
## The skeleton
|
|
|
|
Written: the constants `MAX_HEAD` (16 KiB), `MAX_BODY` (4 MiB) and `MAX_RATE_WAIT` (60 s);
|
|
`HttpError` (`Io`, `Protocol(String)`, `TooLarge(&'static str)`) with `Display` and
|
|
`From<std::io::Error>`; `Head { status, headers }` and `Response { head, body }`.
|
|
|
|
To fill, each with its steps above it: `Head::header`, `write_request`, `request`, `read_head`,
|
|
`read_body`, `read_line`, `read_chunked`, `rate_limit_wait`.
|
|
|
|
Rules that apply to all of them:
|
|
|
|
- `read_head` reads **one byte at a time**. That is slow and it is right: it must stop exactly at
|
|
the end of the head.
|
|
- A retry on `ErrorKind::Interrupted`, and nowhere else.
|
|
- No subtraction that can wrap and no indexing that can go out of bounds: `saturating_sub`, `get`,
|
|
`get_mut`. `raw.truncate(raw.len() - 2)` is fine only right after checking `raw.ends_with(b"\r\n")`.
|
|
- `rate_limit_wait` is about Mattermost's `X-Ratelimit-Reset`, which is a Unix time in seconds; the
|
|
test also gives it a small number of seconds, which some servers send.
|
|
|
|
## Steps
|
|
|
|
- [ ] **1. Copy.** `git switch m4a`, then
|
|
`cp docs/plans/M4a/files/crates/gatewayd/tests/http.rs crates/gatewayd/tests/ && cp docs/plans/M4a/files/crates/gatewayd/src/http.rs crates/gatewayd/src/`.
|
|
Add `pub mod http;` to `lib.rs`.
|
|
- [ ] **2. See it fail.** `cargo test -p gatewayd --test http`. Expected: it compiles and 6 tests
|
|
fail.
|
|
- [ ] **3. Fill the functions in the order above**, `cargo check -p gatewayd` after each.
|
|
- [ ] **4. See it pass.** `cargo test -p gatewayd --test http`. Expected: 6 passed.
|
|
- [ ] **5. Run the gate.** `cargo fmt --all`, then `make gate`. Expected last line: `gate: ok`.
|
|
- [ ] **6. Log and commit.** `git add crates/gatewayd docs/implementer-log.md Cargo.lock && git commit`
|
|
|
|
## Done when
|
|
|
|
- `cargo test -p gatewayd --test http` passes; `make gate` prints `gate: ok`.
|
|
|
|
## Stop and report if
|
|
|
|
- A test needs `read_head` to read ahead into a buffer.
|