Skip to content

Keep the response headers: paginated listings say there's more, failures carry a request id - #3

Merged
mattpodwysocki merged 1 commit into
mainfrom
response-headers
Sep 14, 2026
Merged

mattpodwysocki merged 1 commit into
mainfrom
response-headers

Conversation

@mattpodwysocki

Copy link
Copy Markdown
Contributor

Ported from mapbox-cli-private#133, which can no longer merge there now that oss/ is a submodule (#132). It was reviewed and CI-green there; re-verified here.

bytes() and text() consume a reqwest response, so a header not read before the body is gone for good. Content-Type was the only one taken, which left two separate things unreachable.

Paginated listings truncated silently

The API answers a listing with one page and a Link header naming the next. Nothing read it, so both output modes looked complete — and the generated help text told the reader to use that header, advice the CLI made impossible to follow. Listings now say so:

Tips:
  `-o json` for the response as the API sent it.
  To see one row: add `--id cms92ywgf0k5y2xq5dp196eq0`
  More results: add `--limit 1 --start cms92ywgf0k5y2xq5dp196eq0` for the next page.

The flags are derived, not hardcoded: the next URL's query is matched against op.query_params, so whatever a spec calls its paging parameters is what the line names.

The note goes to stderr in both modes, including -o json: the result is equally partial there, and the API's own document cannot carry the fact without an envelope the output contract forbids.

--id on a paged listing also stops saying No row has the id as though the row did not exist — true of the page, possibly false of the listing.

Failures carried no request id

Now in every -o json failure as request_id, and in -o text for a 5xx only, where the server is at fault and a 404 on a mistyped id stays uncluttered.

Measured, not assumed. I had asserted Mapbox APIs return X-Request-Id. They don't: styles, tokens, fonts and geocoding v6 all answer without it, on success and on a 404. What every response carries is x-amz-cf-id, because the API is fronted by CloudFront. REQUEST_ID_HEADERS reads both, preferring a service's own. Looking only for the conventional name would have shipped a feature that never fired once, with green unit tests throughout.

agent_skills is exempt on purpose — GitHub codeload's id is not one Mapbox support can look up — and tests/source_guards.rs fails a fourth send path that forgets to decide either way.

src/link.rs

Parses Link per RFC 8288 rather than matching a substring, because both things that break a shortcut occur in real headers: a URI may contain a comma, which is also the delimiter between links (bbox=-1,2,-3,4 is ordinary here), and rel may carry a space-separated list. The multi-link test caught a real bug in my first version, where the delimiter comma stayed attached and rel="next", didn't match.

The security property has two tests, deliberately

The access token rides in the query string, so the API echoes it back inside the Link URL this feature parses. Two things keep it out: it is added by dispatch rather than declared in op.query_params, and it is refused by name.

The second exists because the first is not enough, and a test showed me that: I wrote a_declared_parameter_named_access_token_is_still_withheld expecting it to pass on "by construction" reasoning, and it failed. A spec is free to declare a parameter called access_token, and on the day one did, the CLI would have printed a live credential. Please don't collapse those two tests into one.

Verified here

569 tests pass, cargo fmt --check and cargo clippy --all-targets clean. Against the live API before the port: following the printed tip returned a genuinely different page, a real 404 carried request_id, and access_token/sk.ey/pk.ey appear zero times across every stderr capture.

Not included

Retry-After and retry — a field with no consumer would be dead code, which is why the headers are a struct rather than three reads at the call site. --all auto-pagination, which needs a decision about how combined pages are emitted. Tracked in mapbox-cli-private#117.

The original PR also edited AGENTS.md, which has no counterpart in this repository, so those invariant notes stay in the private repo.

`bytes()` and `text()` consume a `reqwest` response, so a header not read
before the body is gone for good. `Content-Type` was the only one taken,
which left two things unreachable.

**Paginated listings truncated silently.** The API answers with one page and
a `Link` header naming the next; nothing read it, so both output modes looked
complete — while the generated `--help` told the reader to use that header.
Listings now print the flags that fetch the next page, on stderr in both
modes, because the result is equally partial under `-o json` and the API's
document cannot carry the fact without an envelope the output contract
forbids. `--id` on a paged listing also stops saying "No row has the id" as
though the row did not exist.

The flags are derived by matching the next URL's query against
`op.query_params`, so whatever a spec calls its paging parameters is what the
tip names. The access token cannot appear in it: `dispatch` adds it to the
query rather than declaring it, *and* it is refused by name — because a spec
is free to declare a parameter called `access_token`, which a test caught
while it was still only excluded "by construction".

**A failure carried no request id.** Now in every `-o json` failure, and in
`-o text` for a 5xx only. Measured rather than assumed: no Mapbox endpoint
sends `x-request-id` — styles, tokens, fonts and geocoding v6 all answer
without it — so `REQUEST_ID_HEADERS` also reads CloudFront's `x-amz-cf-id`,
which every response carries. Looking only for the conventional name would
have shipped a feature that never fired. `agent_skills` is exempt on purpose:
GitHub codeload's id is not one Mapbox support can look up, and
`source_guards.rs` fails a fourth send path that forgets to decide.

`src/link.rs` parses the header per RFC 8288 rather than matching a
substring, because both things that break a shortcut occur in real headers: a
URI may contain a comma, which is also the delimiter between links, and `rel`
may carry a space-separated list.

`Retry-After` is deliberately not read yet — nothing retries, so a field with
no consumer would be dead code. It is the next consumer to add, and the
reason this is a struct rather than three reads at the call site.

Ported from mapbox/mapbox-cli-private#133, which cannot merge there now that
`oss/` is a submodule (mapbox/mapbox-cli-private#132). That PR also edited
`AGENTS.md`, which has no counterpart in this repository, so the invariant
notes stay in the private repo.

@zmofei zmofei left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the diff (994/+47) and CI (all green). Pagination handling (src/link.rs, RFC 8288 parsing), the request-id header fallback (measured against real Mapbox endpoints rather than assumed), and the access-token exclusion from pagination tips (double-covered by tests) all look correct. The new source_guards.rs test guarding future send paths is a good defensive addition. No issues found.

@mattpodwysocki
mattpodwysocki merged commit 52d01ee into main Sep 14, 2026
8 checks passed
@mattpodwysocki
mattpodwysocki deleted the response-headers branch September 14, 2026 17:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants