Skip to content

[13.x] Make health route compatible with route caching across machines - #62049

Merged
taylorotwell merged 1 commit into
laravel:13.xfrom
percymamedy:fix/health-route-cacheable-view-path-13x
Oct 16, 2026
Merged

taylorotwell merged 1 commit into
laravel:13.xfrom
percymamedy:fix/health-route-cacheable-view-path-13x

Conversation

@percymamedy

Copy link
Copy Markdown
Contributor

The problem

The health route registered by withRouting(health: '/up') is a closure that renders its view by absolute path:

return response(View::file(__DIR__.'/../resources/health-up.blade.php', [...]), status: $status);

When routes are cached, route:cache serializes route closures with laravel/serializable-closure. Serializing a closure turns magic constants into literal strings, so __DIR__ is stored as the absolute path of the machine that ran route:cache. The cached route then looks like this:

View::file('/home/runner/work/app/app/.vapor/build/app/vendor/laravel/framework/src/Illuminate/Foundation/Configuration'.'/../resources/health-up.blade.php', ...)

Many deployments cache routes during a build step and run the application from a different path:

  • Laravel Vapor builds in .vapor/build/app (for example on a CI runner) and runs from /var/task
  • Docker multi-stage builds that run php artisan optimize in one stage and copy the app to another path
  • Any CI pipeline that builds an artifact and deploys it to a different directory

In all of these, every request to /up fails with:

Illuminate\Contracts\Filesystem\FileNotFoundException: File does not exist at path
/home/runner/work/app/app/.vapor/build/app/vendor/laravel/framework/src/Illuminate/Foundation/Configuration/../resources/health-up.blade.php.

The view file exists in the deployed application. It is only looked up at the build machine's path. Applications can't work around this without dropping health: or giving up route caching, because the closure lives inside the framework.

The fix

This PR moves the closure body, unchanged, into an invokable class, Illuminate\Foundation\Http\HealthCheck, and registers the route as:

Route::get($health, HealthCheck::class);

A controller route is cached as the string Illuminate\Foundation\Http\HealthCheck@__invoke instead of a serialized closure. __DIR__ inside the class is then evaluated at request time, on the machine actually serving the request, so the view always resolves. This follows the same approach as Illuminate\Filesystem\ServeFile, which the framework already uses for local storage serving.

Behaviour is otherwise identical: DiagnosingHealth is still dispatched, exceptions are still rethrown in debug mode and otherwise reported with a 500, and the JSON {"status": "up"|"down"} response is still returned when the request expects JSON.

I also considered registering the view under a view namespace (like laravel-exceptions) and rendering it by name. That makes view:cache compile everything in that directory, including the exception renderer views, and it broke OptimizeCommandTest. The invokable class avoids that and keeps the change self-contained.

Tests

test_health_route_can_be_cached_without_absolute_paths prepares the /up route for serialization, the same way route:cache does, and asserts that the cached action doesn't contain the framework's absolute src path. Without this change the test fails, and its output shows the absolute __DIR__ path inlined in the serialized closure. With the change it passes.

🤖 Generated with Claude Code

@taylorotwell
taylorotwell merged commit e51b5fe into laravel:13.x Oct 16, 2026
64 checks passed
@percymamedy
percymamedy deleted the fix/health-route-cacheable-view-path-13x branch October 17, 2026 07:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants