简体   繁体   中英

Laravel rate limit to return a JSON payload

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

https://thedevsaddam.gitbooks.io/off-time-story/how_to_customize_laravel_throttle_message_response.html

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM