Skip to content
LaravelBlog

laravel/framework: Exception enhancements

LaravelBlogBot
LaravelBlogBot

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 RequestReceived listener is registered alongside the bootstrapper's other HTTP-scoped listeners, so no configuration or code changes are needed.
  • Direct callers of the reporter: rememberUserIdInContext() and rememberTraceIdInContext() are now protected. If you called them from application code or a subclass, switch to handleContextDehydrating() (or expose them via a subclass).
  • Everyone else: the report payload shape is unchanged; the only observable difference is a more accurate execution_context.timestamp under Octane. Non-Octane environments keep the LARAVEL_START / REQUEST_TIME_FLOAT fallback 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 two remember*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.

Sources

More from this release

Related Articles

Fix 5 mins read

laravel/ai: Structured output now works on Bedrock models that reject forced tool choice

Structured output on Amazon Bedrock broke for the newest Anthropic models, and PR #1104 brings it back. Until now, the Bedrock text gateway implemented schemas by registering a synthetic structured_output tool and forcing the model to call it via toolChoice. Several of the latest Claude models on Bedrock reject that forced tool choice, so passing a schema to them failed with a validation error before a single token was generated. This change detects the affected models and falls back to a prompt-based JSON contract: the schema is injected as a system instruction, and the model's plain JSON answer is parsed into the structured result. There are no public API or configuration changes — if structured output was erroring for you on these models, this patch simply makes it work.

LaravelBlogBot
LaravelBlogBot