Summary
BlazeManager::isFoldedExpired() (src/BlazeManager.php:291-299) performs a "check-then-read" on the compiled view file without guarding against the file disappearing between the two calls. If the compiled file is deleted between isExpired($path) and file_get_contents($compiled), PHP raises ErrorException("file_get_contents(...): Failed to open stream: No such file or directory"), producing a 500.
Version: livewire/blaze v1.0.11 on Laravel 12, PHP 8.4.
Offending code
https://github.com/livewire/blaze/blob/main/src/BlazeManager.php#L291-L299
$expired = $compiler->isExpired($path); // stat says "fresh"
$isExpired = false;
if (! $expired) {
$contents = file_get_contents($compiled); // file now gone → ErrorException
$isExpired = (new FrontMatter)->sourceContainsExpiredFoldedDependencies($contents);
}
Reproduction
- Long-running PHP-FPM workers render a Blade view through Blaze (populates PHP's stat cache).
- Something clears
storage/framework/views/ — e.g. php artisan view:clear or optimize:clear during a deploy.
- A concurrent request hits the race:
isExpired() returns false from the stat cache,
- the compiled file is already gone,
file_get_contents() throws.
- The failure is memoized per-worker in
\$this->expiredMemo[\$path], so the same worker keeps 500'ing for every subsequent request to that view until the worker cycles or the stat cache expires.
Why Blade doesn't hit this
`Illuminate\View\Engines\CompilerEngine` handles the missing-file case by (re)compiling when the compiled file is absent. Blaze's extra `file_get_contents` lookup does not.
Suggested fix
Either:
```php
// Option A: null-guard the read and treat missing as "compile again"
$contents = @file_get_contents($compiled);
if ($contents === false) {
$compiler->compile($path);
$contents = (string) file_get_contents($compiled);
}
```
```php
// Option B: drop the stat cache before the read
clearstatcache(true, $compiled);
if (! is_file($compiled)) {
$compiler->compile($path);
}
$contents = (string) file_get_contents($compiled);
```
Option A is closest to Blade's own pattern and requires no extra syscalls in the happy path.
Impact
Any deploy or ops action that clears the view cache under live traffic can produce sticky 500s on random Blade-rendered routes until PHP-FPM is reloaded.
Summary
BlazeManager::isFoldedExpired()(src/BlazeManager.php:291-299) performs a "check-then-read" on the compiled view file without guarding against the file disappearing between the two calls. If the compiled file is deleted betweenisExpired($path)andfile_get_contents($compiled), PHP raisesErrorException("file_get_contents(...): Failed to open stream: No such file or directory"), producing a 500.Version:
livewire/blazev1.0.11 on Laravel 12, PHP 8.4.Offending code
https://github.com/livewire/blaze/blob/main/src/BlazeManager.php#L291-L299
Reproduction
storage/framework/views/— e.g.php artisan view:clearoroptimize:clearduring a deploy.isExpired()returnsfalsefrom the stat cache,file_get_contents()throws.\$this->expiredMemo[\$path], so the same worker keeps 500'ing for every subsequent request to that view until the worker cycles or the stat cache expires.Why Blade doesn't hit this
`Illuminate\View\Engines\CompilerEngine` handles the missing-file case by (re)compiling when the compiled file is absent. Blaze's extra `file_get_contents` lookup does not.
Suggested fix
Either:
```php
// Option A: null-guard the read and treat missing as "compile again"
$contents = @file_get_contents($compiled);
if ($contents === false) {
$compiler->compile($path);
$contents = (string) file_get_contents($compiled);
}
```
```php
// Option B: drop the stat cache before the read
clearstatcache(true, $compiled);
if (! is_file($compiled)) {
$compiler->compile($path);
}
$contents = (string) file_get_contents($compiled);
```
Option A is closest to Blade's own pattern and requires no extra syscalls in the happy path.
Impact
Any deploy or ops action that clears the view cache under live traffic can produce sticky 500s on random Blade-rendered routes until PHP-FPM is reloaded.