Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

kjr's avatar
Level 1

Throwing ThrottleRequestsException but no Retry-After and X-RateLimit-Reset headers on response

I have a route with the Illuminate\Routing\Middleware\ThrottleRequests middleware as such:

$router->group(['middleware' => 'throttle:1,1'], function () use ($router) {
    $router->get('/someresource', 'SomeResourceController@all');
});

And it is working as expected, when exceeding the limit of 1 request per minute (low limit set for testing purposes) the ThrottleRequestsException is thrown and a 429 Too many requests response returned but the Retry-After and X-RateLimit-Reset headers are absent from response headers

Example response

< HTTP/1.1 429 Too Many Requests
< Date: Fri, 30 Oct 2020 08:16:40 GMT
< Server: Apache/2.4.38 (Debian)
< Vary: Authorization
< X-Powered-By: PHP/7.4.11
< Cache-Control: no-cache, private
< Transfer-Encoding: chunked
< Content-Type: text/html; charset=UTF-8

But the buildException method of class ThrottleRequests clearly sets them

protected function handleRequest($request, Closure $next, array $limits)
    {
        foreach ($limits as $limit) {
            if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) {
                dd($this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback)); //If I add this line I can see the headers
                throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback); //when exception is actually thrown the headers are missing from the http response
            }

            $this->limiter->hit($limit->key, $limit->decayMinutes * 60);
        }
        $response = $next($request);

        foreach ($limits as $limit) {
            $response = $this->addHeaders(
                $response,
                $limit->maxAttempts,
                $this->calculateRemainingAttempts($limit->key, $limit->maxAttempts)
            );
        }
        return $response;
    }

dd() output of buildException()

^ Illuminate\Http\Exceptions\ThrottleRequestsException {#45 ▼
  -statusCode: 429
  -headers: array:4 [▼
    "X-RateLimit-Limit" => 1
    "X-RateLimit-Remaining" => 0
    "Retry-After" => 57
    "X-RateLimit-Reset" => 1604046100
  ]
  #message: "Too Many Attempts."
  #code: 0
  #file: "/var/www/html/app/Http/Middleware/ThrottleRequests.php"
  #line: 199
  trace: {▶}
}

What I have tried to no avail:

  1. Disabling all other middleware I am running
  2. Different HTTP clients
  3. Different APP_ENV and APP_DEBUG value combinations
0 likes
10 replies
laracoft's avatar

@kjr

What is your Laravel version? And what HTTP client are you using?

kjr's avatar
Level 1

@laracoft

I am using the latest Lumen version. 8?

Illuminate\Routing\Middleware\ThrottleRequests class also taken from illuminate branch 8.x

Application is running in a local Docker container (Apache with PHP 7.4)

Sinnbeck's avatar

What do you get if you dd before the return ?

dd($reponse->headers);
return $response;
laracoft's avatar

@kjr I'm scratching my head over how to tackle this.

In my view, it's best if you have xdebug and step through from the exception onwards.

Short of that, your code looks good to me.

Sinnbeck's avatar

Sorry I must be blind today :D

I agree. Use xdebug to step through :)

kjr's avatar
Level 1

Thank you! Seems that you pushed me in the right direction

That particular line you suggested didn't have an effect as the if condition is not met. Seems to me that ThrottleRequestsException is not an instance of HttpResponseException (rather its an instance of HttpException)

So instead the render() method of Exceptions/Handler.php hits its return statement:

return $request->expectsJson()
                        ? $this->prepareJsonResponse($request, $e)
                        : $this->prepareResponse($request, $e);

Which in this case calls the prepareResponse() method (#L164)

protected function prepareResponse($request, Throwable $e)
    {
        $response = new Response(
            $this->renderExceptionWithSymfony($e, config('app.debug', false)),
            $this->isHttpException($e) ? $e->getStatusCode() : 500
        );
        if (method_exists($e, 'getHeaders')) { //Added this block
            $response->headers->add($e->getHeaders());
        }
        $response->exception = $e;

        return $response;
    }

After adding the line there (which adds the headers to the response) I get the desired result:

< HTTP/1.1 429 Too Many Requests
< Date: Fri, 30 Oct 2020 10:19:22 GMT
< Server: Apache/2.4.38 (Debian)
< Vary: Authorization
< X-Powered-By: PHP/7.4.12
< Cache-Control: no-cache, private
< X-RateLimit-Limit: 1
< X-RateLimit-Remaining: 0
< Retry-After: 52
< X-RateLimit-Reset: 1604053216
< Transfer-Encoding: chunked
< Content-Type: text/html; charset=UTF-8

But why isn't adding headers from thrown Exception object to the response default Lumen behaviour?

Seems to be so for Laravel's exception handler (I might be wrong though, didn't test specifically): https://github.com/laravel/framework/blob/8.x/src/Illuminate/Foundation/Exceptions/Handler.php#L585

Sinnbeck's avatar

Ah check. Just looked through the source of laravel itself and tried finding the same in lumen, so I might have missed it by a little :) Great that you got it working

Please or to participate in this conversation.