← jordandalton.com

Advanced Laravel: Terminable Middleware

Terminable middleware can be useful for tasks such as logging, cache management, or any other cleanup tasks that need to be performed after the response has been sent. In this blog post, I'll show you how to create a simple HTTP request/response logger.

If you're a Laravel developer, you're likely familiar with the concept of middleware. These powerful tools allow you to manipulate incoming requests before they're handled by your application, and outgoing responses before they are sent back to the client. But what if you need to perform some action at the end of a request cycle, after the response has been sent? That's where terminable middleware comes in.

Terminable middleware can be useful for tasks such as logging, cache management, or any other cleanup tasks that need to be performed after the response has been sent. In this blog post, I'll show you how to create a simple HTTP request/response logger. Let's get started.

Creating the middleware

First, let's create our middleware class via Artisan:

php artisan make:middleware TerminatingMiddleware

This will place a file, TerminatingMiddleware.php, in our app/Http/Middleware folder. The contents are that of a basic middleware file:

<?php

namespace App\Http\Middleware;

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

class TerminatingMiddleware
{
    /**
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }
}

To make this a terminable middleware we'll add the terminate() method to the class, along with a logging point so we can see when it fires:

/**
 * Handle tasks after the response has been sent to the browser.
 */
public function terminate(Request $request, Response $response): void
{
    \Log::info('terminate', $request->toArray());
}

To make our application aware of this middleware we'll need to add it to Laravel's HTTP Kernel, particularly in the $middleware property where global middleware are defined (these are executed during every request to the application):

protected $middleware = [
    // \App\Http\Middleware\TrustHosts::class,
    \App\Http\Middleware\TrustProxies::class,
    \Illuminate\Http\Middleware\HandleCors::class,
    \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
    \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
    \App\Http\Middleware\TrimStrings::class,
    \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    \App\Http\Middleware\TerminatingMiddleware::class,
];

Now that Laravel knows to use our middleware, let's test it by visiting the application's homepage in the browser:

http://example.test/

We should now have a log entry:

[2023-04-12 09:58:55] local.INFO: terminate

Building the request tracker

So far so good. We'll now begin building our tracking system by using Artisan to create a model along with a corresponding migration file:

php artisan make:model HttpRequest -m

Let's design our migration to capture the desired data points:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('http_requests', function (Blueprint $table) {
            $table->id();
            $table->string('session_id')->index();
            $table->integer('user_id')->index()->nullable();
            $table->string('ip')->index();
            $table->boolean('ajax')->index();
            $table->string('url');
            $table->jsonb('payload');
            $table->integer('status_code')->index();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('http_requests');
    }
};

We'll now move over to our model and define fillable fields and casts:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class HttpRequest extends Model
{
    use HasFactory;

    protected $casts = [
        'payload' => 'collection', // very important
    ];

    protected $fillable = [
        'session_id',
        'user_id',
        'ip',
        'ajax',
        'url',
        'payload',
        'status_code',
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Let's execute the migration:

php artisan migrate

Now let's go back to our middleware and update it to populate the database:

<?php

namespace App\Http\Middleware;

use App\Models\HttpRequest;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class TerminatingMiddleware
{
    /**
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }

    /**
     * Handle tasks after the response has been sent to the browser.
     */
    public function terminate(Request $request, Response $response): void
    {
        $data = [
            'session_id'  => session()->getId(),
            'user_id'     => $request->user()->id ?? null,
            'ip'          => $request->ip(),
            'ajax'        => $request->ajax(),
            'url'         => $request->fullUrl(),
            'payload'     => $request->toArray(),
            'status_code' => $response->getStatusCode(),
        ];

        \App\Models\HttpRequest::create($data);
    }
}

If we now re-visit our homepage with some query string information:

http://example.test?jordan=dalton

We should now have data in our database:

> App\Models\HttpRequest::first()
= App\Models\HttpRequest {#6918
    id: 1,
    session_id: "aHDgiFIU3JklAjF0AGOf3GGMNMbWyLMnwCtZ9kjH",
    user_id: null,
    ip: "127.0.0.1",
    ajax: 0,
    url: "http://terminable.test/?jordan=dalton",
    payload: "{"jordan": "dalton"}",
    status_code: 200,
    created_at: "2023-04-12 10:12:17",
    updated_at: "2023-04-12 10:12:17",
  }

Now that we have this working, you could take it even further by dispatching a queued job inside terminate() to handle the database write asynchronously. The overall beauty of this solution is you can see the requests and responses throughout your system in realtime, which also makes debugging much easier.

Conclusion

As you continue to develop your Laravel applications, keep terminable middleware in mind as a valuable tool for improving performance and functionality. With a little bit of planning and some creative thinking, you can use terminable middleware to take your applications to the next level.

Thanks for reading, and happy coding!


Source code is available on GitHub. Follow me on Twitter and YouTube.

Enjoyed this?

Book a build day.

Take the ideas in here from concept to working software in one focused day.

Book a build day →
jordandalton.com — © 2026 Jordan DaltonBuilt in a day, naturally.