From afe584d942fb75643406e8b905c93d4c94ca70c4 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Fri, 24 Jul 2026 14:39:08 -0400 Subject: [PATCH] Handle cross-realm PHP-WASM failures --- patches/@wp-playground+cli+3.1.46.patch | 4 ++-- patches/README.md | 4 +++- tests/playground-worker-runtime-rejection.test.ts | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/patches/@wp-playground+cli+3.1.46.patch b/patches/@wp-playground+cli+3.1.46.patch index 606ec1212..99ac8c2ef 100644 --- a/patches/@wp-playground+cli+3.1.46.patch +++ b/patches/@wp-playground+cli+3.1.46.patch @@ -6,7 +6,7 @@ index 93b7099..65f76be 100644 } process.on("unhandledRejection", (r) => { S.error("Unhandled rejection:", r); -+ if (r instanceof WebAssembly.RuntimeError && String(r.stack).includes("php.wasm")) ++ if (r?.name === "RuntimeError" && String(r.stack).includes("php.wasm")) + throw r; }); const i = new _(), [c, p] = y( @@ -19,7 +19,7 @@ index fb79a59..77ddb67 100644 } process.on("unhandledRejection", (r) => { E.error("Unhandled rejection:", r); -+ if (r instanceof WebAssembly.RuntimeError && String(r.stack).includes("php.wasm")) ++ if (r?.name === "RuntimeError" && String(r.stack).includes("php.wasm")) + throw r; }); const i = new m(), [h, u] = k( diff --git a/patches/README.md b/patches/README.md index 1f03abd9a..694b2cb90 100644 --- a/patches/README.md +++ b/patches/README.md @@ -11,7 +11,9 @@ contains the same change. `@wp-playground+cli+3.1.46.patch` makes blueprint worker threads rethrow fatal `WebAssembly.RuntimeError` rejections originating from `php.wasm` after logging -them. Other unhandled rejections retain the released CLI's log-only behavior. +them. Detection uses the error name because PHP-WASM errors may cross JavaScript +realm boundaries where `instanceof` is false. Other unhandled rejections retain +the released CLI's log-only behavior. Without this narrow terminalization, a poisoned PHP-WASM worker remains alive and leaves the parent request pending until the recipe timeout. Remove the patch after the Playground CLI ships the same worker terminalization behavior. diff --git a/tests/playground-worker-runtime-rejection.test.ts b/tests/playground-worker-runtime-rejection.test.ts index 09a98edd6..a643eaa55 100644 --- a/tests/playground-worker-runtime-rejection.test.ts +++ b/tests/playground-worker-runtime-rejection.test.ts @@ -10,13 +10,14 @@ const preloadPath = join(root, "reject-on-command.mjs") await writeFile(preloadPath, ` import { parentPort } from "node:worker_threads" +import { runInNewContext } from "node:vm" parentPort?.on("message", (message) => { if (message === "wp-codebox-trigger-non-wasm-rejection") { Promise.reject(new Error("ordinary worker rejection")) setTimeout(() => parentPort?.postMessage("wp-codebox-worker-survived"), 25) } if (message === "wp-codebox-trigger-php-wasm-rejection") { - const error = new WebAssembly.RuntimeError("null function or function signature mismatch") + const error = runInNewContext('new WebAssembly.RuntimeError("null function or function signature mismatch")') error.stack = "RuntimeError: null function or function signature mismatch\\n at php.wasm.zif_mysqli_poll (wasm://wasm/php.wasm-05996276:wasm-function[12986]:0x9949a8)" Promise.reject(error) }