Skip to content

Use atomic writes in BladeCompiler to prevent race condition#58812

Merged
taylorotwell merged 2 commits intolaravel:12.xfrom
cyppe:fix/blade-compiler-atomic-view-writes
Feb 14, 2026
Merged

Use atomic writes in BladeCompiler to prevent race condition#58812
taylorotwell merged 2 commits intolaravel:12.xfrom
cyppe:fix/blade-compiler-atomic-view-writes

Conversation

@cyppe
Copy link
Contributor

@cyppe cyppe commented Feb 14, 2026

Problem

BladeCompiler::compile() uses $this->files->put() (which wraps file_put_contents()) to write compiled Blade views. In threaded PHP servers like FrankenPHP running hundreds of worker threads, concurrent first-requests can race on compiling the same view.

file_put_contents() truncates the target file to 0 bytes before writing new content. If another worker thread does include on that file during the truncate-write window, it receives empty output. This causes Livewire's insertAttributesIntoHtmlRoot() to throw RootTagMissingFromViewException because there is no root HTML tag to find in the empty string.

We observed exactly 2 of these exceptions after every Kamal deploy — one per web server — always on the first requests hitting each freshly-started container (typically from Googlebot).

Solution

Change the two $this->files->put() calls in BladeCompiler::compile() to $this->files->replace().

Filesystem::replace() writes to a temp file first and then uses rename(), which is atomic on POSIX systems. This ensures any concurrent reader always sees either the complete old content or the complete new content — never a truncated/empty file.

This is the same class of fix already applied to Livewire's CacheManager in livewire/livewire#9833, which changed File::put()File::replace() to solve an identical race condition.

Changes

  • src/Illuminate/View/Compilers/BladeCompiler.php: Changed both $this->files->put($compiledPath, $contents) calls to $this->files->replace($compiledPath, $contents) in the compile() method.

cyppe and others added 2 commits February 14, 2026 11:34
Change File::put() to File::replace() when writing compiled Blade
views. In threaded servers like FrankenPHP, concurrent requests can
race on compiling the same view: file_put_contents() truncates the
file to 0 bytes before writing, so another thread doing include()
during that window gets empty output. This causes Livewire's
insertAttributesIntoHtmlRoot() to throw RootTagMissingFromViewException
because there is no root HTML tag to find.

File::replace() uses a temp file + rename, which is atomic on POSIX
systems, ensuring readers always see either the old or new content.

This is the same class of fix applied to Livewire's CacheManager in
livewire/livewire#9833.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@taylorotwell taylorotwell merged commit a7e1f31 into laravel:12.x Feb 14, 2026
45 of 70 checks passed
DarkGhostHunter pushed a commit to DarkGhostHunter/laravel-framework that referenced this pull request Feb 22, 2026
…#58812)

* Use atomic writes in BladeCompiler to prevent race condition

Change File::put() to File::replace() when writing compiled Blade
views. In threaded servers like FrankenPHP, concurrent requests can
race on compiling the same view: file_put_contents() truncates the
file to 0 bytes before writing, so another thread doing include()
during that window gets empty output. This causes Livewire's
insertAttributesIntoHtmlRoot() to throw RootTagMissingFromViewException
because there is no root HTML tag to find.

File::replace() uses a temp file + rename, which is atomic on POSIX
systems, ensuring readers always see either the old or new content.

This is the same class of fix applied to Livewire's CacheManager in
livewire/livewire#9833.

* Update BladeCompiler tests to expect replace() instead of put()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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