Idempotent requests — Laravel

G
4 min readJan 9, 2021

Have you ever been in a situation where you needed to lock a route once a request is run? I.e. prevent the user from sending the same request twice. If that’s the case, you are in the right place.

Please note that this is not throttling, if you are interested in that instead, please check here.

Requirements

Some basic understanding of Laravel and how it works.

Step 1: Creating custom locking mechanism

We need a way to persistently store data, there are many options for this, but I decided to use cache simply because it is so easy to implement.

Here we are going to create a custom class/trait and use it as a locking mechanism. I personally prefer to use the trait method since it saves on the need to instantiate the class.

To proceed in this manner, first create a folder called Traits in app/Traits and create a new file called LockEventTrait.php

Therefore, our new file structure will be like this

app/Traits/LockEventTrait.php

We now need to make the Traits folder visible to our Laravel application, we do so by editing composer.json and adding the following configuration

"classmap": [
.......
"app/Traits",
]

After adding the above configuration run composer dumpautoload or composer dump-autoload to regenerate autoload files.

Step 2: Writing the code

I am going to save you time and instead paste the code that I use.

A bit of an explanation about the code above. Basically the code uses cache storage to indicate that a lock has been set. If the application is using array as the default cache store then we will instead use FileStore as the cache driver in that situation.

Step 3: Creating the middleware

We need to create a middleware that will utilize the lock class above, to create a middleware in Laravel, run the command php artisan make:middleware LockRouteMiddleware

We need to register the middleware alias in app\Http\Kernel.php and add the following configuration.

protected $routeMiddleware = [....
'lock' => \App\Http\Middleware\LockRouteMiddleware::class,
....
];

In your app\Http\Middleware folder you should have the LockRouteMiddleware.php file, replace it with the one below.

Explanation of the above code

Basically, we can pass in parameters to the middleware that allows us to tweak the behavior of the lock mechanism.

The most important parameter here is, $key this is the unique string that identifies the event. We first call the getKey($key) function which is found in theLockEventTrait ensures that no two requests from different users have the same key.

The $seconds parameter here specifies the TTL- time to live, in short how long the $key is valid in the system, for example if $seconds 100 then the $key is only valid for 100 seconds after that the route is unlocked.

The $unlock_after_request parameter specifies whether you want the route to be unlocked immediately after the request. This parameter overrides the $unlock_on error parameter.

The $unlock_on error parameter specifies that whenever the status code is not that of success i.e. 200, and 300 series then unlock the route.

More about how the configurations can be set below.

Step 4: Using the middleware

This is the final step, we now want to use middleware with a specific route.

We can use the above middleware like this.

Route::group(['middleware' => 'lock:my_key,200'], function(){
// Locked route go here example
Route::get('/example_route', function(){
.....
);
});

In the above configuration, we lock the route for 200 seconds provided an error does not occur. If an error does occur, then the route is unlocked.

Please note that my_key can be anything.

Other configurations possible

Here is a list of other configurations possible

Route::group(['middleware' => 'lock:my_key,200,false'], function(){
// Locked route go here example
Route::get('/example_route', function(){
.....
);
});

In this configuration, the route is not unlocked once the request has been processed and no error occurred.

Route::group(['middleware' => 'lock:my_key,200,false,false'], function(){
// Locked route go here example
Route::get('/example_route', function(){
.....
);
});

In this configuration, the route is locked for 200 seconds regardless of whether an error occurred or not.

Warning

Route::group(['middleware' => 'lock:my_key,0,false,false'], function(){
// Locked route go here example
Route::get('/example_route', function(){
.....
);
});

The above configuration must be used carefully, since it

  1. Locks the route forever, i.e. when $seconds is 0.
  2. It does not unlock the route after response.
  3. It does not unlock the route even if an error occurs.

Extending this idea

This is not limited to routes only, for example you can lock a route when a request is sent and unlock it after a job is processed.

This can also be used internally, maybe to prevent race condition. For example, if the same job is dispatched twice, then the second job can be ignored.

I also believe with a bit of ingenuity you can unlock the routes if a certain response code is received, e.g. if response code is 200 then unlock else continue locking, or vice versa. This is useful in a situation whereby you want to lock the route on success because it triggered an event that would last for several minutes. For example, suppose you delayed deleting a certain record from DB. If the response is 200 then we have started the delete process. But if it’s not, then we need to unlock the route, or you simply want to prevent another request for a certain period if the response was successfully processed.

Use cases

  1. You wish to lock the route to prevent double requests, another request is rejected until the initial request is processed. ['middleware' =>'lock:my_key'
  2. You wish to lock a route for 300 seconds (5 minutes), for example you wish that the user sends only one sms per 300 seconds. ['middleware' =>'lock:my_key,500,false']
  3. You wish to lock a route until a job is processed, ['middleware' =>'lock:my_key,0,false,false'] remember to create the unlock logic.

Recommendation

I recommend that you only lock a route forever if and only if you understand what you are doing. As I said, the above locking idea can be extended to more than just locking routes.

Using a good cache driver will affect the overall speed of the locking. Moreover, this must be used at a bare minimum since it slows down the request a bit. Therefore, it should be used in situations where we really want to restrict double requests.

Final thoughts

I hope the above helps you.

--

--

G

Backend developer with 7+ years of experience. Specializing mainly in PHP Laravel. Interested in natural simulations, visualization systems, and anything nerdy