DEV Community

Cover image for Preventing Duplicate Requests in Laravel: The AtomicLockMiddleware
Hafiq Iqmal
Hafiq Iqmal

Posted on

Preventing Duplicate Requests in Laravel: The AtomicLockMiddleware

How to Handle Multiple Request Collisions and Keep Your Laravel App Running Smoothly??


If you ever developed a web app in Laravel, you must have faced one such problem which is rather common: handling of duplicate requests currently being processed. It's that trivial problem, but it may lead to data inconsistency, a failed transaction, or-what's worse-a poor user experience. Imagine your application handling some form submission twice. You'd end up with duplicate records or maybe even duplicated payments, which would be a huge headache both for you and your users.

For this, we need to ensure each request should be processed one at a time, especially when the action is sensitive, such as making any payment or updating something. One of the solutions is by using an atomic lock. In this post, I am going to walk you through how I implemented a simple middleware in Laravel to handle such scenarios. This middleware will allow processing of only one request from any particular user at one time for specific actions.

Let's take a closer look into how it works and why you might want to use it in your projects.


Why Should You Care About Duplicate Requests?

Before getting into the code, let's talk about why handling duplicate requests is important. It's easy to think of a web request as a one-time thing, but there are several scenarios where duplicate requests can sneak in, like:

  • Slow internet connections: Users might click the same button multiple times out of impatience.
  • Page reloads: If a user refreshes the page quickly after submitting a form, it might send the same request again.
  • Automated scripts or bots: These can sometimes trigger requests in a way you don't expect.

In these cases, Laravel could end up processing 2 (or more) identical requests at the same time. That's where the atomic lock middleware comes in.


The Middleware

The middleware is simple and its ensures only one request from a user is processed at a time. It works by locking the request URL with a unique key in Laravel's cache. If another request with the same URL comes in before the first one finishes, the middleware throws an error, preventing the duplicate request from being processed.

Here's the full code of the AtomicLockMiddleware:-

class AtomicLockMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next, $prefix = null): Response
    {
        // Ensure that no duplicate request is being processed
        $this->ensureSingleRequest($request);

        // Proceed with the original request
        $response = $next($request);

        // Release the lock after the request is processed
        $this->releaseSingleRequest($request);

        return $response;
    }

    /**
     * Generate a unique key for the request.
     */
    public function requestKey(Request $request): string
    {
        return 'atomic_lock_middleware_' . $this->suffix($request);
    }

    /**
     * Handle request termination and release lock.
     */
    public function terminate(Request $request, Response $response): void
    {
        $this->releaseSingleRequest($request);
    }

    /**
     * Ensure that only one request is processed at a time by locking it.
     */
    private function ensureSingleRequest($request): void
    {
        // Check if the request is already locked
        if (Cache::has($this->requestKey($request))) {
            throw new HttpException(
                429, // HTTP status code for "Too Many Requests"
                __('Multiple duplicate request. Wait until the previous request is completed.')
            );
        }

        // Lock the request for 60 seconds
        Cache::put($this->requestKey($request), 'this signature has been consumed', 60);
    }

    /**
     * Release the lock after the request is processed.
     */
    private function releaseSingleRequest($request): void
    {
        Cache::forget($this->requestKey($request));
    }

    /**
     * Generate a unique prefix for the request based on URL and other parameters.
     */
    private function suffix(Request $request): string
    {
        $prefix = 'global';

        // You can add custom logic here to modify the suffix, such as user IDs or other identifiers

        return md5($request->url()) . '_' . $prefix;
    }
}
Enter fullscreen mode Exit fullscreen mode

Breaking Down the Middleware

Let's dive into each part of the code to understand how this middleware works:

1. Handling Requests

The handle method is the core of this middleware. It ensures that duplicate requests are blocked until the original one is processed:

public function handle(Request $request, Closure $next, $prefix = null): Response
{
    $this->ensureSingleRequest($request);

    $response = $next($request);

    $this->releaseSingleRequest($request);

    return $response;
}
Enter fullscreen mode Exit fullscreen mode

First, it calls the ensureSingleRequest method to check whether the request is already locked. If not, it processes the request normally. After the request is processed, it calls releaseSingleRequest to remove the lock from the cache.

2. Locking the Request

In the ensureSingleRequest method, we create a unique lock key for each request. If the request is already locked, the middleware throws an exception:

private function ensureSingleRequest($request): void
{
    if (Cache::has($this->requestKey($request))) {
        // Too Many Requests status code
        throw new HttpException(
            429,
            __('Multiple duplicate request. Wait until the previous request is completed.')
        );
    }

    Cache::put($this->requestKey($request), 'this signature has been consumed', 60);
}
Enter fullscreen mode Exit fullscreen mode

The lock is set to expire after 60 seconds, meaning if a request takes longer than that to process, the lock will automatically expire and allow new requests.

3. Generating Unique Keys

To ensure that each request gets its own lock, we generate a unique key using the request URL and a prefix / suffix:

public function requestKey(Request $request): string
{
    return 'atomic_lock_middleware_' . $this->suffix($request);
}
Enter fullscreen mode Exit fullscreen mode

This allows us to lock requests based on their URL, so two different URLs won't interfere with each other.

4. Releasing the Lock

Once the request is fully processed, we remove the lock from the cache, allowing new requests to be made:

private function releaseSingleRequest($request): void
{
    Cache::forget($this->requestKey($request));
}
Enter fullscreen mode Exit fullscreen mode

This keeps the system clean and ensures that the lock isn't held unnecessarily after the request is done.


How to Use the Middleware

To use this middleware, first, register it in your Kernel.php file:

Laravel 10

protected $routeMiddleware = [
    // Other middleware...
    ...
    'atomic.lock' => \App\Http\Middleware\AtomicLockMiddleware::class,
    ...
];
Enter fullscreen mode Exit fullscreen mode

Laravel 11

return Application::configure(basePath: dirname(__DIR__))
    ....
    ->withMiddleware(function (Middleware $middleware): void {
        .....

        $middleware->alias([
            ...
            'atomic.lock' => \App\Http\Middleware\AtomicLockMiddleware::class,
            ...
        ]);
    })
    ....
Enter fullscreen mode Exit fullscreen mode

You can then apply it to specific routes or controllers:

Route::post('/approve-order', [OrderApprovalController::class, 'submit'])->middleware('atomic.lock');
Enter fullscreen mode Exit fullscreen mode

This will ensure that only one request is processed at a time for the form submission.


Why You Should Use This Middleware

The AtomicLockMiddleware provides several benefits for your Laravel application:

  • Prevents double submissions: Avoid issues like duplicated form submissions, which can create duplicate records or even duplicate payments.
  • Ensures atomicity: Actions are guaranteed to only happen once at a time, ensuring data consistency.
  • Improves user experience: Users are prevented from accidentally submitting forms multiple times, which could otherwise cause confusion or errors.

This simple middleware adds a layer of protection against unwanted duplicate requests, helping your application run more smoothly.


Customising the Middleware

You can easily extend this middleware to fit your needs. Here are a few ideas:

  • Add user-specific locks: You could modify the prefix method to include the user's ID in the request key. This would allow each user to have their own lock for requests.
  • Custom lock expiration times: Depending on the type of request, you may want to adjust the expiration time of the lock. For example, you might set longer expiration times for actions that typically take more time to complete.

Conclusion

Basically, anything significant or sensitive that any Laravel application does needs to be handled by duplicated requests. In this case, the atomic lock middleware keeps it off so that every request would go ahead accordingly in an orderly, atomic fashion.

This little middleware can save you from duplicte form submissions, or any actions which written into your database, and so on, with just some lines of code. Put this lock on your routes for everything from the most simple app up to the most complex API.

Feel free to play around with it in your Laravel projects and understand how it simplifies the parallel requests handling for you!

This middleware offers a straightforward solution to a common problem, helping you ensure that no request gets processed twice unintentionally. By keeping things simple and atomic, your app will provide a more seamless experience for users and help you avoid messy data issues.


Thank you for reading! Don’t forget to subscribe to stay informed about the latest updates in system design. Happy designing!

If you found this article insightful and want to stay updated with more content on system design and technology trends, be sure to follow me on :-

Twitter: https://twitter.com/hafiqdotcom
LinkedIn: https://www.linkedin.com/in/hafiq93
Buy Me Coffee: https://paypal.me/mhi9388 /
https://buymeacoffee.com/mhitech
Medium: https://medium.com/@hafiqiqmal93

Top comments (1)

Collapse
 
cyrille37 profile image
Cyrille37 • Edited