How does Laravel 13 turn an HTTP request into a response, from handleRequest() through the kernel and router to the controller and back?
answer
- kernel handle(), then send(), then terminate()
- bootstrappers run on first handle
- global middleware in a Pipeline
- Router::dispatch, route middleware, controller
- Router::toResponse turns return values into responses
basics
~20 shandleRequest() passes the request to the HTTP kernel, which bootstraps the app and runs global middleware into the router. The router runs route middleware and the controller, converts its return value into a response, and handleRequest() sends it, then terminates.
solid answer
~40 s`Application::handleRequest()` resolves the HTTP kernel (`Illuminate\Foundation\Http\Kernel`) and calls `handle()`. The kernel binds the request, runs its **bootstrappers** once (environment, configuration, exception handling, facades, provider registration, provider boot), then sends the request through a `Pipeline` of **global middleware** ending in `Router::dispatch()`. The router matches a route (firing `Routing` and `RouteMatched` events), sends the request through that route's **middleware**, runs the controller or closure, and converts whatever it returned with `Router::toResponse()`: a view or string becomes a `Response`, an array or model becomes a `JsonResponse`, a freshly created model becomes a 201. The response travels back out through route and global middleware. Any exception is caught in `handle()`, reported and rendered into a response. `handle()` fires `RequestHandled`, `handleRequest()` calls `send()`, then `terminate()`.
code
php · 10 lines<?php
use Illuminate\Support\Facades\Route;
// Each return value is converted by Router::toResponse()
Route::get('/events/{id}/ping', fn () => 'ok'); // text/html Response
Route::get('/events/{id}/stats', fn () => ['sold' => 120]); // JsonResponse
Route::post('/listings', function () {
return \App\Models\Listing::create(request()->only('title')); // 201 JsonResponse
});go deeper
Remember the order: kernel, middleware, router, controller, response back out through middleware, then send.
Name the bootstrappers, the two middleware pipelines, the router's events and how toResponse converts return values.
Use the walkthrough to place code correctly and to explain what happens to exceptions, RequestHandled and terminate on every path.
Decide which cross-cutting concerns belong at which stage across services, so teams do not scatter the same logic over providers, middleware and controllers.
## The shape of the journey In Laravel 13 a request is handled by three calls inside `Application::handleRequest()`: ```php $kernel = $this->make(HttpKernelContract::class); $response = $kernel->handle($request)->send(); $kernel->terminate($request, $response); ``` Everything interesting happens inside `handle()`, then `send()` writes the response, and `terminate()` runs work that should happen after it. ## Inside the kernel's `handle()` `Illuminate\Foundation\Http\Kernel::handle()`: 1. records `requestStartedAt` and enables HTTP method override (so a form's `_method` field can turn a POST into a PUT); 2. calls `sendRequestThroughRouter()`, which binds the request into the container as `request` and runs `bootstrap()`; 3. `bootstrap()` runs the **bootstrappers** if the application has not been bootstrapped yet: `LoadEnvironmentVariables`, `LoadConfiguration`, `HandleExceptions`, `RegisterFacades`, `RegisterProviders`, `BootProviders`; 4. sends the request through a `Pipeline` of the **global middleware**, whose last step is the router; 5. wraps all of that in `try`/`catch (Throwable)`: an exception is **reported** and **rendered** into a response instead of escaping; 6. dispatches the `RequestHandled` event with the request and response, and returns the response. ## Inside the router `Router::dispatch()` calls `dispatchToRoute()`: - `findRoute()` fires the `Routing` event and matches the request against the route collection; no match throws a not-found exception, which the kernel renders as a 404. - `runRoute()` attaches the route to the request and fires `RouteMatched`. - `runRouteWithinStack()` sends the request through the route's **middleware** (for example the `web` group) and, at the centre, runs the controller action or closure. - The return value goes through `prepareResponse()`, which calls `Router::toResponse()`. ## How return values become responses | Controller returns | Becomes | |---|---| | A `Response` or other Symfony response | Used as is | | A `Responsable` object | Its `toResponse($request)` result | | An array, `Arrayable`, `Jsonable` or `JsonSerializable` | `JsonResponse` | | An Eloquent model just created in this request | `JsonResponse` with status 201 | | A string or `Stringable` (including a rendered view) | `Response` with `Content-Type: text/html` | Finally `prepare($request)` adjusts the response to the request before it starts back out. ## Back out and finished - The response returns through the route middleware, then the global middleware, each of which can inspect or replace it. - `handleRequest()` calls `send()`, which writes the status line, headers and body. - `terminate()` then fires the `Terminating` event, calls `terminate()` on terminable middleware, runs callbacks registered with `app()->terminating()`, and finally any handlers registered for slow requests. ## Events you can hook along the way The lifecycle fires events at fixed points, which is useful for tracing and for cross-cutting behaviour: | Event | Fired by | Moment | |---|---|---| | `Routing` | `Router::findRoute()` | Before the route is matched | | `RouteMatched` | `Router::runRoute()` | After matching, before route middleware | | `PreparingResponse` | `Router::prepareResponse()` | Before a return value is converted | | `ResponsePrepared` | `Router::prepareResponse()` | After it has become a response | | `RequestHandled` | Kernel `handle()` | After the whole pipeline, before `send()` | | `Terminating` | Kernel `terminate()` | After `send()` | A listener on `RouteMatched` sees which route a ticket-resale request hit before its middleware runs; a listener on `RequestHandled` sees the final response, including one rendered from an exception. ## Why interviewers ask it The walkthrough tests whether you know **where** to put code: before bootstrapping nothing is configured; in a provider's `boot()` everything is registered; in middleware you see every request; after `send()` you can no longer change the response. What each middleware does, and how providers register and boot, are separate questions.
- Are the bootstrappers run again for every request in a PHP-FPM deployment?Yes in practice: under PHP-FPM each request starts a fresh PHP process state, builds a new application in `bootstrap/app.php`, and the kernel's `bootstrap()` runs the bootstrappers because the new instance has not been bootstrapped. The `hasBeenBootstrapped()` guard only matters when one application instance handles several requests, as with long-lived workers.
- Where does an exception thrown inside a controller end up in this flow?It unwinds through the route and global middleware pipelines to the `try`/`catch` in the kernel's `handle()`, which reports it and renders it into a response. `RequestHandled` still fires with that rendered response, `send()` still runs, and `terminate()` still runs, so the request always completes the lifecycle even when the controller fails.
saying these in an interview costs you the question
- Middleware runs only after the controller has finished
- Routes are matched before the service providers boot
- Returning an array from a controller is an error without response()
- An uncaught controller exception skips send() and terminate()
- Bootstrapping happens once per server, even under PHP-FPM