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.
install_error_handlersrenders everyHTTPExceptionthrougherror_response(status_code, code, message), which takes no headers, andon_http_errorreadsexc.status_codeandexc.detailand never touchesexc.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
405 loses
Allowthe same way, and nobody raised that one. Starlette's router raises the 405 itself with anAllowheader, and this handler drops it: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.pypublishesERRORS[429]describingRetry-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.401andWWW-Authenticateis the same shape and I have not measured it.The fix
error_responsegains an optionalheaders, andon_http_errorpassesexc.headersthrough. 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
Responseobject rather than raising, which works and is a second spelling for something the kit should do once. When this lands, that workaround should go.