HTTP mocks
A mock is a frozen response sitting next to its .curl / .http source file. Save one when an endpoint is being flaky; replay it later when you want to work on the UI that consumes the response without poking the live API. Useful for offline development, for canned data in screenshots / demos, and for the “I want to test how the page handles a 401 without convincing prod to give me a real one” case.
The file format is small and the round-trip is one-key — http.save_mock to capture, http.replay_mock to serve.
File shape
Section titled “File shape”requests/auth/login.curl ← the sourcerequests/auth/login.curl.mock.json ← the frozen responseA mock is a sibling file with .mock.json appended to the source’s full filename — extension included, so login.curl becomes login.curl.mock.json (not login.mock.json). This keeps .http and .curl versions of the same endpoint distinguishable on disk.
Per-block sidecars in multi-block .http files
Section titled “Per-block sidecars in multi-block .http files”A .http file with multiple ### name blocks gets one mock per named block, not one shared mock. The block name is interpolated into the sidecar path:
requests/users.http ← three blocks: list-users / get-user / delete-userrequests/users.list-users.http.mock.jsonrequests/users.get-user.http.mock.jsonrequests/users.delete-user.http.mock.jsonBlock-name characters that aren’t ASCII alphanumerics or - / _ / . are replaced with - in the filename, so a block named users/by id becomes users-by-id. Unnamed leading blocks (a .http file with no ### separator, or the chunk before the first one) fall back to the bare sibling path (users.http.mock.json) — that’s the single-block compatibility case.
Before 2026-06-28, all blocks shared one sidecar, so saving the second block’s response silently overwrote the first. The per-block path is now used by both http.save_mock and http.replay_mock, so the round-trip is symmetrical.
{ "status": 401, "status_text": "Unauthorized", "headers": [ ["content-type", "application/json"], ["x-trace-id", "abc-123"] ], "body": "{\"error\":\"token expired\",\"code\":\"AUTH_001\"}", "ts": 1734652800123}Fields:
| Field | What |
|---|---|
status | The numeric HTTP status (u16, required) |
status_text | The status reason phrase ("OK", "Unauthorized" — empty allowed) |
headers | Array of [name, value] pairs, preserving source casing and order |
body | The response body as a string (binary bodies aren’t supported — base64-encode externally if you need them) |
ts | Unix millis when the mock was saved. Informational; not read on replay |
A mock with a missing status field is rejected on load — that’s the one field replay can’t fabricate. Everything else has a sensible default ("" for status_text, empty array for headers, "" for body).
Saving a mock — http.save_mock
Section titled “Saving a mock — http.save_mock”From a Request pane that has a Done response:
| Surface | Call |
|---|---|
| Palette | HTTP: save current response as a sibling .mock.json |
| Ex-command | :http.save_mock |
Requirements:
- The active pane must be a
Pane::Request(http.save_mock: needs an active Request paneif not). - The pane must have a
source_path(http.save_mock: pane has no source file pathif you fired the request from a paste or an in-memory edit — there’s no canonical sibling location). - The response must be ready (
http.save_mock: response not ready yetif the state is stillSendingorFailed).
When all three check out, mnml writes the sibling file with the response’s status, status_text, headers, body, and the current timestamp. The directory is created if missing. Existing mocks are overwritten — there’s one mock per request, not a history of them.
Successful save toasts saved mock → <path>.
Replaying a mock — http.replay_mock
Section titled “Replaying a mock — http.replay_mock”:http.replay_mockOr palette HTTP: replay sibling .mock.json into the active request pane.
Replay flips the active Request pane straight into RunState::Done with the mock’s payload. No network call. The pane’s view also flips to Response so you see the result immediately:
- The status line reads the mock’s status + status_text.
- The headers list is the mock’s headers.
- The body is rendered with the same pretty-printer the live
http.senduses (JSON if it parses; raw otherwise). elapsedisDuration::ZERO(the mock didn’t take any time).- The assertions and captures arrays are empty —
@assertand@capturedirectives are response-driven, not mock-replayed.
The pane retains its source_path and source_block_name, so Ctrl+S after replay still writes the request back to the source file. The mock is read-only; saving doesn’t touch .mock.json.
Error cases:
- No
Pane::Requestactive →http.replay_mock: needs an active Request pane. - Pane has no source path →
http.replay_mock: pane has no source file path. - Sibling
.mock.jsonis missing →http.replay_mock: read <path>: No such file or directory. - Sibling is malformed JSON or missing
status→http.replay_mock: parse mock: …/http.replay_mock: mock missing status.
The save-then-replay loop
Section titled “The save-then-replay loop”1. Open requests/auth/login.curl2. :http.send → real request, real response, status 4013. :http.save_mock → writes requests/auth/login.curl.mock.json4. Edit the UI code that handles 4015. :http.replay_mock → instant 401 response, no network call6. Tweak the UI, replay again, iterateThe mock survives across mnml restarts because it’s a checked-in file. Commit .mock.json files for canned demo data; gitignore them when they hold session tokens or PII.
What mocks aren’t
Section titled “What mocks aren’t”- A request mocker. mnml mocks responses you’ve seen. There’s no “if the URL contains
/v2/users, serve this canned response” routing layer. You replay one mock at a time, manually, against a specific request pane. - A network mocker. Replay flips the pane state without going through the HTTP send path — no
reqwestcall, no DNS, no TLS. That means it also doesn’t exercise the network stack, so a mock-only test won’t catch a misconfigured TLS chain. - A history layer. Each save overwrites. If you want a history of past responses, that’s HTTP history — a separate JSONL log of every
http.send. - A response template. Mocks are static JSON. The body field is the literal bytes you got; there’s no
{{var}}substitution on replay. If you want dynamic mock data, write a helper that re-saves the mock with the substituted body.
Mock files in CI
Section titled “Mock files in CI”A useful pattern: commit a .mock.json next to every request file that hits a slow / paid / rate-limited endpoint. Run your tests with mnml run against the live endpoint when you want freshness; flip to replay during development by editing the .test step to load the mock instead of firing. The .test E2E runner doesn’t know about mocks today — you’d need an explicit step that calls :http.replay_mock on the active pane — but the disk layout is set up for it.
- HTTP client — the parent overview: how
http.sendbuilds the Request pane that mocks are saved from - HTTP history — for “every response I’ve ever seen” instead of “one frozen response per request”
- HTTP captured browser traffic — captured traffic is a separate file format; not directly compatible with mocks but useful for grabbing real-world response shapes
- HTTP envs & templating —
.mock.jsonfiles don’t substitute{{VAR}}, but the.envfile controls what the livehttp.sendsaw before you froze it