紀錄工作經驗、相關知識,解決技術相關問題。

Node JS, React, 網站優化

Vite 建置後 API 404 與 CORS 預檢失敗:Production 部署排查步驟

Vite 建置後 API 404 與 CORS 預檢失敗:Production 部署排查步驟 特色圖片

專案在本機用 npm run dev 一切正常,卻在 Vite 建置後 API 404,或在 Console 看到 CORS 錯誤,是 Vite 專案上線時很常見的狀況。原因通常出在開發與生產環境的差異:server.proxy 只屬於開發伺服器,建置後的靜態檔案並不帶有這層代理。

想了解 server.proxy 各參數(changeOrigin、rewrite、bypass、ws 等)的行為,可前往 〈Vite proxy 開發伺服器轉發設定、CORS 繞過與 Production 部署排查〉 觀看。若是第一次碰到 CORS 錯誤本身,可先看 〈CORS 錯誤是什麼? 如何解決本地端跨域請求 CORS Error〉。本文聚焦在建置之後:為什麼會 404、怎麼改成正式環境的 API 網址、預檢失敗長什麼樣子,以及依序排查的步驟。


一、 開發伺服器與生產環境的差異

在 Development 環境中,瀏覽器的 API 請求送往 Vite 本地開發伺服器(例如 http://localhost:5173/api/users),再由 Vite 以伺服器對伺服器的方式轉發到 target。依 MDN 的定義,協定、連接埠與主機都相同才算同源(Same-Origin),所以瀏覽器把這個請求當成同源請求,不會進入跨來源的 CORS 檢查。

下圖把同一個頁面(來源 http://localhost:5273)的四種請求放在一起:直接打沒有 CORS 標頭的後端會被瀏覽器擋下;經過 Vite proxy 的請求正常回應;vite build 後放在不含代理的靜態主機上,同一個相對路徑回 404;vite preview 則沿用了 server.proxy。

Vite 8.3.5、Chrome 158、Windows 11 的瀏覽器執行結果:直接跨來源請求、經 proxy、靜態主機與 vite preview 的比較
圖中為腳本回傳的原始結果:後端為本機 Node.js 程式,不回應任何 CORS 標頭。

當執行 vite build 進行 Production 建置時,產出的是純靜態 HTML、JavaScript 與 CSS 檔案,Node.js 開發伺服器並不會被打包進去。一旦部署至不含代理設定的靜態主機,客戶端的 fetch('/api/users') 便會直接對該主機發送請求,導致 404 Not Found 或無法取得後端回應。

vite preview 不等於正式主機

vite preview 的 preview.proxy 預設值就是 server.proxy,所以在本機用它檢查建置結果時,API 請求看起來一切正常,這只代表本機預覽沿用了開發時的設定,部署後的靜態主機並沒有這層代理。上線前要確認的是正式主機(Nginx、CDN 或後端)自己有對應的轉發規則,或前端改成直接呼叫有 CORS 設定的後端網址。


二、 Production 環境下的 API 請求路徑對應

由於 Vite proxy 僅在開發期生效,在 Production 環境中,通常會透過環境變數(Environment Variables)動態切換 API 基礎路徑。Vite 官方文件說明,import.meta.env.PROD 表示是否為生產環境,只有 VITE_ 前綴的變數會暴露在客戶端程式碼中,vite build 則會載入 .env.production。

// src/services/apiClient.ts
const getApiBaseUrl = (): string => {
  if (import.meta.env.DEV) {
    // 開發環境利用 Vite proxy,走相對路徑
    return '/api';
  }
  // 生產環境直接指向實際後端服務網域
  return import.meta.env.VITE_PRODUCTION_API_URL || 'https://api.example.com';
};

export const fetchUserData = async (userId: string) => {
  const baseUrl = getApiBaseUrl();
  const response = await fetch(`${baseUrl}/users/${userId}`, {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
    },
  });

  if (!response.ok) {
    throw new Error(`API 請求失敗,狀態碼:${response.status}`);
  }

  return response.json();
};

這個範本在開發環境走 /api,由 Vite proxy 轉發,能正常取得資料。建置後改成直接呼叫後端網址,就是真正的跨來源請求,後端必須自己處理 CORS,而且要留意這支 fetch 帶了 Content-Type: application/json:MDN 說明簡單請求的 Content-Type 只允許 application/x-www-form-urlencoded、multipart/form-data、text/plain,其他值會讓瀏覽器先對後端送出 OPTIONS 預檢請求,後端的預檢回應也要用 Access-Control-Allow-Headers 列出 content-type。後端只回 Access-Control-Allow-Origin 而沒有處理預檢時,瀏覽器會在 Console 顯示 Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response,畫面見下圖的最後一列。

Vite 8.3.5、Chrome 158、Windows 11 的執行結果:secure、proxyTimeout、目標離線、WebSocket 升級與預檢失敗訊息
圖中為腳本輸出內容,後端為本機 Node.js 程式,HTTPS 後端使用自行產生的自簽憑證。

三、 預檢請求與連線問題排查步驟

遇到「開發時正常、部署後失敗」或「代理設定了卻沒作用」時,依序從下面三步往外縮小範圍。

  1. 先不經過瀏覽器,直接對開發伺服器發請求,確認 Vite proxy 本身有沒有轉發成功。Windows PowerShell 中請輸入 curl.exe,避免被別名 curl 攔截。狀態碼 502 表示代理連不上或逾時,Vite 終端機會同時印出 http proxy error 與原因;404 則先檢查 rewrite 後的路徑。
curl.exe -i http://localhost:5173/api/users/7
  1. 直接對後端模擬瀏覽器的預檢請求,看回應有沒有 Access-Control-Allow-Origin 與 Access-Control-Allow-Headers。以下 api.example.com 請換成實際的後端網址,Origin 換成前端的正式網址。
curl.exe -i -X OPTIONS https://api.example.com/users/7 `
  -H "Origin: https://app.example.com" `
  -H "Access-Control-Request-Method: GET" `
  -H "Access-Control-Request-Headers: content-type"

回應若沒有 Access-Control-Allow-Headers: content-type,就是上一節遇到的預檢失敗;有標頭但狀態碼不是 2xx,則要改查後端對 OPTIONS 方法的處理。

  1. 確認正式主機本身有轉發規則。vite preview 沿用 server.proxy,它正常不代表正式環境正常;正式環境要由 Nginx 等 Web 伺服器轉發,或像上一節那樣直接指向有 CORS 設定的後端。

四、 常見問題與排錯(Q&A)

Q1:vite proxy in production 環境中為何失效?建置後如何替代本地代理?

  • 問題原因:Vite 提供的 server.proxy 是開發伺服器的功能。官方文件指出,若要在內部的 connect 應用加入自訂中介層,要寫成使用 configureServer 的外掛。一旦執行 vite build 產出靜態檔案並部署至靜態主機,代理邏輯就不存在了。
  • 解法:在生產環境中,應改由正式的 Web 伺服器(如 Nginx 反向代理)處理 /api 請求的轉發,或在前端程式碼中透過環境變數(import.meta.env.VITE_API_URL)直接指向實際後端網址,後端則要設定好 CORS,作法見第二節。

Q2:本機用 vite preview 看起來正常,部署後 API 卻 404,是哪裡不同?

  • 問題原因:preview.proxy 預設值就是 server.proxy,本機預覽沿用了開發時的代理設定;部署後的靜態主機沒有這層代理,相對路徑 /api/... 會直接打到靜態主機本身。
  • 解法:依第三節的步驟 3,確認正式主機有轉發規則,或把前端改成呼叫有 CORS 設定的後端網址。

Q3:部署後 Console 出現 not allowed by Access-Control-Allow-Headers in preflight response,該改前端還是後端?

  • 問題原因:fetch 帶了 Content-Type: application/json,不屬於簡單請求允許的值,瀏覽器會先送 OPTIONS 預檢;後端的預檢回應沒有用 Access-Control-Allow-Headers 列出 content-type。
  • 解法:改後端,讓 OPTIONS 請求回應 Access-Control-Allow-Origin 與 Access-Control-Allow-Headers: content-type。可用第三節步驟 2 的 curl.exe 指令先確認回應標頭,再回到瀏覽器驗證。

參考資料與延伸閱讀

發佈留言