Skip to content

Make WP install/DB checks resilient to post-flush stdout (fixes false-negative "Error connecting to the SQLite database")#3646

Draft
ivan-ottinger wants to merge 1 commit into
WordPress:trunkfrom
ivan-ottinger:ivan-ottinger/fix-bootstrap-result-check-strict-equality
Draft

Make WP install/DB checks resilient to post-flush stdout (fixes false-negative "Error connecting to the SQLite database")#3646
ivan-ottinger wants to merge 1 commit into
WordPress:trunkfrom
ivan-ottinger:ivan-ottinger/fix-bootstrap-result-check-strict-equality

Conversation

@ivan-ottinger
Copy link
Copy Markdown
Contributor

@ivan-ottinger ivan-ottinger commented May 15, 2026

Symptom

Running @wp-playground/cli server with --wordpress-install-mode=install-from-existing-files-if-needed against an imported WordPress site exits with:

Error: Error connecting to the SQLite database.

…on a site whose SQLite database is healthy (PRAGMA integrity_check returns ok, wp_options.siteurl is set, all WordPress tables are present). Switching only the --wordpress-install-mode flag to do-not-attempt-installing boots the same site cleanly, which rules out the site, the file mount, and the SQLite plugin as the cause.

Reproducer

# Fails with "Error: Error connecting to the SQLite database." (process exits)
npx -y @wp-playground/cli@3.1.28 server \
  --port=8902 --site-url=http://localhost:8902 \
  --mount=/path/to/site:/wordpress \
  --wordpress-install-mode=install-from-existing-files-if-needed

# Same site, only the install-mode changes — boots fine
npx -y @wp-playground/cli@3.1.28 server \
  --port=8901 --site-url=http://localhost:8901 \
  --mount=/path/to/site:/wordpress \
  --wordpress-install-mode=do-not-attempt-installing

We have a real-world Jetpack-backed site backup that reproduces this deterministically: https://linear.app/a8c/issue/PLAYGRD-653/bug-wordpress-install-modeinstall-from-existing-files-if-needed-fails

What I observed in the code

packages/playground/wordpress/src/boot.ts has two functions that each run a small PHP probe with ob_start() / ob_clean() / ob_end_flush(), then strictly compare the captured PHP stdout to "1":

  • isWordPressInstalled() — line 511, returns result.text === '1' on line 529
  • isDatabaseConnectionValid() — line 639, returns result.text === '1' on line 639

The user-facing Error: Error connecting to the SQLite database. is thrown when one of these checks returns false. In the failing reproducer above, that happens on a site where the underlying state we can independently inspect (database integrity, WP options, tables) is fine. So the boolean these functions return doesn't reflect the site state — something about the captured result.text makes the strict-equality check return false.

I did not pinpoint exactly what is in result.text in the failing run.

Status across versions

The same text === "1" lines are present in:

  • @wp-playground/wordpress@3.1.28
  • @wp-playground/wordpress@3.1.33 (latest published as of writing)

So the symptom is reproducible on both, and there's no parallel fix already shipped.

Proposed fix

Wrap the controlled output in a sentinel and match by .includes() instead of ===. This makes the check tolerant of any extra bytes that may end up alongside the '1'/'0'.

-			echo is_blog_installed() ? '1' : '0';
+			echo '<<PG-RESULT:' . ( is_blog_installed() ? '1' : '0' ) . ':END>>';
 			ob_end_flush();
 		`,
 		env: { DOCUMENT_ROOT: php.documentRoot },
 	});
-	return result.text === '1';
+	return result.text.includes('<<PG-RESULT:1:END>>');

Both call sites get the same change. The -1 (file-not-found) branch is also wrapped to keep the protocol uniform — both existing callers only check the boolean return, so behavior is unchanged for the negative case. packages/playground/wordpress/src/legacy-wp/legacy-boot.ts uses different probes and is unaffected.

Verification

Patched a local copy of @wp-playground/wordpress with this diff and re-ran the failing reproducer above. With the patch applied, the wp server starts cleanly and the site is reachable. Without the patch, the same command fails as described in the symptom section.

…-flush stdout

Both functions run a tiny PHP probe that uses ob_start()/ob_clean()/ob_end_flush()
to ensure only a controlled '1' or '0' reaches stdout, then JS compares the
captured text strictly against "1". This breaks for any site whose bootstrap
writes to stdout *after* ob_end_flush() — e.g. PHP shutdown handlers,
register_shutdown_function() callbacks, wp_cron's spawn_cron loopback writes,
or transport noise from wp_remote_post fallbacks (Action Scheduler's async
queue runner is a common trigger).

When the strict comparison fails, callers misinterpret the site as
"not installed" and trigger /wp-admin/install.php?step=2, then misinterpret
the post-install verification as "database disconnected" and surface
"Error connecting to the SQLite database" — a triply-misleading message
when the real cause is a single byte of trailing output.

Fix: wrap the controlled output in a sentinel ('<<PG-RESULT:1:END>>') and
match by .includes() rather than strict equality. Trailing post-flush bytes
no longer corrupt the result.

The legacy-boot.ts file uses different probes and isn't affected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant