Found while fixing #16937, which corrects the details bags in the same fences. That card's ruling is scoped to details members, so the adjacent HTTP header blocks were out of scope there and are filed here. Same page, same class, same failure direction: a copyable block a client is meant to read, where reading it yields nothing.
Measured on origin/main at fd5cff209f.
The claims
content/docs/protocol/kernel/error-handling.mdx carries three header blocks:
- under
#### RATE_LIMIT_EXCEEDED and again under ### Rate Limit Exceeded —
X-RateLimit-Limit: 1000, X-RateLimit-Remaining: 0, X-RateLimit-Reset: 1705412460
- under
#### SERVICE_UNAVAILABLE — Retry-After: 300
Readings, each with a positive control on the same corpus
| reading |
result |
positive control on the same corpus |
X-RateLimit anywhere in packages/** |
1 hit, not a producer |
Retry-After in packages/**: 142 |
any header-set of a ratelimit-named header (res.header / setHeader / headers[...]) tree-wide |
0 |
same forms for Retry-After: 5 |
Retry-After within 10 lines of any 503 site (non-test, non-CHANGELOG) |
0 |
identical technique on 429 sites: 2 |
Widening X-RateLimit from packages/** to the whole tree gives 31 hits, and every one is about GitHub's own API quota headers, not an ObjectStack response — they sit in scripts/pm/check-clause2-carriers.mjs and .github/workflows/docs-drift-check.yml. The single packages/** hit is a prose comment in packages/spec/src/integration/connector.test.ts:656 about upstream connector header names. No ObjectStack producer sets an X-RateLimit-* header on any response.
The Retry-After row is the more interesting one, because the header itself is real: both 429 producers set it —
packages/runtime/src/endpoint-policy.ts headers: { 'Retry-After': String(retryAfterSec) }
packages/runtime/src/security/inbound-rate-limit.ts res.header('Retry-After', String(retryAfterSec))
— so the control fires and the zero on the 503 side is a reading, not a dead grep. The five real 503 producers (packages/objectql/src/action-activation.ts:269, packages/triggers/trigger-api/src/api-trigger.ts:204, packages/plugins/plugin-auth/src/auth-plugin.ts:2260 and :2883) set no Retry-After and attach no details bag.
Consumer harm
A client written from the 429 header block paces itself on X-RateLimit-Remaining and reads null on every real 429 — so it does not pace at all, which is the opposite of what the block is teaching. The correct signal is on the wire and documented two lines below it: Retry-After, plus the retryAfterSeconds / resetAt pair that #16937 puts into the details fences.
A client written from the 503 block waits on a Retry-After that never arrives.
Why nothing catches it
Same gap #16937 and #17187 record on this page: these are prose fences, so neither the type-check nor any schema reaches them, and no gate compares a documented response header against the headers any producer sets.
Suggested fix
Drop the X-RateLimit-* lines from both 429 header blocks, keeping Retry-After, and drop the 503 header block. Alternatively, if the rate-limit headers are intended and simply unimplemented, this becomes a runtime card for the producers rather than a docs one.
Found while fixing #16937, which corrects the
detailsbags in the same fences. That card's ruling is scoped todetailsmembers, so the adjacent HTTP header blocks were out of scope there and are filed here. Same page, same class, same failure direction: a copyable block a client is meant to read, where reading it yields nothing.Measured on
origin/mainatfd5cff209f.The claims
content/docs/protocol/kernel/error-handling.mdxcarries three header blocks:#### RATE_LIMIT_EXCEEDEDand again under### Rate Limit Exceeded—X-RateLimit-Limit: 1000,X-RateLimit-Remaining: 0,X-RateLimit-Reset: 1705412460#### SERVICE_UNAVAILABLE—Retry-After: 300Readings, each with a positive control on the same corpus
X-RateLimitanywhere inpackages/**Retry-Afterinpackages/**: 142res.header/setHeader/headers[...]) tree-wideRetry-After: 5Retry-Afterwithin 10 lines of any 503 site (non-test, non-CHANGELOG)Widening
X-RateLimitfrompackages/**to the whole tree gives 31 hits, and every one is about GitHub's own API quota headers, not an ObjectStack response — they sit inscripts/pm/check-clause2-carriers.mjsand.github/workflows/docs-drift-check.yml. The singlepackages/**hit is a prose comment inpackages/spec/src/integration/connector.test.ts:656about upstream connector header names. No ObjectStack producer sets anX-RateLimit-*header on any response.The
Retry-Afterrow is the more interesting one, because the header itself is real: both 429 producers set it —— so the control fires and the zero on the 503 side is a reading, not a dead grep. The five real 503 producers (
packages/objectql/src/action-activation.ts:269,packages/triggers/trigger-api/src/api-trigger.ts:204,packages/plugins/plugin-auth/src/auth-plugin.ts:2260and:2883) set noRetry-Afterand attach nodetailsbag.Consumer harm
A client written from the 429 header block paces itself on
X-RateLimit-Remainingand readsnullon every real 429 — so it does not pace at all, which is the opposite of what the block is teaching. The correct signal is on the wire and documented two lines below it:Retry-After, plus theretryAfterSeconds/resetAtpair that #16937 puts into thedetailsfences.A client written from the 503 block waits on a
Retry-Afterthat never arrives.Why nothing catches it
Same gap #16937 and #17187 record on this page: these are prose fences, so neither the type-check nor any schema reaches them, and no gate compares a documented response header against the headers any producer sets.
Suggested fix
Drop the
X-RateLimit-*lines from both 429 header blocks, keepingRetry-After, and drop the 503 header block. Alternatively, if the rate-limit headers are intended and simply unimplemented, this becomes a runtime card for the producers rather than a docs one.