Files
Flash5/flash-extensions/flash-ext-limiter/docs/http-headers.md
T
Zakaria El OrcheandClaude Opus 5 439588c19f feat(ext-openapi): pick the page that reads the document
Swagger UI, Redoc or Scalar, each configured with its own options under the
names its own documentation gives them, or no page at all. What a UI does not
name still passes through, so a bundle's whole option set stays reachable
without this extension tracking it.

The page is rendered once at boot and the document is encoded once per
revision, so a request to any of the three routes hands out bytes rather than
building them: the spec used to be serialized again on every single request.

/openapi/swagger becomes /openapi/docs, because the path names what is served
and not which bundle happens to serve it. That page also named a preset that
lives in a bundle it never loaded, and BaseLayout never needed it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 12:03:48 +00:00

129 lines
3.9 KiB
Markdown

# HTTP headers and 429 response
The extension injects standard rate-limit headers on **every** request — both allowed
and rejected. Clients can use these headers to implement back-off logic without waiting
for a 429.
## Response headers
| Header | Type | Description |
|---|---|---|
| `X-RateLimit-Limit` | integer | Maximum requests allowed in the current window |
| `X-RateLimit-Remaining` | integer | Requests remaining in the current window (≥ 0) |
| `X-RateLimit-Reset` | Unix timestamp (s) | When the quota resets or the next token arrives |
| `Retry-After` | seconds | **Only on 429** — how long to wait before retrying (≥ 1) |
### Example — allowed request
```
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1711750860
Content-Type: application/json
```
### Example — rejected request (429)
```
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711750860
Retry-After: 1
Content-Type: text/plain
Too Many Requests
```
## Header semantics by strategy
### FIXED_WINDOW
| Header | Value |
|---|---|
| `X-RateLimit-Reset` | Unix timestamp of the **next window start** (aligned to clock) |
| `Retry-After` | Seconds until `X-RateLimit-Reset` (minimum 1) |
At a 1-second window boundary `Retry-After` will typically be `1`.
### TOKEN_BUCKET
| Header | Value |
|---|---|
| `X-RateLimit-Remaining` | Current token count (may increase between requests due to refill) |
| `X-RateLimit-Reset` | Estimated Unix timestamp when the **next token arrives** |
| `Retry-After` | Milliseconds-precise estimate converted to seconds (minimum 1) |
Because the token bucket refills continuously, `X-RateLimit-Reset` is a near-future
timestamp rather than an aligned window boundary.
## Retry-After precision
`Retry-After` is computed as:
```
retryAfter = max(1, X-RateLimit-Reset - currentTimeSeconds)
```
The minimum value is always `1` second — RFC 7231 discourages `Retry-After: 0` as it
encourages instant retry loops.
## Client-side back-off example (Java)
```java
HttpResponse<String> res = client.send(request, BodyHandlers.ofString());
if (res.statusCode() == 429) {
String retryAfter = res.headers().firstValue("Retry-After").orElse("1");
long waitMs = Long.parseLong(retryAfter) * 1000L;
Thread.sleep(waitMs);
// retry...
}
```
## Client-side back-off example (JavaScript fetch)
```js
const res = await fetch('/api/search?q=flash');
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '1', 10);
await new Promise(r => setTimeout(r, retryAfter * 1000));
// retry...
}
```
## Monitoring / alerting
`X-RateLimit-Remaining` can be scraped by a metrics agent to track approaching limits
before they hit 429:
- `remaining / limit < 0.1` → warning (less than 10% quota left)
- `status == 429` → rate-limit violation counter increment
If `flash-ext-limiter` is used together with a future metrics extension, the 429 rate
per resolver key is a natural signal for abuse detection or auto-scaling.
## Header injection timing
Headers are injected **before** calling `next.handle(req, res)` on allowed requests,
and **instead of** calling it on rejected requests. This means:
- Handlers cannot accidentally overwrite `X-RateLimit-*` headers (they are set first,
but handlers that call `res.header(...)` with the same name will add a second value —
avoid this by not setting these headers manually).
- On 429, the handler body is never executed — no side effects occur.
## Integration with OpenAPI (flash-ext-openapi)
When `flash-ext-openapi` is installed, handlers annotated with `@Limit` automatically
contribute rate-limit response headers to generated OpenAPI responses:
- `X-RateLimit-Limit`
- `X-RateLimit-Remaining`
- `X-RateLimit-Reset`
- `Retry-After` on `429`
If `429` is not manually declared, OpenAPI auto-adds `429 Too Many Requests`.