Skip to content

feat(router): allow error responses to carry headers - #2267

Merged
brendt merged 1 commit into
tempestphp:3.xfrom
osbre:feat/failed-request-headers
Aug 31, 2026
Merged

feat(router): allow error responses to carry headers#2267
brendt merged 1 commit into
tempestphp:3.xfrom
osbre:feat/failed-request-headers

Conversation

@osbre

@osbre osbre commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Problem

Returning response with headers works from a controller:

return new Json(body: ['message' => 'Slow down.'], status: Status::TOO_MANY_REQUESTS)
    ->addHeader('Retry-After', '30');

But when using throw HttpRequestFailed to return an error response from a different place, there is no way of passing headers, which leads to a few statuses that can only be sent correctly from inside a controller:

  • 429 needs Retry-After, otherwise the client has no idea when to come back
  • 401 needs WWW-Authenticate as per RFC 9110; the response is malformed without it
  • 405 needs Allow
  • (same for custom headers such as X-RateLimit-*)

Solution

HttpRequestFailed gains a headers parameter, and the handler applies it to whatever the renderer returned:

throw new HttpRequestFailed(
    status: Status::TOO_MANY_REQUESTS,
    headers: ['retry-after' => '60'],
);

Values may be a string or a list of strings, so multi-value headers work:

throw new HttpRequestFailed(
    status: Status::METHOD_NOT_ALLOWED,
    headers: ['allow' => ['GET', 'HEAD']],
);

Declared headers replace whatever the renderer set rather than adding to it: they describe the failure, which outranks anything assembled for the body. The lookup is case-insensitive, so a header the renderer spelled Retry-After and a throw site spelling it retry-after resolve to one header rather than two.

This mirrors Symfony's HttpExceptionInterface::getHeaders(), which Laravel reuses and also exposes as abort($status, $message, $headers).

@osbre
osbre force-pushed the feat/failed-request-headers branch from 0399f02 to 356bd6c Compare August 29, 2026 15:03
@github-actions

Copy link
Copy Markdown

Benchmark Results

Comparison of feat/failed-request-headers against 3.x (14514adb5b0a78dbb1409751aefa3f6f1b88be53).

Open to see the benchmark results

No benchmark changes above ±5%.

Generated by phpbench against commit 356bd6c

@osbre osbre changed the title feat(router): allow error responses to carry headers feat(router): error responses carry headers Aug 29, 2026
@osbre osbre changed the title feat(router): error responses carry headers feat(router): error responses can carry headers Aug 29, 2026
@osbre osbre changed the title feat(router): error responses can carry headers feat(router): let error responses carry headers Aug 29, 2026
@osbre osbre changed the title feat(router): let error responses carry headers feat(router): allow error responses carry headers Aug 29, 2026
@osbre osbre changed the title feat(router): allow error responses carry headers feat(router): allow error responses to carry headers Aug 29, 2026
@brendt
brendt merged commit dc589be into tempestphp:3.x Aug 31, 2026
88 of 90 checks passed
@brendt

brendt commented Aug 31, 2026

Copy link
Copy Markdown
Member

Great, thanks!

@osbre
osbre deleted the feat/failed-request-headers branch September 4, 2026 19:54
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