Skip to content

[4.x] Fix Race Condition in Component Cache Writes#9833

Merged
calebporzio merged 1 commit intolivewire:mainfrom
cyppe:fix/atomic-cache-file-writes
Feb 8, 2026
Merged

[4.x] Fix Race Condition in Component Cache Writes#9833
calebporzio merged 1 commit intolivewire:mainfrom
cyppe:fix/atomic-cache-file-writes

Conversation

@cyppe
Copy link
Contributor

@cyppe cyppe commented Jan 26, 2026

The Problem

When multiple requests hit an uncached Livewire component at the same time, you get:

TypeError: Cannot use "::class" on int

This happens after fresh deployments when bots or users hit your site simultaneously.

Why It Happens

In CacheManager.php, all write methods used File::put():

File::put($classPath, $contents);

This is not atomic. When two requests try to compile the same component:

  1. Request A starts writing the file
  2. Request B overwrites it mid-write
  3. Request A reads the half-written file
  4. PHP returns 1 instead of the class instance
  5. $instance::class crashes

The Fix

Use File::replace() which writes to a temp file first, then renames atomically:

File::replace($classPath, $contents);

Rename operations are atomic on POSIX systems - the file is either fully there or not.

This fix applies to all cache write methods:

  • writeClassFile() - the critical one causing the crash
  • writeViewFile()
  • writeScriptFile()
  • writeStyleFile()
  • writeGlobalStyleFile()
  • writePlaceholderFile()
  • writeIslandFile()

Real Example

This error occurred in production with a footer component:

Cannot use "::class" on int (View: footer.blade.php)

The footer had <livewire:footer-links />. After deployment, Googlebot hit multiple pages simultaneously, triggering the race condition.

Use File::replace() instead of File::put() for atomic writes in all
cache write methods:
- writeClassFile()
- writeViewFile()
- writeScriptFile()
- writeStyleFile()
- writeGlobalStyleFile()
- writePlaceholderFile()
- writeIslandFile()

Without atomic writes, concurrent requests compiling the same component
can corrupt the cache file, causing "Cannot use ::class on int" errors.
File::replace() writes to a temp file then renames atomically.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@joshhanley joshhanley changed the title Fix: Race Condition in Component Cache Writes [4.x] Fix: Race Condition in Component Cache Writes Jan 29, 2026
@joshhanley joshhanley changed the title [4.x] Fix: Race Condition in Component Cache Writes [4.x] Fix Race Condition in Component Cache Writes Feb 5, 2026
@calebporzio
Copy link
Collaborator

PR Review: #9833 — [4.x] Fix Race Condition in Component Cache Writes

Type: Bug fix
Verdict: Merge

What's happening (plain English)

  1. Livewire compiles components into PHP class files and caches them to disk using File::put().
  2. File::put() wraps file_put_contents() — it writes directly to the target file, byte by byte.
  3. When two concurrent requests hit an uncached component (e.g. Googlebot swarming after a deploy), both try to write the same class cache file simultaneously.
  4. Request A starts writing. Request B overwrites mid-write. Request A then requires the half-written file.
  5. PHP's require on a truncated/corrupt file returns 1 (int) instead of the expected class instance.
  6. Line 60: $instance::class crashes with TypeError: Cannot use "::class" on int.

The fix: swap File::put()File::replace(). Laravel's File::replace() writes to a temp file first, then atomically renames it into place. rename() is atomic on POSIX — readers always see either the old complete file or the new complete file, never a partial write.

Other approaches considered

  1. File::put() with LOCK_EX — Would prevent concurrent writes, but LOCK_EX with file_put_contents is advisory on many systems and doesn't protect readers. Readers can still read a partially-written file.
  2. Locking with flock() on both read and write paths — Would work but requires modifying getClassName() too and adds complexity. Overkill when atomic rename solves it cleanly.
  3. File::replace() (this PR) — The standard atomic write pattern. Simplest, most robust, zero additional complexity. Clear winner.

Changes Made

No changes made. The PR is clean as-is.

Test Results

  • No test files in the diff (race conditions are inherently non-deterministic and hard to unit test reliably).
  • A mock-based test asserting File::replace() is called would be tautological — it would test the implementation, not the behavior.
  • CI: All 22 checks pass (Unit, Browser, Legacy across L11/L12, PHP 8.4/8.5).

Code Review

  • src/Compiler/CacheManager.php: All 7 write methods correctly updated from File::put()File::replace(). The change is consistent and complete.
  • The comment on writeClassFile() (lines 119-120) explains the "why" for the most critical case. The other methods don't need separate comments — the pattern is established.
  • mutateFileModificationTime() (called after some writes) works correctly with File::replace()rename() sets a fresh mtime, then touch() adjusts it.
  • No built assets in diff. No JS changes. No style issues.

Security

No security concerns identified. This is a safer pattern than what existed before.

Verdict

Clean, minimal, correct fix for a real production race condition. The contributor's diagnosis is accurate and independently verified. File::replace() is the right tool — it's Laravel's built-in atomic write primitive. Zero regression risk (same API surface, just atomic). No test needed — the bug is a timing issue that can't be reliably unit-tested, and the fix is trivially correct (one well-known API swap). All CI passes. Merge.


Reviewed by Claude

@calebporzio
Copy link
Collaborator

great. thanks!

@calebporzio calebporzio merged commit d0fd63b into livewire:main Feb 8, 2026
22 checks passed
cyppe added a commit to cyppe/framework that referenced this pull request Feb 14, 2026
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.
taylorotwell pushed a commit to laravel/framework that referenced this pull request Feb 14, 2026
* 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>
taylorotwell pushed a commit to illuminate/view that referenced this pull request Feb 14, 2026
* 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>
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