Reputation: 762
I'm fairly new to Laravel and am currently using an API that has a limit of 25 requests per minute.
I have a controller method sendRequest()
which is used by all methods to send requests to the API so I was thinking this is the place to put a rate limiter that checks if the current request can be added to the queue if the limit is not yet reached.
I was thinking something like this:
protected function sendRequest(){
if ($this->allowRequest()) {
//proceed to api call
}
}
protected function allowRequest() {
$allow = false;
//probably a do-while loop to check if the limit has been reached within the timeframe?
}
I've found this class Illuminate\Cache\RateLimiter
that I think could be useful but have no idea how to use it yet. Can anyone point me to the right direct with this? So basically the request should "wait" and execute only if the 25 requests/minute limit hasn't been reached.
Thanks!
Upvotes: 5
Views: 14330
Reputation: 541
For Laravel 8 and PHP 7.4 I take the example of https://stackoverflow.com/a/62642143/3145399
and make this Trait:
<?php
namespace App\Traits;
use Closure;
use Illuminate\Cache\RateLimiter;
trait CustomRateLimiter {
protected string $throttleKey = 'GeneralRateLimit';
/**
* Determine if we made too many requests.
*
* @param int $maxAttempts
*
* @return bool
*/
protected function hasTooManyRequests( $maxAttempts = 10 ): bool {
return $this->limiter()->tooManyAttempts(
$this->throttleKey(), $maxAttempts // <= max attempts per minute
);
}
/**
* Get the rate limiter instance.
*
* @return RateLimiter
*/
protected function limiter(): RateLimiter {
return app( RateLimiter::class );
}
/**
* Get the throttle key for the given request.
*
* @param string $key
*
* @return string
*/
protected function throttleKey( $key = 'GeneralRateLimit' ): string {
return $this->throttleKey ?? $key;
}
/**
* @param Closure $callback Anonymous function to be executed - example: function(){ return realFunction();}
* @param int $maxAttempts Maximum number of hits before process sleeps
* @param string $throttleKey If you have different Apis, change this key to a single key.
* @param int $decaySeconds Time that will sleep when the condition of $maxAttempts is fulfilled
* @param int $optionalSecond Optional plus secs to be on safe side
*
* @return mixed
*/
protected function sendRequest( Closure $callback, $maxAttempts = 10, $throttleKey = 'GeneralRateLimit', $decaySeconds = 1, $optionalSecond = 1 ) {
$this->throttleKey = $throttleKey;
if ( $this->hasTooManyRequests( $maxAttempts ) ) {
// wait
sleep( $this->limiter()->availableIn( $this->throttleKey() ) + $optionalSecond );
// Call this function again.
return $this->sendRequest( $callback, $maxAttempts, $throttleKey, $decaySeconds, $optionalSecond );
}
//proceed to api call
$response = $callback();
// Increment the attempts
$this->limiter()->hit(
$this->throttleKey(), $decaySeconds // <= 1 seconds
);
return $response;
}
}
how to use it?
use App\Traits\CustomRateLimiter;
class MyClass {
use CustomRateLimiter;
public function realApiToCall($var1){
// custom logic
}
public function apiCall($var1){
$apiResponse = $this->sendRequest( function () use ( $var1 ) {
return $this->realApiToCall($var1);
}, 4, 'customKey1', 1 );
}
}
Upvotes: 1
Reputation: 9029
The Illuminate\Cache\RateLimiter
class has hit
and tooManyAttempts
methods you can use like this:
use Illuminate\Cache\RateLimiter;
use Illuminate\Http\Request;
protected function sendRequest()
{
if ($this->hasTooManyRequests()) {
// wait
sleep(
$this->limiter()
->availableIn($this->throttleKey()) + 1 // <= optional plus 1 sec to be on safe side
);
// Call this function again.
return $this->sendRequest();
}
//proceed to api call
$response = apiCall();
// Increment the attempts
$this->limiter()->hit(
$this->throttleKey(), 60 // <= 60 seconds
);
return $response;
}
/**
* Determine if we made too many requests.
*
* @return bool
*/
protected function hasTooManyRequests()
{
return $this->limiter()->tooManyAttempts(
$this->throttleKey(), 25 // <= max attempts per minute
);
}
/**
* Get the rate limiter instance.
*
* @return \Illuminate\Cache\RateLimiter
*/
protected function limiter()
{
return app(RateLimiter::class);
}
/**
* Get the throttle key for the given request.
*
* @return string
*/
protected function throttleKey()
{
return 'custom_api_request';
}
See Illuminate\Cache\RateLimiter
class for more available methods.
You may also check Illuminate\Foundation\Auth\ThrottlesLogins
as an example to figure out how to use Illuminate\Cache\RateLimiter
class.
Note: The RateLimiter
methods use seconds instead of minutes since Laravel >= 5.8 and got a major improvement on v8.x.
Upvotes: 7
Reputation: 8979
Here you'll need the shared timer like control for frequency capping of the outgoing request. Create a class that will be a singleton for Laravel application and can be shared within requests.
class FrequencyCapper{
protected $start, $call, $request_frequency, $limit_interval;
public function __construct($frequency, $interval_in_minute){
$this->start = time();
$this->call = 0;
$this->request_frequency = frequency; // frequency of call
$this->limit_interval = $interval_in_minute; // in minutes
}
protected function allowRequest(){
$diff = time() - $this->start;
if($diff >= 60 * $this->limit_interval){
$this->start = time();
$this->call = 0;
}
return $diff < 60 * $this->limit_interval && $this->call < $this->request_frequency){
$this->call++;
return true;
}else{
return false;
}
}
}
Now, attach this class as a singleton in laravel service container. bind singleton in App\Providers\AppServiceProvider.php
's boot method.
$this->app->singleton('FrequencyCapper', function ($app) {
return new FrequencyCapper(25, 1); //max 25 request per minute
});
Now, this class will be available to all controllers as a dependency. You can inject FrequencyCapper
dependency to any controller method as given,
class MyController extends Controller{
protected function sendRequest(FrequencyCapper $capper){
if($capper->allowRequest()){
//you can call the api
}
}
}
if you want, you can use microtime()
instead time() in FrequencyCapper
class. If you want to throttle the incoming request to your own api, you can use laravel's throttle
middleware as,
Route::group(['prefix' => 'api', 'middleware' => 'throttle:25,1'], function(){
//define your routes here
});
Upvotes: 1