Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

jader's avatar
Level 4

Rate Limiter: Serialization of 'Closure' is not allowed

Hey guys,

I'm trying to integrate the https://github.com/artisansdk/ratelimiter package into my laravel application.

The package is a rate limiter that allows us to punish anyone who violates the request rate.

By default it generates a status code 429 when the limit is exceeded and I want to change this behavior by redirecting the request to a route where a captcha must be solved.

For that I created a middleware that extends from the ArtisanSdk\RateLimiter\Middleware class and put the redirection.

<?php

namespace App\Http\Middleware;

use ArtisanSdk\RateLimiter\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;

class Throttle extends Middleware
{
    public function handle($request, Closure $next, ...$args)
    {
        $resolver = $this->makeResolver($request, $args);

        $limiter = $this->configureLimiter($resolver);

        if ($limiter->exceeded()) {
            $limiter->timeout($resolver->duration());
            return redirect()->route('captcha')->with('limiter', $limiter);
        }

        $limiter->hit();

        $response = $next($request);

        return $this->addHeaders($response,
            $limiter->limit(),
            $limiter->remaining()
        );
    }
}

Now I need to send the limiter to the route, so I can easily do $limiter->reset() when the user solves the captcha, but I'm not able to send the limiter.

When I try to do return redirect()->route('captcha')->with('limiter', $limiter); I get exception Serialization of 'Closure' is not allowed.

Could you help me to get the $limiter on the route? Or suggest me another approach?

0 likes
4 replies
Sinnbeck's avatar

You cannot send a full object through to a redirect. Laravel needs to convert it a session header for the redirect. You will need to send only some identifier which you use to reconstruct it in the captcha page

jader's avatar
Level 4

@Sinnbeck Is there another way to get access to this object without recreating it?

Maybe pass it as a parameter to a global variable? I tried to do like this app()->instance('limiter', $limiter); and on route...

Route::get('captcha', function (Request $request) {
    dd(app('limiter'));
})->name('captcha');

but it didn't work.

I am trying to get this object instead of recreating it because in the package documentation there is no explanation on how to recreate the limiter without generating a new one.

Sinnbeck's avatar

@jader no its a completely new request, so I need to recreate it. So maybe the package has some identifier you can use?

jader's avatar
Level 4

@Sinnbeck Maibe my level of knowledge does not allow me to find such information. I even manage to get the key that goes to the bucket in memory, but when I try to create with this key it creates a new empty bucket and doesn't reset the previous one...

class Throttle extends Middleware
{
    protected string $key;

    public function handle($request, Closure $next, ...$args)
    {
        $resolver = $this->makeResolver($request, $args);

        $limiter = $this->configureLimiter($resolver);

        if ($limiter->exceeded()) {
            $limiter->timeout($resolver->duration());

            return redirect()->route('captcha')->with('key', $this->key);
        }

        $limiter->hit();

        $response = $next($request);

        return $this->addHeaders($response,
            $limiter->limit(),
            $limiter->remaining()
        );
    }

    protected function makeResolver(\Symfony\Component\HttpFoundation\Request $request, array $args = []): Resolver
    {
        $class = array_shift($args);
        if (is_null($class) || ! class_exists($class)) {
            array_unshift($args, $class);
            $class = $this->resolver;
        }

        $resolver = new $class($request, ...$args);
        if (! $resolver instanceof Resolver) {
            throw new InvalidArgumentException(get_class($resolver).' must be an instance of '.Resolver::class.'.');
        }

        $this->key = $resolver->key();

        return $resolver;
    }
}

In makeResolver method, I get the key and pass to $this->key

Now I have access to the key and can create a bucket, but, an initial bucket...

ArtisanSdk\RateLimiter\Buckets\Leaky {#1101 ▼
  #key: "1574bddb75c78a6fd2251d61e2993b5146201319"
  #max: 60
  #rate: 1.0
  #drips: 0
  #timer: 1675608068.8532

Please or to participate in this conversation.