I'm building an API on top of Laravel. I'd like to use the built in rate limiting capabilities by using the Throttle
middleware.
Problem is, when the throttle middleware triggers the response is:
// Response headers
Too Many Attempts.
My API uses an error payload in JSON that looks like the following:
// Response headers
{
"status": "error",
"error": {
"code": 404,
"message": "Resource not found."
}
}
What is the best way to get the Throttle
middleware to return an output in the manner I require?
Make your own shiny middleware, extend it by original, and override methods you like to be overriden.
$ php artisan make:middleware ThrottleRequests
Open up kernel.php and remove (comment out) original middleware and add yours.
ThrottleRequests.php
<?php
namespace App\Http\Middleware;
use Closure;
class ThrottleRequests extends \Illuminate\Routing\Middleware\ThrottleRequests
{
protected function buildResponse($key, $maxAttempts)
{
return parent::buildResponse($key, $maxAttempts); // TODO: Change the autogenerated stub
}
}
kernel.php
.
.
.
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
//'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
//'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'throttle' => \App\Http\Middleware\ThrottleRequests::class
];
So what I did was create a custom middleware that extends the ThrottleRequest middleware. You can override the handle function to check the request and see if it expects JSON as a response. If so, call the buildJsonResponse function which will format a JSON 429 response. You can tailor the JsonResponse in buildJsonResponse to suit your API needs.
This allows your throttle middleware to handle both JSON and other responses. If the request is expecting JSON, it will return a json response, but otherwise it will return the standard "Too Many Attempts" plaintext response.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Routing\Middleware\ThrottleRequests;
class ThrottlesRequest extends ThrottleRequests
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param int $maxAttempts
* @param float|int $decayMinutes
* @return mixed
*/
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);
if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
// If the request expects JSON, build a JSON response, otherwise build standard response
if ($request->expectsJson()) {
return $this->buildJsonResponse($key, $maxAttempts);
} else {
return $this->buildResponse($key, $maxAttempts);
}
}
$this->limiter->hit($key, $decayMinutes);
$response = $next($request);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts)
);
}
/**
* Create a 'too many attempts' JSON response.
*
* @param string $key
* @param int $maxAttempts
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function buildJsonResponse($key, $maxAttempts)
{
$response = new JsonResponse([
'error' => [
'code' => 429,
'message' => 'Too Many Attempts.',
],
], 429);
$retryAfter = $this->limiter->availableIn($key);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
$retryAfter
);
}
}
Create a new file ApiThrottleRequests.php in app/Http/Middleware/ and paste the code below:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Cache\RateLimiter;
use Symfony\Component\HttpFoundation\Response;
class ApiThrottleRequests
{
/**
* The rate limiter instance.
*
* @var \Illuminate\Cache\RateLimiter
*/
protected $limiter;
/**
* Create a new request throttler.
*
* @param \Illuminate\Cache\RateLimiter $limiter
*/
public function __construct(RateLimiter $limiter)
{
$this->limiter = $limiter;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param int $maxAttempts
* @param int $decayMinutes
* @return mixed
*/
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);
if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
return $this->buildResponse($key, $maxAttempts);
}
$this->limiter->hit($key, $decayMinutes);
$response = $next($request);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts)
);
}
/**
* Resolve request signature.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function resolveRequestSignature($request)
{
return $request->fingerprint();
}
/**
* Create a 'too many attempts' response.
*
* @param string $key
* @param int $maxAttempts
* @return \Illuminate\Http\Response
*/
protected function buildResponse($key, $maxAttempts)
{
$message = json_encode([
'error' => [
'message' => 'Too many attempts, please slow down the request.' //may comes from lang file
],
'status' => 4029 //your custom code
]);
$response = new Response($message, 429);
$retryAfter = $this->limiter->availableIn($key);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
$retryAfter
);
}
/**
* Add the limit header information to the given response.
*
* @param \Symfony\Component\HttpFoundation\Response $response
* @param int $maxAttempts
* @param int $remainingAttempts
* @param int|null $retryAfter
* @return \Illuminate\Http\Response
*/
protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null)
{
$headers = [
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => $remainingAttempts,
];
if (!is_null($retryAfter)) {
$headers['Retry-After'] = $retryAfter;
$headers['Content-Type'] = 'application/json';
}
$response->headers->add($headers);
return $response;
}
/**
* Calculate the number of remaining attempts.
*
* @param string $key
* @param int $maxAttempts
* @param int|null $retryAfter
* @return int
*/
protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null)
{
if (!is_null($retryAfter)) {
return 0;
}
return $this->limiter->retriesLeft($key, $maxAttempts);
}
}
Then go to your kernel.php file in app/Http/ directory and replace
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
with
'throttle' => \App\Middleware\ApiThrottleRequests::class,
and use it
middleware('throttle:60,1')
or add
'apiThrottle' => \App\Http\Middleware\ApiThrottleRequests::class,
and you use this way
middleware('apiThrottle:60,1')
and help link
I know it is a very old question but I have a very short solution for this problem.
Instead of creating new middleware, we can catch and handle the ThrottleRequestsException inside the Handler.php and return the JSON response accordingly.
app\\Exceptions\\Hanlder.php
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
use Request;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* Report or log an exception.
*
* @param \Throwable $exception
* @return void
*
* @throws \Throwable
*/
public function report(Throwable $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $exception
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Throwable
*/
public function render($request, Throwable $exception)
{
if ($exception instanceof ThrottleRequestsException && $request->wantsJson()) {
return json_encode([
'message' => 'Too many attempts, please slow down the request.',
'status' => false
]);
}
return parent::render($request, $exception);
}
}
For anyone on laravel 8:
RateLimiter::for("verify",function(Request $request){
return Limit::perMinute(2)->by($request->ip())->response(function(){
return response()->json(["state"=>false,"error"=>"Youve tried too many times"],200);
});
});
And then add the throttle middleware to your route
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.