Fix site failing to start when it has a PHP error#2805
Open
Fix site failing to start when it has a PHP error#2805
Conversation
…ialog When a site has a PHP fatal error (in functions.php, plugins, etc.), Studio now starts the site and displays the PHP error in the browser instead of showing an error dialog. This allows users to see exactly what's wrong. The fix intercepts Playground CLI's process.exit(1) call and captures the actual PHP error output. The orphaned HTTP server is repurposed to serve an error page. A file watcher watches for .php changes and automatically restarts the server when the error is fixed.
Move pure error-handling functions (isPhpUserError, parsePhpError, generateErrorPageHtml, serveErrorPage) to apps/cli/lib/php-error-handling.ts and simplify output capture by using the existing global console/stdout interceptors instead of duplicating them inside runCLIWithoutExit.
When Playground fails before creating an HTTP server, the orphaned server is null. Previously, the file watcher started unconditionally and the function returned success — leaving nothing on the port and silently retrying with no user feedback. Now the watcher only starts when there is an orphaned server to serve the error page, and the PHP error is sent as a console-message so the parent/UI can display it. Without an orphaned server, the error is rethrown so the parent shows a dialog.
- Remove double-capture of stdout (console.log already captures to capturedBootOutput; the process.stdout.write interceptor was duplicating every entry) - Clear lastCapturedOutput after successful server recovery to free stale error output from memory - Clean up abortControllers record after message handling to prevent unbounded growth over the process lifetime
The cleanup commit incorrectly removed the stdout capture line, but Playground outputs PHP errors to stdout, not console.log. Without this, parsePhpError falls back to the generic message.
Collaborator
📊 Performance Test ResultsComparing 4c7db85 vs trunk app-size
site-editor
site-startup
Results are median values from multiple test runs. Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issues
How AI was used in this PR
AI assisted with drafting the implementation, exploring the Playground CLI internals, the feasibility of using
mount-onlymode, and reviewing the final code for reuse, quality, and efficiency issues.Proposed Changes
When a site has a PHP fatal error (eg.
this_function_does_not_exist()infunctions.php), the server child process crashes withprocess.exit(1)and the user sees a generic "Failed to start site" error dialog. This happens because Playground's internal error handler callsprocess.exit(1)directly in a.catch(), bypassing all try-catch blocks. The user has no way to know what went wrong or fix it without digging through logs.This PR catches PHP errors during startup and shows them in the browser instead:
runCLIWithoutExit()wrapper inwordpress-server-child.tsthat overridesprocess.exitto throw instead of exiting, so PHP fatal errors become catchable exceptions. It also interceptshttp.createServerto capture Playground's orphaned HTTP server, which is already bound to the port after a failed boot.watchForPhpChanges()file watcher that watches the site directory for.phpfile changes. When the user fixes the error, the watcher closes the orphaned server, attempts a full restart, and on success restores normal operation (blueprints applied, admin credentials set).apps/cli/lib/php-error-handling.tsmodule.uncaughtExceptionandunhandledRejectionhandlers to prevent stale async error callbacks from Playground's failed boot from crashing the child process.Other approaches I explored
I also investigated using Playground's
mount-onlymode as an alternative approach. The idea was to retry withmount-onlymode after a failed normal boot, skipping the WordPress boot (where the fatal error occurs) and starting the HTTP server with all site files mounted. Each browser request would then trigger fresh PHP execution, letting WordPress display errors naturally per-request, the same behavior you see when a fatal error occurs on an already-running server.However,
mount-onlyis a V2 runner concept only (worker-thread-v2.ts). The V1 worker that Studio currently uses silently ignores themodeparameter.This approach would be worth revisiting once the V2 runner ships and
mount-onlybecomes available.Testing Instructions
functions.phpfile and add an invalid function call likethis_function_does_not_exist();functions.phpCleanShot.2026-03-17.at.12.47.59.mp4
Pre-merge Checklist