Advanced Middleware & HTTP Kernel
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.
- 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.
// 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;
}
}
// 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);
});
// 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);
}
}
// 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.
// 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(),
]);
}
}
}
// 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());
}
}
}
Advanced Middleware Patterns
Let's explore some sophisticated middleware implementations.
// 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');
// 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']);
});
// 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.
// 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.
// 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,
];
}
Advanced Middleware Techniques
// 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();
}
}
// 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
- 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