Rate limiting routes in Laravel - with tests

I just had to do some work on some rate limits on a few routes in Laravel and I could not find any resources on how to set it up, and test it properly.

Setting up the rate limit

Add the following code inside the configureRateLimiting() method in RouteServiceProvider.php

1RateLimiter::for('test', function (Request $request) {
2 return Limit::perMinute(10)->by($request->ip());
3});

Here we add a new rate limiter for use in routes and name it test. We set it to only allow 10 requests per minute, and track it by the clients ip address. The route can now be added to a route (or route group) as middleware.

1Route::get('/test', [TestController::class, 'index'])->middleware(['throttle:test']);

Testing that its active

Add a new Feature test, and add a new test

1public function test_rate_limit_is_active()
2{
3 $this->get('/test')
4 ->assertOk()
5 ->assertHeader('X-Ratelimit-Limit', 10)
6 ->assertHeader('X-Ratelimit-Remaining', 9);
7}

We first check that the response is ok, and then check if the header has the rate limiter limit (max attempts per minute) and the remaining count.

Next we can check if the remaining goes down by 1 for each request.

1public function test_rate_limit_decreases_remaining()
2{
3 for(range(1, 10) as $i) {
4 $this->get('/test')
5 ->assertOk()
6 ->assertHeader('X-Ratelimit-Remaining', 10 - $i);
7 }
8 $this->get('/test')
9 ->assertStatus(429)
10 ->assertHeader('Retry-After', 60);
11}

First we make 10 requests to the page, ensuring that remaining is decreased properly. We then finally check that we are refused access and cannot try again for 60 seconds.

Resetting attempts

If for some reason the rate limiter needs to reset on a proper request (for a page that uses signed URL's for instance) this can be a bit tricky to set up.

First let use add the signed middleware to url route to secure it

1Route::get('/test', [TestController::class, 'index'])->middleware(['throttle:test', 'signed']);

and a test

1public function test_signed_url_blocks()
2{
3 $this->get('/test')
4 ->assertForbidden()
5 ->assertHeader('X-Ratelimit-Remaining', 9);
6}

Now we just need to make sure that remaining attempts reset after actually getting to the route. Open the TestController and set it up like the following

1<?php
2 
3namespace App\Http\Controllers;
4 
5use Illuminate\Http\Request;
6use Illuminate\Support\Facades\RateLimiter;
7 
8class TestController extends Controller
9{
10 public function index(Request $request)
11 {
12 RateLimiter::clear(md5('test' . $request->ip()));
13 
14 return view('test');
15 }
16}

Notice here the RateLimiter::clear(md5('test' . $request->ip()));. This is what will reset the rate limit when the request goes through. Laravel does this by concatenating the rate limiter name, with the limit key set with ->by(). It then hash the string with md5.

1RateLimiter::for('test', function (Request $request) {
2 return Limit::perMinute(10)->by($request->ip());
3});

Lets test that as well

1public function test_ratelimit_resets()
2{
3 $this->get('/test')
4 ->assertForbidden()
5 ->assertHeader('X-Ratelimit-Remaining', 9);
6 
7 $this->withoutMiddleware(\Illuminate\Routing\Middleware\ValidateSignature::class)
8 ->get('/test')
9 ->assertOk()
10 ->assertHeader('X-Ratelimit-Remaining', 10);
11}

First we run a forbidden request to get the rate limiter to decrease. Next we do a secondary request where we disable the signature validation, and asserts that the remaining is back to 10.

The end

Hope that was helpful to some. As you can see, rate limit in laravel is simple to interact with once you get the basics. If you find any mistakes or have ideas for improvements, please contact me on @rsinnbeck