換了保險絲,燈還是不亮。以為是新保險絲壞的,再換一次,還是暗。最後才發現燈泡本身斷了。第一個問題是真的,只是它把更深的問題遮住了,直到移開它,第二個才浮出來。
技術環境
n8n 自架工作流程平台,Set 節點負責資料轉換與路徑組裝,下游串接物件儲存服務。節點以 typeVersion 欄位區分行為版本,工作流程 JSON 儲存於平台資料庫,修改透過 PATCH API 或 UI 操作進行。執行模式為事件驅動,workflow 啟動後節點依序執行,任一節點輸出異常不一定觸發頂層錯誤。
下游一直收到 400,但工作流沒有報錯
工作流跑起來沒有錯誤,日誌乾淨,但下游物件儲存服務持續回應 400。追進去,發現資料處理節點的設定在某次修改後被洗空了——輸出的儲存路徑變成了 undefined,物件儲存當然不收這種請求。
補回設定、重新啟動。400 還在。
這是第一次感覺到不對勁的時機點:修了一個明確的錯,問題沒有消失,代表這條鏈上還有別的東西。
錯誤傳染鏈(時序)
[工作流啟動]
|
v
Set node 執行 .............. ✓(節點顯示成功)
|
v
expression 求值 ............. ✗(v3.0 silent no-op,值從未算出)
|
v
輸出路徑 = undefined ........ ✗(第一個 bug 被補回後仍發生)
|
v
物件儲存 ← 400 ............. ✗(下游拒絕)
版本號差了 0.3,行為差了一整個 expression 引擎
再往裡追,看到這個節點的 typeVersion 是 3.0,同一條工作流裡其他同類節點全是 3.3。兩個版本在 UI 上長得一模一樣,差別在於 3.0 的 expression 求值邏輯是壞的——節點確實執行了,只是它從來沒有算出任何值,靜靜地輸出空白,不拋錯,不留痕跡。
把 typeVersion 從 3 改成 3.3,問題消失。
這就是疊加 bug 最難的地方:第一個 bug 是真實存在的,路徑設定確實被洗空了,修它沒有錯。只是修完之後,它原本遮住的第二個問題才有機會現身。如果第一個 bug 沒發生,下游的 400 可能會更早出現,反而更容易被發現。
Code 對照:修法前後
// 修改前:typeVersion 版本混用
{
"name": "Set 儲存路徑",
"type": "n8n-nodes-base.set",
"typeVersion": 3, // ← v3.0,expression 求值為 silent no-op
"parameters": {
"assignments": [
{
"name": "storagePath",
"value": "={{ $json.outputDir }}/{{ $json.filename }}"
// 上面這行在 v3.0 從未真正被求值
}
]
}
}
// 修改後:統一升至 3.3
{
"name": "Set 儲存路徑",
"type": "n8n-nodes-base.set",
"typeVersion": 3.3, // ← expression 求值正常
"parameters": {
"assignments": [
{
"name": "storagePath",
"value": "={{ $json.outputDir }}/{{ $json.filename }}"
}
],
"options": {
"includeOtherFields": true // v3.3 預設不傳上游欄位,需明確補回
}
}
}
側效應隔離:修完一個 bug 之後要問的七件事
- 問題是否真的消失,還是只是症狀轉移?修完後觀察的是下游實際行為,不是節點層的「成功」標誌。
- 同一條工作流裡有沒有版本不一致的同類節點?兩個版本外觀相同但行為不同,是最難被注意到的靜默差異。
- 節點「執行成功」是否等於「有產出值」?v3.0 的 silent no-op 會讓節點顯示成功,但輸出為空,這兩件事不是同一件事。
- 設定被洗空的原因是否已明確?補回設定是修復,但如果不知道為什麼被洗空,同樣的事會再發生。
- PATCH 操作是否真的生效?PATCH 回應成功不等於變更已被套用,需要重新拉取 workflow JSON 確認 typeVersion。
- upgradeOtherFields 的預設行為是否影響下游?v3.3 預設不傳上游欄位,若上游資料有其他節點依賴,需明確補
includeOtherFields: true。 - container 快取是否清除?版本切換後如果 task runner 快取未清,舊行為可能仍在,重發 webhook 前確認 container 狀態。
下次碰到類似情境,值得先確認的一件事
修完一個明確的錯誤之後,問題如果沒有消失,不要立刻假設修法本身有誤。比較值得先做的事,是在工作流 JSON 裡檢查所有同類節點的 typeVersion 是否一致——任何版本低於 3.3 的 Set 節點,都值得警惕,因為它的執行紀錄看起來乾淨,但 expression 可能從來沒有真正跑過。
兩個 bug 疊在一起的時候,第一個 bug 不只是一個問題,它同時也是第二個問題的掩護。
— 邱柏宇
延伸閱讀
Fix the First Bug, the Second One Appears
Replace the fuse, the light still doesn’t turn on. Replace it again thinking the new fuse was bad. Still dark. Eventually: the bulb itself is broken. The first problem was real — it just had a second problem hiding behind it.
Environment
Self-hosted n8n workflow platform. Set nodes handle data transformation and path assembly; downstream target is an object storage service. Node behavior is versioned via a typeVersion field stored in the workflow JSON. Workflows run event-driven, node by node. A node failing silently does not necessarily surface a top-level error.
Downstream kept returning 400, workflow showed no errors
The workflow ran clean. No errors in the logs. But the downstream object storage service kept responding 400. Traced into it: the data processing node’s configuration had been wiped in a previous edit — the output storage path had become undefined, which the storage service correctly refused.
Patched the config back. Restarted. Still 400.
That gap — fix applied, problem unchanged — was the first signal that something else was in the chain.
Error Propagation Sequence
[Workflow starts]
|
v
Set node executes .......... ✓ (node reports success)
|
v
expression evaluated ....... ✗ (v3.0 silent no-op, value never computed)
|
v
output path = undefined .... ✗ (persists even after config patch)
|
v
object storage ← 400 ....... ✗ (downstream rejects)
typeVersion 3.0 vs 3.3: identical appearance, broken expression engine
Deeper in the workflow JSON: this node’s typeVersion was 3.0. Every other Set node in the same workflow was 3.3. The two versions look identical in the UI. The difference is that v3.0’s expression evaluation logic is broken — the node runs, reports success, but never actually computes any value. Silent. No error. Empty output.
Changing typeVersion from 3 to 3.3 made the 400s stop.
This is the structure of stacked bugs: the first bug was real, the config wipe was real, fixing it was the right call. But fixing it was also what cleared the way for the second bug to surface. If the config had never been wiped, the 400s might have appeared earlier and been easier to catch.
Code Diff: Before and After
// Before: mixed typeVersion in same workflow
{
"name": "Set Storage Path",
"type": "n8n-nodes-base.set",
"typeVersion": 3, // ← v3.0: expression evaluation is a silent no-op
"parameters": {
"assignments": [
{
"name": "storagePath",
"value": "={{ $json.outputDir }}/{{ $json.filename }}"
// this expression was never evaluated
}
]
}
}
// After: upgraded to 3.3
{
"name": "Set Storage Path",
"type": "n8n-nodes-base.set",
"typeVersion": 3.3, // ← expression evaluation works
"parameters": {
"assignments": [
{
"name": "storagePath",
"value": "={{ $json.outputDir }}/{{ $json.filename }}"
}
],
"options": {
"includeOtherFields": true // v3.3 doesn't pass upstream fields by default
}
}
}
Seven things worth isolating after a bug fix that doesn’t resolve the symptom
- Is the problem actually gone, or just moved? Check downstream behavior directly, not node-level success flags.
- Are all same-type nodes in the workflow on the same version? Version drift is invisible in the UI — two nodes can look identical with different behavior.
- Does “node executed successfully” mean “values were produced”? In v3.0, these are different things.
- Is the root cause of the config wipe known? Patching the value back is a fix; not knowing why it was wiped means it will happen again.
- Did the PATCH actually apply? A 200 response from PATCH doesn’t confirm the change took effect — re-fetch the workflow JSON and verify
typeVersion. - Does
includeOtherFieldsneed to be set explicitly? v3.3 doesn’t forward upstream fields by default; any downstream node expecting them will break silently. - Is the task runner cache cleared? After a version change, stale cache can preserve old behavior — confirm container state before re-testing.
One thing worth checking next time
When a clearly-identified fix doesn’t make the problem go away, the first move isn’t to second-guess the fix. Pull the workflow JSON and check that all Set nodes share the same typeVersion. Any node at 3.0 is worth suspecting — its execution log will look clean, but the expressions may never have run.
In a stacked-bug situation, the first bug isn’t just a problem. It’s also cover for the second one.
— 邱柏宇
Related Posts
https://justfly.idv.tw/s/FL9cJQS