Skip to content

install_error_handlers drops every header on a raised error, so 429 has no Retry-After and 405 has no Allow #16

Description

@crypto-a

install_error_handlers renders every HTTPException through error_response(status_code, code, message), which takes no headers, and on_http_error reads exc.status_code and exc.detail and never touches exc.headers. So any header a raiser attaches is silently dropped, fleet-wide.

Found while building harness's workspace seam, where the workspace is told to back off and retry with jitter and the 429 could not carry the one field that says when.

Reproduced, not inferred

from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from kit.httpapi import install_error_handlers

app = FastAPI()
install_error_handlers(app)

@app.get("/limited")
def limited() -> dict[str, str]:
    raise HTTPException(status_code=429, detail="slow down", headers={"Retry-After": "5"})

r = TestClient(app, raise_server_exceptions=False).get("/limited")
status        : 429
Retry-After   : None
body          : {'error': {'code': 'rate_limited', 'message': 'slow down'}, 'request_id': ''}

405 loses Allow the same way, and nobody raised that one. Starlette's router raises the 405 itself with an Allow header, and this handler drops it:

405 Allow header: None
405 status      : 405

RFC 9110 says a 405 response "MUST generate an Allow header field". That is not a service's mistake to make; it is happening to every service that installs these handlers, on a path none of them wrote.

Why this is worth more than the missing header

It is the fleet's own documented failure shape: an artifact asserting something nothing makes true. Harness's api/v1/responses.py publishes ERRORS[429] describing Retry-After, in its OpenAPI document, to clients. That description was accurate about intent and false about behaviour, and the only way to find out was to read the response.

401 and WWW-Authenticate is the same shape and I have not measured it.

The fix

error_response gains an optional headers, and on_http_error passes exc.headers through. The envelope shape is contract and does not change; headers are transport and were never part of it.

Worth pairing with a test per status that asserts the header ARRIVES rather than that the envelope is well formed, because a well-formed envelope is exactly what this returns today.

Harness's workaround, so you know what to expect

Harness writes its 429 onto the Response object rather than raising, which works and is a second spelling for something the kit should do once. When this lands, that workaround should go.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions