8e9bd09072
真因:零件(main.go)給 host function 的接收緩衝區是固定大小(http_request 64KB、
claude_api 1MB)。回應超過這個大小時,wasi-shim 的 writeOut 照寫不誤:
new Uint8Array(buf, outPtr, data.length).set(data)
data 比零件的 outBuf 大 → 覆寫零件堆積體,零件接著 outBuf[:outLen] 切片 panic;
或 writeOut 撞 memory 邊界丟例外 → 回 1 → 零件印一句 "HTTP request failed"。
使用者照那句去查連線/URL/防火牆,方向全錯。
修法(容量握手,不改 host function 簽名、向後相容):
- 零件呼叫前把 outBuf 長度預先寫進 *outLenPtr(宣告容量)
- host 在寫回前讀這個值當上限;塞不下就**不寫**(不再覆寫零件記憶體),回新的
HOST_TOO_LARGE=3
- http_request host fn 收到 3 → 改寫一段講真話的 error envelope(實際大小+上限+
「不是連線失敗」+分頁/篩選的具體做法+機器可讀 code/actual_bytes/limit_bytes),
沿用既有 parsed["error"] 判定鏈原樣送到使用者面前
- 舊零件沒宣告容量(讀到 0)→ 維持舊行為。不可硬套 64KB 預設:各零件緩衝區大小不同,
硬套會把原本正常的大回應誤判成「太大」,那只是換一種說謊
同一條路徑上另一個「訊息與真因脫節」一併修:component-loader 的 makeHttpRunner
`try res.json() catch res.text()`,在零件回非 JSON 時 body 已被消費 → 丟
"Body has already been used",與真因無關(同檔 readBodyOnce 的註解早就寫明這個坑)。
改成只讀一次。
驗證狀態(誠實標示,mindset §7):
- 通:5 顆零件 tinygo build 全過,wasm 已重編進 .component-builds/
(claude_api 依 .gitignore 慣例不入庫,由部署端重編)
- 未跑:runtime 驗證。本 session 的權限層擋掉 node/vitest/wasmtime,
before/after 實測輸出待人跑 scripts/repro-oversize-response.mjs
- 未做:.worker-builds/ 重編(需 node scripts/build-worker-artifacts.mjs),
否則修法不會進 self-hosted 安裝路徑(Arcrun#93 同款陷阱)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
92 lines
3.4 KiB
JavaScript
92 lines
3.4 KiB
JavaScript
/**
|
||
* Arcrun#92 重現腳本 — 「回應太大」到底會讓使用者看到什麼訊息
|
||
*
|
||
* 為什麼要有這支:http_request 零件的接收緩衝區是 64 KB。回應超過這個大小時,
|
||
* 舊版會硬把資料寫進零件記憶體(寫爆)或讓 host 丟例外回 1,零件對外只講得出一句
|
||
* "HTTP request failed"。使用者照那句去查連線/防火牆/URL,方向全錯。
|
||
* 這支腳本把那個情境真的做出來,讓「修之前 / 修之後」的訊息可以並排比。
|
||
*
|
||
* 用法(本機,不碰任何線上實例):
|
||
*
|
||
* # 修之後(工作區現在的 wasm)
|
||
* node scripts/repro-oversize-response.mjs 200000
|
||
*
|
||
* # 修之前(把 main 上的舊 wasm 取出來當對照組;舊 wasm 不做容量握手 → 走舊路徑)
|
||
* git show origin/main:.component-builds/http_request/component.wasm > /tmp/old-http_request.wasm
|
||
* node scripts/repro-oversize-response.mjs 200000 /tmp/old-http_request.wasm
|
||
*
|
||
* # 對照:沒超過上限時兩者都應該正常
|
||
* node scripts/repro-oversize-response.mjs 1024
|
||
*
|
||
* Node < 22.18 請加 --experimental-strip-types(本檔會 import 一支 .ts)。
|
||
*/
|
||
import { readFileSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, resolve } from 'node:path';
|
||
|
||
const here = dirname(fileURLToPath(import.meta.url));
|
||
const repoRoot = resolve(here, '..');
|
||
|
||
const { createWasiShim } = await import(
|
||
resolve(repoRoot, 'cypher-executor/src/lib/wasi-shim.ts')
|
||
);
|
||
|
||
const responseBytes = Number(process.argv[2] ?? 200_000);
|
||
const wasmPath = process.argv[3]
|
||
? resolve(process.argv[3])
|
||
: resolve(repoRoot, '.component-builds/http_request/component.wasm');
|
||
|
||
// 假裝遠端回了一包很大的 JSON(2xx,host function 照原樣把 body 交給零件)
|
||
const filler = 'x'.repeat(Math.max(0, responseBytes - 14));
|
||
const remoteBody = JSON.stringify({ items: filler });
|
||
|
||
const shim = createWasiShim(
|
||
JSON.stringify({ url: 'https://example.com/big-list', method: 'GET' }),
|
||
{ http_request: async () => remoteBody },
|
||
);
|
||
|
||
const instance = await WebAssembly.instantiate(
|
||
await WebAssembly.compile(readFileSync(wasmPath)),
|
||
shim.imports,
|
||
);
|
||
shim.setMemory(instance.exports.memory);
|
||
|
||
let crashed = null;
|
||
try {
|
||
await shim.run(instance);
|
||
} catch (e) {
|
||
crashed = e instanceof Error ? e.message : String(e);
|
||
}
|
||
|
||
const stdout = shim.getStdout().trim();
|
||
const stderr = shim.getStderr().trim();
|
||
|
||
console.log(`wasm : ${wasmPath}`);
|
||
console.log(`模擬回應大小 : ${remoteBody.length} bytes(零件緩衝區上限 65536 bytes)`);
|
||
console.log('');
|
||
|
||
// 以下複刻 .component-builds/http_request/src/index.ts 的收尾,
|
||
// 印出「使用者真的會拿到的那一包」
|
||
if (stderr) console.log(`零件 stderr : ${stderr.slice(0, 300)}`);
|
||
if (crashed) console.log(`WASM 執行中止 : ${crashed}`);
|
||
|
||
if (!stdout) {
|
||
console.log('使用者看到 : HTTP 500 {"success":false,"error":"WASM component produced no output"}');
|
||
process.exit(0);
|
||
}
|
||
|
||
let parsed;
|
||
try {
|
||
parsed = JSON.parse(stdout);
|
||
} catch (e) {
|
||
console.log(`使用者看到 : HTTP 500 {"success":false,"error":"${e.message}"}`);
|
||
process.exit(0);
|
||
}
|
||
|
||
console.log(`success : ${parsed.success}`);
|
||
console.log(`error : ${parsed.error ?? '(無)'}`);
|
||
if (parsed.success) {
|
||
const body = parsed.data?.body ?? '';
|
||
console.log(`data.body 長度 : ${String(body).length} bytes`);
|
||
}
|