0

Terminable middleware — terminate() after response is sent

Advanced5 min read·lv-07-005
laravel-src

Concept

Terminable middleware can execute code AFTER the HTTP response has been sent to the client. This is enabled by the terminate() method on middleware classes.

How it works: After Laravel sends the response, it calls terminate(Request $request, Response $response) on all middleware that implement it. The client has already received the response — this code doesn't affect response time.

Use cases:

  • Request logging: Log the final response code, headers, and response body after the request completes.
  • Analytics: Record request metrics without slowing the response.
  • Session flushing: Write session data to storage after response delivery (this is what Laravel's StartSession middleware does in its terminate() method).
  • Cleanup: Release resources that were held during the request.

Important: terminate() is only called if the server handles shutdown properly (PHP-FPM with fastcgi_finish_request() called, or Octane). In some configurations, terminate() blocks the next request. Verify your server sends the response before terminate() runs.

PHP-FPM and fastcgi_finish_request(): PHP-FPM automatically calls fastcgi_finish_request() before cleanup functions, allowing terminate middleware to run after the response. Laravel's App\Http\Kernel::terminate() calls this.

Octane and terminable middleware: In Octane, terminate() runs between requests (after response sent, before next request). State from terminate() should be cleaned up carefully.

Code Example

php
<?php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

// Terminable middleware — logs after response is sent
class RequestLogger
{
    private float $startTime;

    public function handle(Request $request, Closure $next): Response
    {
        $this->startTime = microtime(true);
        return $next($request);
    }

    // Called AFTER response is sent to client
    public function terminate(Request $request, Response $response): void
    {
        $duration = round((microtime(true) - $this->startTime) * 1000, 2);

        logger()->info('Request completed', [
            'method'   => $request->method(),
            'path'     => $request->path(),
            'status'   => $response->getStatusCode(),
            'duration' => $duration . 'ms',
            'ip'       => $request->ip(),
            'user_id'  => auth()->id(),
        ]);
    }
}

// Real example: StartSession middleware (Laravel core)
// This is essentially what Laravel does:
class StartSession
{
    public function handle(Request $request, Closure $next): Response
    {
        $this->startSession($request);       // start session
        $response = $next($request);
        $this->storeCurrentUrl($request);    // save current URL in session
        $this->collectGarbage();             // GC expired sessions
        return $this->addCookieToResponse($response); // add session cookie
    }

    public function terminate(Request $request, Response $response): void
    {
        // Write session data to storage AFTER response is sent
        // This avoids blocking the response on session writes
        if ($this->sessionHandled) {
            $this->manager->driver()->save();
        }
    }
}

// Usage in routes — terminable middleware works just like normal middleware
Route::middleware(RequestLogger::class)->group(function() {
    Route::apiResource('orders', OrderController::class);
});