
走進一家 7-11,找 iBon 機要印票,結果這家沒有。不是店員藏起來,是這台機器根本不在這裡。整個台灣 7-11 密度數一數二,但「這家有 iBon」不等於「每家都有」。走到櫃台,把需求一步一步講給工讀生聽,才完成了一件以為理所當然的事。
技術環境
n8n 的 Code 節點(Code Node),JavaScript 執行環境。n8n 以 vm2 或自訂 task runner 沙箱隔離執行,不是完整的 Node.js runtime。問題觸發模式:在 Code 節點頂層直接呼叫 fetch(),同步執行,n8n 版本 1.x 以上、Node.js 18+。問題模式與框架版本無關——任何依賴 Web API 全域物件(fetch、URL、FormData、Headers)而非 Node.js 核心模組的程式碼,在沙箱環境中都會複現相同行為。
自動化平台的 Code 節點裡寫 fetch(),直覺完全正確——Node.js 18 之後 fetch 是原生支援的,不需要引入任何東西。但執行到一半,ReferenceError: fetch is not defined。
不是 Node.js 壞了。是這裡根本不是完整的 Node.js。
沙箱暴露的是平台決定的全域物件
自動化平台的 Code 節點跑在沙箱環境裡。沙箱不是一個縮小版的 Node.js,它是平台自己決定要暴露哪些全域物件、哪些 API 可以用。fetch 沒有在這個清單裡,所以就是 undefined。
同樣的邏輯:URL 也是。n8n 的 task runner 沙箱不把 URL 放進 global scope,因為那屬於 Web API 全域,不是 Node.js 核心模組。直接寫 new URL(someString) 會撞到一樣的牆,ReferenceError: URL is not defined。解法是在 Code 節點頂部明確 require:const { URL } = require('url');,這是有效的,因為 url 是 Node.js 核心模組,沙箱有暴露。
問題的核心是:程式碼語法正確,runtime 環境卻不是你以為的那個。
錯誤傳染鏈(時序)
n8n Workflow Code Node Sandbox External API
| | |
|── 觸發 Code Node ───>| |
| | |
| fetch() ← ReferenceError: fetch is not defined
| 沙箱全域無此物件 |
| | |
|<── 執行中斷 ─────────| |
| (uncaught error) | |
| X (未送出任何請求) |
Workflow state: FAILED / External API: 完全沒收到請求
錯誤在沙箱初始化 fetch 那一刻就炸,外部 API 完全沒收到請求——不是請求失敗,是根本沒出去。
容易誤判的地方
第一時間的反應通常是懷疑版本——是不是這個平台跑的 Node.js 太舊?去查版本,發現是 18 以上,fetch 應該要有。這條路走下去什麼都查不到。
真正的分界點不在版本,在沙箱邊界。沙箱不繼承完整的 Node.js global,它的全域物件是平台 fork 出來另外定義的。同樣的程式碼,在本機跑沒問題,在平台的 Code 節點裡跑就爆掉——不是環境版本差異,是根本性的不同容器。
這才是反直覺的地方:語言一樣,語法一樣,但在不同容器裡的可用 API 集合不一樣。
怎麼繞過去
改用 https 模組。核心模組在沙箱裡是可用的。把 response 的 chunks 用 Buffer.concat() 收齊,再 JSON.parse,外面套 try/catch,加上 timeout 防止外部服務慢回應把整個 workflow 卡住。
步驟多了幾行,但機制明確。https.request 的行為是可以預期的,不依賴沙箱有沒有實作某個 Web API。
如果需要處理 URL 字串,就在頂部加 const { URL } = require('url');,不要假設它在 global 裡。確認方式很直接:在 Code 節點裡 console.log(typeof fetch),如果回傳 undefined,這個沙箱就是沒有 fetch,不用再往其他方向查。
Code 對照:修法前後
修法前(依賴 Web API fetch,沙箱無此全域)
// ❌ ReferenceError: fetch is not defined
const response = await fetch('https://api.example.com/data'); // ← 問題在這裡
const data = await response.json();
return [{ json: data }];
修法後(改用 Node.js 核心模組 https,沙箱有暴露)
const https = require('https');
const { URL } = require('url'); // URL 也不在 global,明確 require
function httpsGet(url, timeoutMs = 5000) {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const req = https.get({
hostname: parsed.hostname,
path: parsed.pathname + parsed.search,
timeout: timeoutMs,
}, (res) => {
const chunks = [];
res.on('data', chunk => chunks.push(chunk));
res.on('end', () => {
try {
resolve(JSON.parse(Buffer.concat(chunks).toString()));
} catch (e) {
reject(new Error('JSON parse failed: ' + e.message));
}
});
});
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
req.on('error', reject);
});
}
try {
const data = await httpsGet('https://api.example.com/data');
return [{ json: data }];
} catch (e) {
return [{ json: { error: e.message } }];
}
差異:fetch 是 Web API 全域,https 是 Node.js 核心模組。沙箱只暴露後者,改完後在任何 n8n 版本都能跑。
該被隔離的側效應類型
- Event Log / 審計記錄:Code Node 拋出 ReferenceError 後 workflow 中斷,後續的「寫入記錄」節點不執行,操作痕跡遺失。
- 推播通知:執行中斷代表下游推播節點不觸發,用戶端收不到任何反饋,也不會知道流程沒有完成。
- Webhook Callback:若 workflow 需要在完成後回 callback 給觸發方,中斷後 callback 不發出,觸發方一直等直到 timeout。
- 快取失效:若流程目的是更新資源後清快取,Code Node 失敗後快取不會清,下游系統讀到舊資料。
- 搜尋索引更新:若流程包含「抓取資料 → 索引」步驟,抓取中斷代表索引不更新,搜尋結果停留在舊版。
- Analytics / 成效追蹤:設計在完成時觸發的 analytics 事件節點(conversion、custom event)不執行,成效數據出現缺口。
- Async Queue / 任務派送:若 Code Node 是要把任務推進 Queue,中斷後任務不進 Queue,下游 worker 沒東西消費。
判斷標準:Code Node 以外任何依賴「這個節點正常完成」才能執行的節點,都是潛在受影響點——ReferenceError 不只是一個執行錯誤,是整條鏈從這裡往後全部中止的訊號。
留給下次的一件事
進任何新的自動化平台 Code 節點之前,先花三十秒確認全域物件的邊界,而不是假設它跟 Node.js REPL 一樣。這不是版本問題的 checklist,而是一個更根本的認知:Code 節點的執行環境,永遠是平台說了算。
— 邱柏宇
延伸閱讀
fetch is not defined — The Sandbox Is Not Node.js
Walk into a 7-11 looking for the iBon kiosk, and sometimes it just isn’t there. Not every branch has one. You assumed the service was universal. It isn’t. So you walk to the counter and explain your request step by step — one more layer of indirection you hadn’t planned for.
Writing fetch() inside an automation platform’s Code node feels just as natural. Node.js has supported native fetch since version 18, and once you’re used to that, you stop thinking about whether the environment actually has it. Then, mid-execution: ReferenceError: fetch is not defined.
Node.js isn’t broken. The sandbox simply isn’t Node.js.
Technical Environment
n8n Code node, JavaScript execution environment. n8n isolates code execution inside a vm2-based or custom task runner sandbox — not a full Node.js runtime. Problem trigger: calling fetch() at the top level of a Code node, synchronously, on n8n 1.x with Node.js 18+. The pattern is runtime-agnostic: any code that assumes Web API globals (fetch, URL, FormData, Headers) rather than Node.js core modules will hit the same wall in any sandboxed execution context.
The Sandbox Exposes What the Platform Decides
A Code node runs inside a sandbox. That sandbox is not a trimmed-down Node.js — it’s the platform’s own definition of which global objects and APIs are available. fetch isn’t on that list, so it’s undefined.
Same story with URL. The n8n task runner sandbox doesn’t put URL into global scope — it’s a Web API global, not a Node.js core module. Writing new URL(someString) hits the same wall: ReferenceError: URL is not defined. The fix is to explicitly require it at the top of the Code node: const { URL } = require('url');. That works because url is a Node.js core module, and the sandbox does expose core modules.
The code is syntactically correct. The runtime just isn’t what you assumed.
Error Propagation Sequence
n8n Workflow Code Node Sandbox External API
| | |
|── trigger Code ─────>| |
| | |
| fetch() ← ReferenceError: fetch is not defined
| (not in sandbox global) |
| | |
|<── execution error ──| |
| (uncaught) | |
| X (no request sent) |
Workflow state: FAILED / External API: received nothing
The error fires the moment fetch is referenced — the external API never receives a request. This isn’t a failed HTTP call; the call was never made.
Why It’s Easy to Misdiagnose
The first instinct is to check the Node.js version — maybe the platform is running something older than 18. Check the version. It’s 18 or above. fetch should be there. That path leads nowhere.
The real boundary isn’t the version number. It’s the sandbox edge. The sandbox doesn’t inherit a full Node.js global; its global object is defined separately by the platform. The same code runs fine locally and crashes in the Code node — not because of a version gap, but because it’s a fundamentally different container.
That’s the counterintuitive part: same language, same syntax, different available API surface depending on where the code runs.
The Workaround
Use the https module. Core modules are available in the sandbox. Collect response chunks with Buffer.concat(), then JSON.parse the result. Wrap it in try/catch and add a timeout — external services that respond slowly will otherwise stall the entire workflow.
More lines of code, but the mechanism is explicit and predictable. https.request doesn’t depend on whether the sandbox has implemented a particular Web API.
For URL handling, add const { URL } = require('url'); at the top. Don’t assume it’s in global scope. The quickest verification: console.log(typeof fetch) inside the Code node. If it returns undefined, the sandbox doesn’t have fetch — no further investigation needed in that direction.
Code Diff: Before and After
Before (relying on fetch Web API global — not available in sandbox)
// ❌ ReferenceError: fetch is not defined
const response = await fetch('https://api.example.com/data'); // ← problem here
const data = await response.json();
return [{ json: data }];
After (using Node.js core https module — available in sandbox)
const https = require('https');
const { URL } = require('url'); // URL isn't in global either — require explicitly
function httpsGet(url, timeoutMs = 5000) {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const req = https.get({
hostname: parsed.hostname,
path: parsed.pathname + parsed.search,
timeout: timeoutMs,
}, (res) => {
const chunks = [];
res.on('data', chunk => chunks.push(chunk));
res.on('end', () => {
try {
resolve(JSON.parse(Buffer.concat(chunks).toString()));
} catch (e) {
reject(new Error('JSON parse failed: ' + e.message));
}
});
});
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
req.on('error', reject);
});
}
try {
const data = await httpsGet('https://api.example.com/data');
return [{ json: data }];
} catch (e) {
return [{ json: { error: e.message } }];
}
The key difference: fetch is a Web API global; https is a Node.js core module. The sandbox exposes core modules, not Web API globals. The rewrite works across all n8n versions.
Side Effects That Should Be Isolated
- Event Log / Audit Trail: When the Code node throws ReferenceError, the workflow halts. Any downstream “write to log” nodes never execute — the operation leaves no trace.
- Push Notifications: Downstream notification nodes don’t trigger. The end user gets no feedback and never knows the workflow didn’t complete.
- Webhook Callback: If the workflow is supposed to call back to the system that triggered it, that callback never fires. The triggering system waits until timeout.
- Cache Invalidation: If the workflow’s job was to update a resource and then clear cache, the cache doesn’t clear. Downstream systems read stale data.
- Search Index Update: If the flow included a “fetch data → index” step, the index update doesn’t happen. Search results stay on the old version.
- Analytics Events: Analytics nodes designed to fire on completion (pageview, conversion, custom event) don’t get triggered. Data gaps appear in the metrics.
- Async Queue / Task Dispatch: If the Code node was supposed to push a job into a queue, the push never happens. Downstream workers have nothing to consume.
The rule: any node that depends on “this Code node completed successfully” is a potential downstream casualty. A ReferenceError isn’t just one execution error — it’s a full chain termination signal for everything downstream.
One Thing for Next Time
Before writing a single line in any new platform’s Code node, spend thirty seconds confirming what the global scope actually contains. Not as a checklist item, but as a baseline assumption reset: in a Code node, the execution environment is always what the platform says it is.
— 邱柏宇
Related Posts
https://justfly.idv.tw/s/fa2r4Yn