/**
* 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)
})