/** * THROWAWAY SPIKE (milestone MV): can headless Chrome reach a local HTTP server * in this environment? * * scripts/browser/README.md documents a sandbox mode where headless Chrome has * no network at all, including 127.0.0.1, and the symptom is indistinguishable * from a page boot failure. This probe answers the question in isolation before * any harness is built on the assumption. * * Diagnostic per the README: if `location.href === 'about:blank'` while the CDP * target list shows the right URL, it is the environment, not the page. * * Delete after the answer is recorded. */ import { spawn } from 'node:child_process' import { createServer } from 'node:http' const sleep = ms => new Promise(r => setTimeout(r, ms)) const MARKER = 'SPIKE_MARKER_8FA31C' async function main() { // 1. Plain node HTTP server, no vite, no build step. const server = createServer((req, res) => { console.log(`[server] hit: ${req.method} ${req.url}`) if (req.url === '/probe.json') { res.writeHead(200, { 'content-type': 'application/json' }) res.end(JSON.stringify({ marker: MARKER })) return } res.writeHead(200, { 'content-type': 'text/html' }) res.end(`${MARKER}
${MARKER}
`) }) await new Promise(r => server.listen(0, '127.0.0.1', r)) const port = server.address().port const baseUrl = `http://127.0.0.1:${port}` console.log(`[spike] server at ${baseUrl}`) // 2. Sanity: the server is reachable from node itself. const nodeResp = await fetch(`${baseUrl}/probe.json`) console.log(`[spike] node fetch -> ${nodeResp.status} ${JSON.stringify(await nodeResp.json())}`) // 3. Headless Chrome, same flags the existing harnesses use. const debugPort = 9333 const chrome = spawn('/usr/bin/google-chrome', [ '--headless=new', `--remote-debugging-port=${debugPort}`, '--no-sandbox', '--disable-dev-shm-usage', '--enable-webgl', '--ignore-gpu-blocklist', '--use-gl=angle', '--use-angle=swiftshader', '--window-size=800,600', 'about:blank', ]) chrome.stderr.on('data', d => { const s = String(d).trim() if (s) console.log(`[chrome stderr] ${s.slice(0, 300)}`) }) let verdict = 'UNKNOWN' try { let wsUrl = null for (let i = 0; i < 60; i++) { await sleep(200) try { const res = await fetch(`http://127.0.0.1:${debugPort}/json/list`) const pages = await res.json() const page = pages.find(p => p.type === 'page') if (page?.webSocketDebuggerUrl) { wsUrl = page.webSocketDebuggerUrl break } } catch { // CDP endpoint not up yet; this retry loop is the only tolerated swallow // and it is bounded, after which we throw below. } } if (!wsUrl) throw new Error('could not reach Chrome CDP endpoint at all') console.log(`[spike] CDP ws: ${wsUrl}`) const ws = new WebSocket(wsUrl) await new Promise(r => { ws.onopen = () => r() }) let id = 1 const send = (method, params = {}, timeoutMs = 15000) => new Promise((resolve, reject) => { const myId = id++ const timer = setTimeout(() => { ws.removeEventListener('message', handler) reject(new Error(`CDP ${method} timed out after ${timeoutMs}ms`)) }, timeoutMs) const handler = ev => { const msg = JSON.parse(String(ev.data)) if (msg.id === myId) { clearTimeout(timer) ws.removeEventListener('message', handler) if (msg.error) reject(new Error(`CDP ${method}: ${JSON.stringify(msg.error)}`)) else resolve(msg.result) } } ws.addEventListener('message', handler) ws.send(JSON.stringify({ id: myId, method, params })) }) const evalJs = async expression => { const res = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }) if (res.exceptionDetails) throw new Error(`JS error: ${JSON.stringify(res.exceptionDetails)}`) return res.result.value } await send('Page.enable') await send('Runtime.enable') await send('Network.enable') console.log('[spike] Page.navigate ...') let navErr = null try { await send('Page.navigate', { url: baseUrl }, 20000) console.log('[spike] Page.navigate returned') } catch (err) { navErr = err console.log(`[spike] Page.navigate FAILED: ${err.message}`) } await sleep(2000) const href = await evalJs('location.href') const title = await evalJs('document.title') const body = await evalJs('document.getElementById("m") ? document.getElementById("m").textContent : "(no #m)"') const fetchState = await evalJs('String(window.__spikeFetch)') console.log(`[spike] location.href = ${href}`) console.log(`[spike] document.title = ${title}`) console.log(`[spike] #m textContent = ${body}`) console.log(`[spike] in-page fetch = ${fetchState}`) const navOk = navErr === null && href.startsWith(baseUrl) && body === MARKER const fetchOk = fetchState === `ok:${MARKER}` verdict = navOk && fetchOk ? 'YES' : 'NO' if (!navOk && href === 'about:blank') { console.log('[spike] DIAGNOSIS: href is about:blank -> sandbox network block (README §已知限制)') } } finally { chrome.kill('SIGKILL') server.close() } console.log('======================================================================') console.log(`SPIKE VERDICT: headless Chrome can reach a local HTTP server = ${verdict}`) console.log('======================================================================') process.exit(verdict === 'YES' ? 0 : 1) } main().catch(err => { console.error(`[spike] FATAL: ${err.stack}`) console.log('SPIKE VERDICT: headless Chrome can reach a local HTTP server = NO (fatal)') process.exit(1) })