騎機車去一個地址,門牌號碼對了,但把「巷」跟「弄」的欄位填反了。導航說到達目的地,下車一看,是隔壁巷的人家。這種事在台灣街道系統裡不算少見——路→巷→弄→號,錯一層就差一條街。
API 也會這樣。
請求送出去,回應 200 OK
觸發 webhook,格式對,型別對,JSON schema 驗證通過,系統回傳已接受。看起來一切正常。但後續行為完全不對——流程跑的是預設環境,用的是預設設定,不是我指定的目標。
反覆確認 payload 結構。型別沒錯,值沒填錯,送出去也沒有 error。打開文件對照,參數應該傳的東西都傳了。可是系統就是不理我。
最後才發現:API 預期的參數名跟我寫的不一樣,只差了幾個字母。targetEnv 和 targetEnvironment,一個縮寫,一個全名。問題是 API 沒有拒絕我——它接受了這個請求,然後默默把那個不認識的 key 忽略掉,fallback 到預設值,一聲不吭地跑去叫了預設環境。
改一個字,立刻正確。
Silent fallback 的設計哲學
很多 API 設計者認為「寬鬆接受」是好事。不認識的參數?忽略。格式稍有偏差?補預設值。這樣系統不會因為小問題就中斷,聽起來很穩健。
但這對呼叫端來說是災難。你以為你在控制行為,其實系統在自作主張。你傳了參數,它不用;你指定了目標,它去找代班的。整個過程沒有任何警告,沒有任何 log 提示「你傳的參數我不認識」。
這種 bug 最難抓的地方不是哪裡報錯了,而是根本沒有報錯。系統表現得像是一切正常,只是結果不是你要的。你得自己去猜哪個環節被偷偷替換掉了。
型別系統也救不了命名
就算用 TypeScript 或 Go 這種有型別檢查的語言,如果你是透過 HTTP 送 JSON,到了對方手上還是字串比對。Compiler 保護不了你跨網路呼叫時的拼字錯誤。
有些團隊會用 code generation 從 OpenAPI spec 產生 client SDK,這樣至少參數名會強制一致。但如果對方文件本身就寫錯,或是文件跟實作不同步,你還是會踩坑。
更慘的是有些 API 根本沒有 spec,只有一份 Notion 文件,裡面範例程式碼的參數名跟正式環境的不一樣。你照著文件寫,結果 fallback 到預設值,然後花兩小時 debug 才發現文件過期了三個月。
strict mode 該是預設
好的 API 設計應該在收到不認識的參數時直接拒絕,或至少在 response 裡加一個 warnings 欄位告訴你「這些欄位我忽略了」。不要讓呼叫端猜。
如果你在寫 API,考慮加一個 strict flag。開啟時,任何非預期欄位直接回 400。如果你在呼叫 API,先用錯的參數測試看看會不會被擋——如果不會,那這個 API 的 error handling 可能不可靠。
改一個字母,從 fallback 到正確執行。這種問題的成本不是程式碼,是時間。
— 邱柏宇
延伸閱讀
When APIs Accept Your Typo and Call the Backup
You ride your scooter to an address. The street number is correct, but you swapped the “lane” and “alley” fields. The GPS says you’ve arrived. You park, look up—it’s the neighbor’s house, one block over.
APIs do this too.
Request sent, 200 OK returned
I triggered a webhook. Format correct, types correct, JSON schema validated, system returned accepted. Everything looked fine. But the subsequent behavior was completely wrong—it ran the default environment, used default settings, not the target I specified.
I double-checked the payload structure. Types correct, values not misspelled, no error on send. Opened the docs, compared line by line. All expected parameters were present.
Finally found it: the parameter name the API expected was different from what I wrote by a few characters. targetEnv versus targetEnvironment. One abbreviated, one full. The problem? The API didn’t reject my request—it accepted it, silently ignored the unrecognized key, fell back to the default value, and called the default environment without a word.
Changed one word. Immediately correct.
The philosophy of silent fallback
Many API designers believe “be liberal in what you accept” is a virtue. Unrecognized parameter? Ignore it. Format slightly off? Fill in defaults. This way the system won’t break over small issues. Sounds robust.
For the caller, it’s a nightmare. You think you’re controlling behavior, but the system is making its own decisions. You send a parameter, it doesn’t use it. You specify a target, it calls the backup. No warnings, no logs saying “parameter not recognized.”
The hardest part of this bug isn’t where it errors—it’s that it doesn’t error at all. The system acts like everything is normal, just that the result isn’t what you wanted. You have to guess which part got secretly replaced.
Type systems can’t save naming
Even with type-checked languages like TypeScript or Go, if you’re sending JSON over HTTP, it’s still string comparison on the other end. The compiler can’t protect you from typos in network calls.
Some teams use code generation from OpenAPI specs to produce client SDKs, which at least forces parameter name consistency. But if the spec itself is wrong, or the spec and implementation are out of sync, you still fall into the pit.
Worse are APIs with no spec at all, just a Notion doc with example code where parameter names differ from production. You follow the docs, fallback kicks in, you spend two hours debugging only to find the docs are three months stale.
Strict mode should be default
Good API design should reject unrecognized parameters outright, or at minimum include a warnings field in the response saying “these fields were ignored.” Don’t make callers guess.
If you’re writing an API, consider adding a strict flag. When enabled, any unexpected field returns 400. If you’re calling an API, test with wrong parameters first to see if they get blocked—if not, the error handling is probably unreliable.
One character changed, from fallback to correct execution. The cost of this problem isn’t code—it’s time.
— 邱柏宇