Skip to content

Usage

Constructor

final class GuardMiddleware
{
    public function __construct(GuardEngine $engine) { ... }

    public function handle(Request $request, Closure $next): Response;
}

handle() is the Laravel middleware contract. The constructor calls $engine->initialize(); a GuardRedisException is swallowed only when SecurityConfig.redisFailOpen is true, otherwise construction fails closed by rethrowing. Block verdicts are translated by ResponseTranslator into an Illuminate\Http\Response with the verdict status, body, and headers.

Registration

In a Laravel application, register the middleware in the HTTP kernel (global stack or route middleware alias) and resolve the engine from the container:

// AppServiceProvider: singleton the engine so Octane workers reuse it
$this->app->singleton(GuardEngine::class, fn () => new GuardEngine(
    new SecurityConfig(/* ... */)
));

// bootstrap/app.php (Laravel 11+):
->withMiddleware(function (Middleware $middleware) {
    $middleware->append(GuardMiddleware::class);
})

The guard runs before the route callable; a block verdict means the route never executes.

Verdicts

When the engine returns a block verdict, the middleware returns the translated Illuminate\Http\Response and never calls $next:

Situation Status Body
Banned IP 403 IP address banned
Suspicious content 400 Suspicious activity detected
Rate limit exceeded 429 Too many requests plus Retry-After
Engine malfunction 500 Security check failed

Bodies can be overridden globally through SecurityConfig.customErrorResponses.

Fail-closed behavior

If the engine check throws, the middleware catches it and responds with the engine's fail-closed response (500 Security check failed) rather than letting the request through. A custom 500 body comes from customErrorResponses[500], not from adapter code.

Route-scoped configuration

The engine supports per-route RouteConfig (required headers, per-route rate limits, bypassed checks) keyed by a route ID on the request state. This adapter builds the engine request internally, so there is no route-ID hook; the engine-sanctioned equivalent is the customRequestCheck config closure, which runs as the last pipeline check and can return a block verdict for any request shape. See the advanced example app for an admin gate built that way.