From a4c2f183e5929b8ee768d315bd7bc90ff91cb734 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Fri, 17 Jul 2026 05:32:17 -0300 Subject: [PATCH] fix(sse): lazy-load playwright in claudeTurnstileSolver (#7265) (#7566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Termux/Android's Node reports process.platform === 'android'. playwright-core's serverRegistry.js throws 'Unsupported platform: ' from a top-level IIFE at require time, so merely importing the playwright package crashed — no browser ever launched. claudeTurnstileSolver.ts was the only playwright consumer in the codebase with a static top-level import; every other call site (browserPool.ts, inAppLoginService.ts) already lazy-loads it. That static import is unconditionally reachable from the Next.js instrumentation hook on every boot via open-sse/executors/index.ts, so any unsupported platform crashed the whole server at startup regardless of which provider was configured. Fix: import type { Browser, Page } (erased at compile time) and move the chromium binding to a lazy await import("playwright") inside solveTurnstile(), matching the existing pattern. --- .../7265-termux-playwright-static-import.md | 1 + open-sse/services/claudeTurnstileSolver.ts | 6 ++- ...roid-playwright-static-import-7265.test.ts | 39 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/7265-termux-playwright-static-import.md create mode 100644 tests/unit/termux-android-playwright-static-import-7265.test.ts diff --git a/changelog.d/fixes/7265-termux-playwright-static-import.md b/changelog.d/fixes/7265-termux-playwright-static-import.md new file mode 100644 index 0000000000..93eaf69a0a --- /dev/null +++ b/changelog.d/fixes/7265-termux-playwright-static-import.md @@ -0,0 +1 @@ +- fix(sse): lazy-load playwright in claudeTurnstileSolver so unsupported platforms (e.g. Termux/Android) don't crash on boot (#7265) diff --git a/open-sse/services/claudeTurnstileSolver.ts b/open-sse/services/claudeTurnstileSolver.ts index c206edf563..2f3af01fa3 100644 --- a/open-sse/services/claudeTurnstileSolver.ts +++ b/open-sse/services/claudeTurnstileSolver.ts @@ -10,7 +10,7 @@ * 6. Returns fresh cookie for tls-client-node */ -import { chromium, type Browser, type Page } from "playwright"; +import type { Browser, Page } from "playwright"; const CLAUDE_WEB_URL = "https://claude.ai"; const CHALLENGE_TIMEOUT = 60000; // 60s to solve challenge @@ -80,7 +80,9 @@ export async function solveTurnstile(options?: { let page: Page | null = null; try { - // Launch headless browser + // Launch headless browser (lazy import — avoids crashing platforms + // playwright-core doesn't support, e.g. Termux/Android, on module load) + const { chromium } = await import("playwright"); browser = await chromium.launch({ headless }); const context = await browser.newContext({ userAgent: diff --git a/tests/unit/termux-android-playwright-static-import-7265.test.ts b/tests/unit/termux-android-playwright-static-import-7265.test.ts new file mode 100644 index 0000000000..da6cc858e8 --- /dev/null +++ b/tests/unit/termux-android-playwright-static-import-7265.test.ts @@ -0,0 +1,39 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +// Regression guard for #7265: on Termux/Android, `process.platform === "android"`. +// `playwright-core`'s serverRegistry.js throws `Unsupported platform: android` from a +// top-level IIFE at *require time* — merely importing the `playwright` package crashes, +// no browser needs to be launched. `claudeTurnstileSolver.ts` used to `import { chromium } +// from "playwright"` statically, and that module is unconditionally reachable from the +// Next.js instrumentation hook on every boot via open-sse/executors/index.ts, so any +// unsupported platform crashed the whole server at startup regardless of configured provider. +const HERE = dirname(fileURLToPath(import.meta.url)); +const SOLVER = join(HERE, "../../open-sse/services/claudeTurnstileSolver.ts"); + +test("claudeTurnstileSolver.ts does not statically import the playwright runtime", () => { + const src = readFileSync(SOLVER, "utf8"); + // Only a type-only import of playwright is allowed at module top level. + assert.doesNotMatch(src, /^import\s*\{\s*chromium[^}]*\}\s*from\s*"playwright"/m); + assert.match(src, /^import type \{ Browser, Page \} from "playwright";/m); + // The real chromium binding must come from a lazy dynamic import inside a function body. + assert.match(src, /const \{ chromium \} = await import\("playwright"\);/); +}); + +test("importing the real executor chain does not throw on an unsupported process.platform", async () => { + const originalDescriptor = Object.getOwnPropertyDescriptor(process, "platform")!; + Object.defineProperty(process, "platform", { value: "android", configurable: true }); + + try { + // This is the exact reachability chain from the Next.js instrumentation hook: + // instrumentation-node.ts -> open-sse/index.ts -> executors/index.ts -> claude-web*.ts + // -> claudeTurnstileSolver.ts. Before the fix, this threw + // "Unsupported platform: android" purely from the static playwright import. + await import("../../open-sse/executors/index.ts"); + } finally { + Object.defineProperty(process, "platform", originalDescriptor); + } +});