The HTTP Kernel — handle(Request): Response flow
Concept
The HTTP Kernel is the orchestrator of a single HTTP request-response cycle. Its contract is beautifully simple: receive a Request, return a Response. Everything that happens in between — middleware pipeline execution, route matching, controller dispatch, exception handling — is the Kernel's responsibility to coordinate.
The Kernel interface defines two methods: handle(Request): Response and terminate(Request, Response): void. The handle() method runs synchronously during the request. terminate() runs after the response has been sent to the client — it is the place for cleanup work that should not delay the response: writing access logs, committing deferred database writes, garbage-collecting sessions.
The flow inside handle() is: wrap everything in a try/catch, send the request through the middleware pipeline, dispatch it to the router, get a response, return it. The try/catch ensures that any unhandled exception from controllers or middleware produces a proper HTTP response (404, 422, 500) rather than crashing the process. This is different from the global set_exception_handler() registered in fw-01-007 — the Kernel's try/catch is specifically for HTTP exceptions, while the global handler is for truly unexpected crashes.
The middleware pipeline wraps the core router dispatch in an onion of middleware layers. In fw-05 we will build the full pipeline; for now, the Kernel holds the middleware stack as an ordered array of class names and the pipeline system composes them. The key insight: middleware classes are resolved from the container, which means they can have constructor dependencies injected automatically.
Laravel's HTTP Kernel is Illuminate\Foundation\Http\Kernel. It holds three middleware arrays: $middleware (global, run on every request), $middlewareGroups (named groups like web and api), and $middlewareAliases (short names for individual middleware). The Kernel is registered in the service container by the FoundationServiceProvider, which is why app()->make(Kernel::class) works from index.php.
Code Example
<?php
declare(strict_types=1);
namespace Lumen\Http;
use Lumen\Foundation\Application;
/**
* The HTTP Kernel contract.
* Everything that handles an HTTP request implements this interface.
*
* Laravel equivalent: Illuminate\Contracts\Http\Kernel
*/
interface KernelInterface
{
public function handle(Request $request): Response;
public function terminate(Request $request, Response $response): void;
}
// ------------------------------------------------------------------
namespace Lumen\Foundation\Http;
use Lumen\Foundation\Application;
use Lumen\Http\KernelInterface;
use Lumen\Http\Request;
use Lumen\Http\Response;
use Lumen\Pipeline\Pipeline;
use Lumen\Routing\Router;
/**
* Concrete HTTP Kernel: runs middleware pipeline + router dispatch.
*
* Register in the container:
* $container->singleton(KernelInterface::class, Kernel::class);
*
* Laravel equivalent: Illuminate\Foundation\Http\Kernel
*/
class Kernel implements KernelInterface
{
/**
* Global middleware — run on EVERY request, before routing.
* Add classes like TrimStrings, VerifyCsrfToken here.
*
* @var list<class-string>
*/
protected array $middleware = [];
/**
* Middleware groups — named stacks applied to route groups.
*
* @var array<string, list<class-string>>
*/
protected array $middlewareGroups = [
'web' => [],
'api' => [],
];
public function __construct(
private readonly Application $app,
private readonly Router $router
) {}
/**
* Handle an incoming HTTP request.
*
* 1. Run global middleware pipeline (each layer wraps the next)
* 2. At the core of the pipeline: dispatch via the Router
* 3. Return the Response
*/
public function handle(Request $request): Response
{
try {
$response = $this->sendRequestThroughRouter($request);
} catch (\Throwable $e) {
$response = $this->renderException($request, $e);
}
return $response;
}
/**
* Perform post-response tasks (run after the response is sent).
*/
public function terminate(Request $request, Response $response): void
{
// Call terminate() on any middleware that implements it.
foreach ($this->middleware as $middlewareClass) {
$middleware = $this->app->getContainer()->make($middlewareClass);
if (method_exists($middleware, 'terminate')) {
$middleware->terminate($request, $response);
}
}
}
private function sendRequestThroughRouter(Request $request): Response
{
// Build the middleware stack as resolved instances.
// The pipeline is built in fw-05; here we stub it.
$middlewareStack = array_map(
fn(string $class) => $this->app->getContainer()->make($class),
$this->middleware
);
// Core handler: the router dispatches to the controller.
$coreHandler = fn(Request $req): Response => $this->router->dispatch($req);
// Wrap the core handler in the middleware stack.
// Full Pipeline implementation arrives in fw-05.
$pipeline = new Pipeline($this->app->getContainer());
return $pipeline
->send($request)
->through($middlewareStack)
->then($coreHandler);
}
private function renderException(Request $request, \Throwable $e): Response
{
// A production-ready implementation would resolve an ExceptionHandler
// from the container and call its render() method.
// For now, return a plain 500 response.
$debug = env('APP_DEBUG', false) === true;
$message = $debug ? $e->getMessage() : 'Internal Server Error';
$response = new Response(body: $message, status: 500);
$response->header('Content-Type', 'text/plain');
return $response;
}
}Interview Q&A
Q: Why does the Kernel have both handle() and terminate()? What would go wrong if you put post-response work inside handle()?
handle() runs synchronously — the HTTP connection stays open and the browser waits until handle() returns. Any work inside handle() adds to the user-perceived response time. Tasks like writing session data to disk, logging analytics events, or sending queued emails do not need to be done before the user sees the page. terminate() runs after $response->send(), meaning the browser has already received its bytes and the user sees the page immediately. The web server closes the connection, then PHP keeps running the terminate() code. This is the mechanism behind Laravel's "deferred" work: terminable middleware, dispatch()->afterResponse(), and $response->withCallback() all use the terminate() phase.
Q: How does the global exception handler in HandleExceptions relate to the Kernel's try/catch?
They are two different defense layers. The Kernel's try/catch (inside handle()) is the first responder for HTTP exceptions — it converts NotFoundHttpException into a 404 response, ValidationException into a 422, etc. It has full context about the request and can produce a proper HTTP response. The global set_exception_handler() from HandleExceptions is the last resort for exceptions that somehow escape even the Kernel — for example, exceptions in the bootstrap sequence before the Kernel starts, or exceptions thrown during terminate(). It produces a raw error response with no HTTP Kernel infrastructure. In practice, if the global handler fires, something has gone seriously wrong with the framework itself.
Q: Laravel's Kernel has $middleware, $middlewareGroups, and $middlewareAliases. Why three separate arrays?
They serve different scoping purposes. $middleware (global) runs on every single request — security headers, CORS, session starting. You want exactly this behaviour for things like TrustProxies which must run before routing so that $request->ip() is correct. $middlewareGroups are named stacks (web, api) applied to groups of routes — web includes sessions and CSRF, api does not. $middlewareAliases are short names like 'auth' for Authenticate::class used in route definitions: Route::middleware('auth'). This layering gives you global-by-default with opt-out (via WithoutMiddleware in tests), group-level control, and per-route control — the three levels of granularity you need in a real application.