laravel/framework: Exception enhancements
Contributed by timacdonald
Part of the framework v13.35.0 release
Pull request #61791 polishes the exception reporting that Laravel Cloud's bootstrapper wires into the framework via Illuminate\Foundation\Cloud\ExceptionReporter. It does three things: it makes execution timestamps accurate under Laravel Octane, it consolidates the reporter's context-dehydration hooks behind a single, better-encapsulated method, and it adds a batch of regression tests that pin down exactly when the authenticated user's ID may — and may not — appear in an exception report. For most applications this lands as a transparent improvement; the visible changes are more accurate timestamps under Octane and the reclassification of two previously public methods as protected.
What changed
The reporter gained a prepareForOctaneRequest() method that snapshots the current time — (float) Date::now()->format('U.u') — into a new $octaneRequestReceivedAt property ("the time the Octane request was received"). The laravelStartedAtTimestamp() helper, which feeds the execution_context.timestamp attached to every report, now resolves the start of execution with a match (true) expression:
$microtime = match (true) {
$this->octaneRequestReceivedAt !== null => $this->octaneRequestReceivedAt,
defined('LARAVEL_START') => LARAVEL_START,
default => $_SERVER['REQUEST_TIME_FLOAT'],
};
The Octane request time wins when available, and the framework otherwise falls back to the exact same chain it used before: LARAVEL_START, then $_SERVER['REQUEST_TIME_FLOAT'].
Meanwhile, the bootstrapper used to register two separate ContextDehydrating listeners — one for the user ID, one for the trace ID. Both are now folded into a single call to a new handleContextDehydrating(ContextRepository $context) method, and rememberUserIdInContext() and rememberTraceIdInContext() were demoted from public to protected.
Why the Octane timestamp matters
Under a classic PHP-FPM setup, every request boots the framework from scratch, so LARAVEL_START and $_SERVER['REQUEST_TIME_FLOAT'] genuinely mark when the failing request began. Octane workers are different: a single worker process serves many requests across a long lifetime, and those values keep reflecting the moment the worker booted — potentially hours earlier. Exception reports emitted under Octane therefore carried a stale execution timestamp.
The fix hooks into Octane's own lifecycle. CloudBootstrapper::registerExceptionReporting() now listens for Laravel\Octane\Events\RequestReceived and calls prepareForOctaneRequest(), capturing the precise moment the request was handed to the worker. The new test demonstrates the payoff: it fakes $_SERVER['REQUEST_TIME_FLOAT'] to an hour in the past (simulating a long-lived worker), dispatches a RequestReceived event, triggers a 500 from a route, and asserts the reported execution_context.timestamp matches frozen "now" — not the stale boot time.
A tidier, stricter context API
The encapsulation half of the PR is small but worth knowing about if you touch this class directly:
// Before: two public methods, wired as two listeners
$reporter->rememberUserIdInContext($event->context);
$reporter->rememberTraceIdInContext($event->context);
// After: one public entry point
$reporter->handleContextDehydrating($event->context);
handleContextDehydrating() is the new public seam; the two remember*InContext() methods are implementation details behind it. Grouping the user and trace capture in one place also means future changes to context dehydration only need to touch a single method.
Proving user attribution stays scoped
The additional tests target a real hazard: the scheduler and Octane workers run many units of work inside one shared process, so if the reporter remembered a user ID too eagerly, an authenticated user could be blamed for exceptions thrown by entirely unrelated code. The new scheduled-task test runs a task that logs in, reports, logs out, then reports again:
$schedule->call(function () {
Auth::login($user);
report(new RuntimeException('While logged in!')); // user_id: abc123
Auth::logout();
report(new RuntimeException('Whoops!')); // user_id: null
})->everyMinute();
The assertions require the first write to carry user_id: 'abc123' and every subsequent write in the same schedule:run to carry user_id: null. A similar job test was strengthened to actually report an exception mid-job while logged in, verifying the ID is attributed during the job and cleared once the user logs out.
Upgrade impact
- Laravel Cloud + Octane: the improvement is automatic. The
RequestReceivedlistener is registered alongside the bootstrapper's other HTTP-scoped listeners, so no configuration or code changes are needed. - Direct callers of the reporter:
rememberUserIdInContext()andrememberTraceIdInContext()are nowprotected. If you called them from application code or a subclass, switch tohandleContextDehydrating()(or expose them via a subclass). - Everyone else: the report payload shape is unchanged; the only observable difference is a more accurate
execution_context.timestampunder Octane. Non-Octane environments keep theLARAVEL_START/REQUEST_TIME_FLOATfallback chain untouched.
Takeaways
- Exception reports under Octane are now timestamped from the moment the request was received, not when the worker process booted.
- Context dehydration is consolidated into
ExceptionReporter::handleContextDehydrating(); the tworemember*InContext()methods are now protected. - New tests lock in that user IDs don't leak across jobs, scheduled tasks, or logout boundaries in shared processes.
- No config changes are required; only code calling the previously public
remember*methods needs a one-line update.