前言
之前在正式環境遇到一個棘手的問題:使用者回報某支 API 回傳 HTTP 200,瀏覽器卻直接噴出 ERR_CONTENT_DECODING_FAILED。當下排查了很久才發現問題出在 Response Header 中多了一個不該出現的 gzip,特別記錄一下整個排查過程。
問題現象
打開 DevTools 檢查異常 API 的 Response Headers,可以看到幾個關鍵資訊:
Content-Encoding: gzip— 伺服器宣稱回傳內容經過 gzip 壓縮Transfer-Encoding: chunked— 資料以分段方式傳輸X-Powered-By: ASP.NET— 請求可能直接打到後端 .NET 伺服器,或經過了某層 Proxy 轉發
問題核心:空 Body + gzip 標頭
瀏覽器收到 Content-Encoding: gzip 後,會自動嘗試用 gzip 解碼器解壓內容。但實際上這支 API 的 Response Body 是空的(長度為 0),解碼器無法處理一個空的壓縮流,因此直接報出 ERR_CONTENT_DECODING_FAILED。
為什麼 Body 會是空的?根據 Header 中的 X-Powered-By: ASP.NET,可能有兩種原因:
- 後端程式異常中斷 — ASP.NET 拋出 Exception 後沒有回傳 500,而是直接結束了 Response,導致 Body 為空。
- 查詢超時 — 後端資料庫查詢耗時過久,連線中斷,最終只傳回了 Headers 就結束了。
關鍵線索:對比正常與異常的 API
進一步對比同專案中兩支 API 的 Response Headers,差異一目了然:
| 欄位 | 異常 API | 正常 API |
|---|---|---|
| Content-Encoding | gzip | (無) |
| X-Powered-By | ASP.NET | (無) |
| X-Aspnet-Version | 4.0.30319 | (無) |
正常的 API 回傳的 Headers 很乾淨,是經過 Nuxt BFF(Node.js)處理後回傳的;而異常的 API 卻直接暴露了 ASP.NET 的資訊。這代表:
- 情況 A:這支 API 繞過了 Nuxt BFF,直接打到了後端 .NET 伺服器。
- 情況 B:後端 .NET 伺服器強制啟動了 gzip 壓縮,但因資料量過大或超時導致傳輸中斷,最終變成「空 Body + gzip 標頭」的組合。
真相:Nitro 透傳 Response 物件導致 gzip 標頭外洩
後端伺服器其實沒有噴錯,它正常回傳了資料(只是內容為空陣列或空物件)。真正的問題出在 Nuxt Server(Nitro)端的 API Handler 寫法。
檢查 Server 端程式碼後,發現異常的 API Handler 是這樣寫的:
1 | const response = await api.memberGetMemberPriorityHistories(query); |
這裡 api.memberGetMemberPriorityHistories() 回傳的是一個繼承自原生 Response 的物件。當直接 return response 時,Nitro 引擎會將其視為透傳(Proxy),把後端的所有原始標頭原封不動地轉發給瀏覽器。
整個流程是這樣的:
- 後端 ASP.NET 回傳帶有
Content-Encoding: gzip的壓縮內容 - Nuxt Server(Node.js)在接收時已經自動解壓了 gzip 內容
- 但因為直接
return response,Nitro 把後端的原始 Headers(包含Content-Encoding: gzip)照搬給瀏覽器 - 瀏覽器收到
Content-Encoding: gzip標頭,嘗試解壓 Body —— 但 Body 早就被 Node.js 解壓過了,已經不是 gzip 格式 - 解碼失敗,噴出
ERR_CONTENT_DECODING_FAILED
簡單來說:gzip 被 Nuxt Server Side 解開了一次,但 gzip 標頭卻還在,瀏覽器又試圖解第二次,自然就失敗了。
這也解釋了為什麼對比表中異常 API 會帶有 X-Powered-By: ASP.NET 和 X-Aspnet-Version — 這些都是後端的原始標頭被透傳出來的結果。
修正方式
根因找到後,修正方式很明確:不要直接回傳整個 Response 物件,改為只回傳 response.data。 這樣 Nitro 會重新封裝成乾淨的 JSON Response,由 Nuxt 統一處理壓縮,後端的 ASP.NET 標頭就不會外洩。
修改前:
1 | export default defineLogAndAuthHandler(async (event) => { |
修改後:
1 | export default defineLogAndAuthHandler(async (event) => { |
這個修正需要系統性地檢查所有 Server API Handler,只要有直接 return response 的地方都應該改為 return response.data,避免同樣的問題在其他 API 上重演。
結論
這個問題的根因不在後端、也不在前端元件,而是 Nuxt Server(Nitro)的 API Handler 直接透傳了後端 Response 物件。Node.js 在接收後端回應時已經解開了 gzip,但透傳卻把原始的 Content-Encoding: gzip 標頭一併帶給瀏覽器,導致瀏覽器嘗試二次解壓而失敗。
排查這類問題的關鍵:對比正常與異常 API 的 Response Headers。當你發現某支 API 突然多了 X-Powered-By: ASP.NET 和 Content-Encoding: gzip,而其他 API 都沒有,很可能就是 Nitro 把後端的原始標頭透傳出來了。回去檢查 Server API Handler 的回傳值,把 return response 改成 return response.data 就能解決。
