Advanced Laravel

Advanced Middleware & HTTP Kernel

18 min Lesson 5 of 40

Understanding Laravel Middleware

Middleware provides a convenient mechanism for filtering HTTP requests entering your application. Beyond authentication and authorization, advanced middleware can perform complex operations, modify requests/responses, and implement cross-cutting concerns.

Common Middleware Use Cases:
  • Authentication and authorization
  • Request/response modification
  • Logging and monitoring
  • Rate limiting and throttling
  • CORS handling
  • Response caching
  • Request validation and sanitization
  • Feature flags and A/B testing

Creating Custom Middleware

Let's explore various types of custom middleware implementations.

Basic Custom Middleware:
// Generate middleware
php artisan make:middleware LogRequestMiddleware

// app/Http/Middleware/LogRequestMiddleware.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class LogRequestMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        // Before the request is handled
        $startTime = microtime(true);

        Log::info('Request started', [
            'method' => $request->method(),
            'url' => $request->fullUrl(),
            'ip' => $request->ip(),
            'user_agent' => $request->userAgent(),
        ]);

        // Handle the request
        $response = $next($request);

        // After the request is handled
        $duration = microtime(true) - $startTime;

        Log::info('Request completed', [
            'status' => $response->status(),
            'duration' => round($duration * 1000, 2) . 'ms',
        ]);

        return $response;
    }
}
Middleware with Parameters:
// app/Http/Middleware/CheckUserRole.php
namespace App\Http\Middleware;

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

class CheckUserRole
{
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        if (!auth()->check()) {
            return redirect('login');
        }

        $user = auth()->user();

        // Check if user has any of the required roles
        if (!in_array($user->role, $roles)) {
            abort(403, 'Unauthorized action.');
        }

        return $next($request);
    }
}

// Usage in routes
Route::get('/admin', [AdminController::class, 'index'])
    ->middleware('role:admin,super-admin');

Route::group(['middleware' => ['role:admin,editor']], function () {
    Route::resource('posts', PostController::class);
});
Request Modification Middleware:
// app/Http/Middleware/SanitizeInput.php
namespace App\Http\Middleware;

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

class SanitizeInput
{
    public function handle(Request $request, Closure $next): Response
    {
        // Sanitize all input
        $input = $request->all();

        array_walk_recursive($input, function (&$value) {
            if (is_string($value)) {
                // Remove XSS attempts
                $value = strip_tags($value);

                // Trim whitespace
                $value = trim($value);

                // Convert special characters
                $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
            }
        });

        // Replace request input with sanitized version
        $request->merge($input);

        return $next($request);
    }
}
Response Modification Middleware:
// app/Http/Middleware/AddSecurityHeaders.php
namespace App\Http\Middleware;

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

class AddSecurityHeaders
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        // Add security headers
        $response->headers->set('X-Frame-Options', 'SAMEORIGIN');
        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-XSS-Protection', '1; mode=block');
        $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
        $response->headers->set(
            'Content-Security-Policy',
            "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
        );

        return $response;
    }
}

Terminable Middleware

Terminable middleware performs work after the response has been sent to the browser, perfect for tasks that shouldn't delay the response.

Creating Terminable Middleware:
// app/Http/Middleware/LogActivityMiddleware.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;

class LogActivityMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }

    public function terminate(Request $request, Response $response): void
    {
        // This runs after the response is sent to the browser
        if (auth()->check()) {
            DB::table('activity_logs')->insert([
                'user_id' => auth()->id(),
                'action' => $request->method() . ' ' . $request->path(),
                'ip_address' => $request->ip(),
                'user_agent' => $request->userAgent(),
                'response_status' => $response->status(),
                'created_at' => now(),
            ]);
        }
    }
}
Terminable Middleware for Cache Warming:
// app/Http/Middleware/WarmCache.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;

class WarmCache
{
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }

    public function terminate(Request $request, Response $response): void
    {
        // Warm cache after response is sent
        if ($response->status() === 200 && $request->isMethod('GET')) {
            $cacheKey = 'page.' . md5($request->fullUrl());

            Cache::put($cacheKey, $response->getContent(), now()->addHour());
        }
    }
}
Performance Tip: Use terminable middleware for operations that don't need to complete before sending the response to the user, such as logging, cache warming, or analytics tracking.

Advanced Middleware Patterns

Let's explore some sophisticated middleware implementations.

Feature Flag Middleware:
// app/Http/Middleware/CheckFeatureFlag.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;

class CheckFeatureFlag
{
    public function handle(Request $request, Closure $next, string $feature): Response
    {
        // Check if feature is enabled
        $enabled = Cache::remember("feature.{$feature}", 3600, function () use ($feature) {
            return DB::table('feature_flags')
                ->where('name', $feature)
                ->value('enabled') ?? false;
        });

        if (!$enabled) {
            // Feature is disabled
            if ($request->expectsJson()) {
                return response()->json([
                    'message' => 'This feature is currently unavailable'
                ], 503);
            }

            abort(503, 'Feature temporarily unavailable');
        }

        return $next($request);
    }
}

// Usage
Route::get('/beta-feature', [BetaController::class, 'index'])
    ->middleware('feature:new-dashboard');
API Version Middleware:
// app/Http/Middleware/ApiVersion.php
namespace App\Http\Middleware;

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

class ApiVersion
{
    public function handle(Request $request, Closure $next, string $version): Response
    {
        // Store API version in request
        $request->attributes->set('api_version', $version);

        // Get response
        $response = $next($request);

        // Add version to response headers
        if ($response instanceof \Illuminate\Http\JsonResponse) {
            $response->header('X-API-Version', $version);
        }

        return $response;
    }
}

// Usage in routes
Route::prefix('v1')
    ->middleware('api.version:1.0')
    ->group(function () {
        Route::get('/users', [UserController::class, 'index']);
    });

Route::prefix('v2')
    ->middleware('api.version:2.0')
    ->group(function () {
        Route::get('/users', [UserV2Controller::class, 'index']);
    });
Request Signature Validation:
// app/Http/Middleware/ValidateRequestSignature.php
namespace App\Http\Middleware;

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

class ValidateRequestSignature
{
    public function handle(Request $request, Closure $next): Response
    {
        $signature = $request->header('X-Signature');

        if (!$signature) {
            return response()->json([
                'error' => 'Missing signature'
            ], 401);
        }

        // Get request body
        $body = $request->getContent();

        // Calculate expected signature
        $secret = config('app.webhook_secret');
        $expectedSignature = hash_hmac('sha256', $body, $secret);

        // Compare signatures
        if (!hash_equals($expectedSignature, $signature)) {
            return response()->json([
                'error' => 'Invalid signature'
            ], 401);
        }

        return $next($request);
    }
}

HTTP Kernel Configuration

The HTTP Kernel is the heart of your application's request/response cycle. Understanding how to configure it is crucial for advanced applications.

Understanding Kernel Structure:
// app/Http/Kernel.php
namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * Global middleware (runs on every request)
     */
    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,
    ];

    /**
     * Middleware groups (can be assigned to routes)
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

    /**
     * Route middleware (assigned individually to routes)
     */
    protected $middlewareAliases = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \App\Http\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,

        // Custom middleware
        'role' => \App\Http\Middleware\CheckUserRole::class,
        'feature' => \App\Http\Middleware\CheckFeatureFlag::class,
        'api.version' => \App\Http\Middleware\ApiVersion::class,
        'log.request' => \App\Http\Middleware\LogRequestMiddleware::class,
    ];
}

Middleware Priority

Laravel allows you to control the order in which middleware executes by defining priority.

Setting Middleware Priority:
// app/Http/Kernel.php
namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The priority-sorted list of middleware.
     *
     * Forces non-global middleware to always be in the given order.
     */
    protected $middlewarePriority = [
        \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
        \Illuminate\Cookie\Middleware\EncryptCookies::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
        \Illuminate\Routing\Middleware\ThrottleRequests::class,
        \Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class,
        \Illuminate\Contracts\Session\Middleware\AuthenticatesSessions::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \Illuminate\Auth\Middleware\Authorize::class,

        // Add custom middleware priority
        \App\Http\Middleware\LogRequestMiddleware::class,
        \App\Http\Middleware\CheckFeatureFlag::class,
    ];
}
Middleware Order Matters: The order of middleware execution can significantly affect your application's behavior. Authentication middleware should run before authorization, logging should typically run early or late (terminable), and response modification should happen after the main logic.

Advanced Middleware Techniques

Conditional Middleware:
// In routes/web.php
Route::get('/profile', [ProfileController::class, 'show'])
    ->middleware(function ($request, $next) {
        if ($request->user()->is_banned) {
            return redirect('/banned');
        }

        return $next($request);
    });

// Or create reusable conditional middleware
class ConditionalMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        if ($this->shouldSkip($request)) {
            return $next($request);
        }

        // Middleware logic here

        return $next($request);
    }

    protected function shouldSkip(Request $request): bool
    {
        return $request->is('api/*') || $request->ajax();
    }
}
Middleware Groups with Dynamic Loading:
// app/Providers/RouteServiceProvider.php
public function boot(): void
{
    $this->configureRateLimiting();

    $this->routes(function () {
        // API routes with dynamic middleware
        Route::middleware($this->getApiMiddleware())
            ->prefix('api')
            ->group(base_path('routes/api.php'));

        // Web routes
        Route::middleware('web')
            ->group(base_path('routes/web.php'));
    });
}

protected function getApiMiddleware(): array
{
    $middleware = ['api'];

    // Add rate limiting based on environment
    if (app()->environment('production')) {
        $middleware[] = 'throttle:60,1';
    }

    // Add API key validation for certain environments
    if (config('api.require_key')) {
        $middleware[] = 'api.key';
    }

    return $middleware;
}

Exercise 1: Create an API Rate Limiter

Build a custom rate limiting middleware that:

  • Limits requests per user (authenticated) or IP (guest)
  • Stores counts in Redis with expiry
  • Returns appropriate headers (X-RateLimit-Limit, X-RateLimit-Remaining)
  • Returns 429 status when limit exceeded
  • Accepts parameters for limits (e.g., ratelimit:100,60 = 100 requests per 60 seconds)

Exercise 2: Request/Response Logger

Create a terminable middleware that logs:

  • Request method, URL, IP, user agent
  • Request payload (sanitized, no passwords)
  • Response status code and size
  • Request duration in milliseconds
  • User ID if authenticated
  • Store logs in database or file, queued for performance

Exercise 3: Multi-Tenant Middleware

Build middleware for a multi-tenant application:

  • Extract tenant identifier from subdomain or header
  • Validate tenant exists and is active
  • Set database connection to tenant-specific database
  • Store tenant context in request for easy access
  • Handle missing or invalid tenant gracefully
Key Takeaways:
  • Middleware filters and modifies HTTP requests/responses
  • Terminable middleware runs after response is sent
  • Middleware can accept parameters for flexibility
  • HTTP Kernel controls global middleware, groups, and aliases
  • Middleware priority determines execution order
  • Use middleware for cross-cutting concerns (logging, auth, caching)
  • Keep middleware focused and single-purpose