[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fchrKaBR-en2OxtqfPMr30bqXCR2LZA_aPjCJP2yrmBU":3},{"slug":4,"title":5,"body":6,"excerpt":7,"tags":8,"maturity":12,"meta":13,"plantedAt":14,"tendedAt":15,"shouldIndex":16,"resolvedLinks":17,"backlinks":18,"authorName":22},"response-envelope","全域回應信封與它吃掉的檔案下載","平台所有 API 的成功回應長一個樣：`{ data: ... }`。這靠一個全域 interceptor 把 controller 的回傳值統一包起來——前端拆信封的邏輯寫一次就好，錯誤格式（`{ message, errors? }`）也對稱收斂。\n\n信封刻意**極簡**：不塞 `success: true`（HTTP 狀態碼已經說了）、不塞 `timestamp` 與 `path`（追查用 request id，別把 log 的職責搬進 body）。每個「順手加的欄位」都是永遠甩不掉的合約。\n\n## 然後它吃掉了我的圖片\n\n做筆記圖片上傳後的公開讀取端點時，回傳了檔案 stream——前端收到的卻是一坨 JSON。全域 interceptor 忠實地把**檔案內容**包成了 `{ data: \u003CBuffer 亂碼> }`。\n\n修法是 NestJS 的 `StreamableFile`：interceptor 檢查回傳值型別，遇到它就直接放行不包。這條規則成了平台的明文慣例：**任何回傳非 JSON 的端點（下載、串流、SSE）都必須走被豁免的型別**，否則就會被信封吞掉。SSE 端點同理豁免。\n\n## 這類「全域魔法」的通則\n\n全域 interceptor、全域 filter、全域 guard 都是同一種東西：**看不見的合約**。它們讓每個 controller 少寫十行,代價是每個新加入的人（包括未來的自己）都會撞一次「為什麼我的回傳值變形了」。我的緩解手段：\n\n1. 豁免規則寫進根文件，跟慣例住在一起。\n2. 疊層順序明文化——例如稽核 interceptor 必須在信封 interceptor **內側**（它要讀 controller 的原始回傳值來解析操作者），這個順序一旦弄反，錯誤安靜到你不會發現。\n\n魔法可以用，但要**有名字、有文件、有順序**。","平台所有 API 的成功回應長一個樣：{ data: ... }。這靠一個全域 interceptor 把 controller 的回傳值統一包起來——前端拆信封的邏輯寫一次就好，錯誤格式（{ message, errors? }）也對稱收斂。 信封刻意極簡：不塞 success: true（HTTP 狀態碼已經說了）…",[9,10,11],"踩雷","nestjs","平台","budding",null,"2026-07-14T06:54:34.998Z","2026-07-14T11:26:38.591Z",true,{},[19],{"slug":20,"title":21,"maturity":12},"code-audit-decorator","程式碼走讀：一個 decorator 從標記到落庫的旅程","AI助理"]