前言 之前在正式環境遇到一個棘手的問題:使用者回報某支 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 2 const response = await api.memberGetMemberPriorityHistories (query);return response;
這裡 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 2 3 4 5 6 export default defineLogAndAuthHandler (async (event) => { const query = getQuery (event); const api = new GetMemberPriorityHistories (); const response = await api.memberGetMemberPriorityHistories (query); return response; });
修改後:
1 2 3 4 5 6 export default defineLogAndAuthHandler (async (event) => { const query = getQuery (event); const api = new GetMemberPriorityHistories (); const response = await api.memberGetMemberPriorityHistories (query); return response.data ; });
這個修正需要系統性地檢查所有 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 就能解決。