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

Node JS, React, 網站優化

Vite proxy 開發伺服器轉發設定、CORS 繞過與 Production 部署排查

Vite proxy 開發伺服器轉發設定、CORS 繞過與 Production 部署排查 特色圖片

在前端專案開發過程中,串接遠端後端 API 或微服務時最常遭遇的痛點,莫過於瀏覽器因同源政策(Same-Origin Policy)而擋下跨來源請求。若後端伺服器未正確設定 CORS 回應標頭,前端工程師在本地端(Localhost)呼叫 API 時便會頻繁收到 CORS error。透過 Vite proxy 機制,我們能夠在本地開發伺服器中建立一層反向代理,將前端發出的特定路徑請求轉發至遠端後端,避開本地開發時的跨域限制。

想了解基礎原理細節或專案建置流程,可前往 〈React 工具鏈規格解析:基於 Vite 的初始化流程、目錄架構與生產環境建構〉 觀看。本文將聚焦於 Vite proxy 的進階底層機制、常見配置參數、WebSocket 支援、以及 Production 部署時的架構邊界。建置後的 API 404 與預檢失敗排查步驟,另見 〈Vite 建置後 API 404 與 CORS 預檢失敗:Production 部署排查步驟〉。


一、 核心速查與常見瓶頸對照

在配置代理時,若未正確理解 Node.js 伺服器與瀏覽器之間的轉發差異,常會導致請求路徑錯亂或後端接收不到原始標頭。以下先提供標準的 vite.config.ts 配置範本與常見瓶頸對照。

1. 常見錯誤寫法的工程瓶頸

  • 忽略 changeOrigin: true:後端 API 收到的 Host 標頭仍是前端開發伺服器的 localhost:5173,而非目標後端網域。後端若依 Host 做虛擬主機(Virtual Host)路由,就可能對應失敗。
  • 路徑正規化(rewrite)遺漏:前端請求 /api/users 轉發至 https://api.example.com 時,若未將 /api 前綴透過正規表達式清除,後端實際收到的請求會變成 https://api.example.com/api/users,造成 404 找不到路徑。
  • 混淆 Development 與 Production 邊界:誤以為在 vite.config.ts 設定的 server.proxy 會打包進最終的靜態檔案中,導致專案部署到靜態伺服器後所有 API 請求直接失效。

2. 核心機制規格對照表

參數選項型態/預設值核心功能與工程用途
targetstring指定欲轉發的目標後端伺服器網址(可包含協定與連接埠)。
changeOriginboolean (false)把送往後端的 Host 標頭改成 target 的主機與連接埠。它不會修改 Origin 標頭。
rewrite(path: string) => string接收路徑字串並返回改寫後的路徑,常用於去除 /api 前綴。
secureboolean是否驗證目標的 SSL 憑證;目標使用自簽憑證時設為 false 才連得上。
wsboolean是否代理 WebSocket 升級請求,用於即時通訊連線。
configure(proxy, options) => void取得底層代理實例,用來監聽 error、proxyReq、proxyRes 等事件。
bypass(req, res, options) => ...依請求決定是否略過代理,回傳值的行為見第二節。
proxyTimeoutnumber對外代理請求的逾時時間(毫秒)。

以下為標準的 Vite 代理伺服器設定範例:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        secure: false, // 若後端使用自簽憑證可設為 false
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
});

上表的 target、changeOrigin、secure、ws、proxyTimeout 與事件,來自 Vite 底層使用的 http-proxy-3;rewrite、configure、bypass 是 Vite 自己加上的選項。Vite 官方文件在 server.proxy 只列出 rewrite、configure、ws 的範例,bypass 的簽章與行為出自 Vite 原始碼與型別定義。


二、 關鍵機制解析:底層規範與注意事項

1. 開發伺服器與生產環境的本質差異

在 Development 環境中,瀏覽器發送的 API 請求實際上是送往 Vite 本地開發伺服器(例如 http://localhost:5173/api/users)。依 MDN 的定義,兩個 URL 的協定、連接埠(若有指定)與主機都相同才算同源(Same-Origin);頁面與這個請求網址符合,所以瀏覽器把它當成同源請求,不會進入跨來源的 CORS 檢查。Vite 再以底層的 http-proxy-3(Vite 官方文件寫明 server.proxy 的選項擴充自它)把請求由伺服器對伺服器(Server-to-Server)轉發至目標 target,再將回應帶回給瀏覽器。

Production 建置後只剩純靜態檔案,Node.js 開發伺服器不會被打包進去,部署到不含代理的靜態主機時,fetch('/api/users') 會直接對該主機發送請求而得到 404。四種請求的比較圖與完整排查步驟,請前往 〈Vite 建置後 API 404 與 CORS 預檢失敗:Production 部署排查步驟〉 觀看。

2. 關於 changeOrigin 與 Host 標頭的運作原理

http-proxy-3 的 README 對 changeOrigin 的說明是預設 false,作用是把 Host 標頭改成 target 的網址。設為 true 時,後端收到的 Host 與 target 的主機和連接埠一致;保持 false 時,後端收到的 Host 依然是前端開發伺服器的 localhost:5273。依 Host 路由的後端(例如多租戶的 API Gateway 或 Nginx 虛擬主機)會需要前者,實際回應什麼狀態碼則由後端決定。

要特別分清楚的是,changeOrigin 不會改 Origin 標頭。MDN 說明 Origin 會加在跨來源請求,以及同源但不是 GET 或 HEAD 的請求(例如 POST)上,所以經過代理的 POST 請求仍帶著 Origin: http://localhost:5273 到達後端,不論 changeOrigin 是 true 還是 false。如果後端自己有 CORS 白名單並檢查 Origin,就必須在後端的白名單放行開發網址(例如 http://localhost:5173),光靠 changeOrigin 解決不了。下圖是同一個測試後端回報自己收到的標頭:

Vite 8.3.5、Chrome 158、Windows 11 的瀏覽器執行結果:changeOrigin 為 true 與 false 時,後端收到的 Host 與 Origin 標頭,以及未設定 rewrite 時後端收到的路徑
圖中為腳本回傳的原始結果:GET 請求沒有 Origin,POST 請求有;只有 Host 隨 changeOrigin 改變,/a3 未設定 rewrite,後端收到的路徑仍帶 /a3 前綴。

3. rewrite 沒有設定時後端收到的路徑

上圖最後一列是沒有 rewrite 的情況:前端請求 /a3/users,後端收到的路徑就是 /a3/users,不會自動去掉前綴。後端路由不含這個前綴時就會回 404。遇到「代理有通、卻一直 404」,先看後端日誌印出的實際路徑,再回頭對照 rewrite 的正規表達式。

4. bypass 回傳值決定請求去向

Vite 的型別定義把 bypass 標為 webpack-dev-server 風格的略過函式,簽章是 (req, res, options)。依 Vite 原始碼(proxy.ts)的處理,回傳值有三種結果:

回傳值結果
字串把請求網址改成該字串,交給 Vite 後續的中介層處理,不轉發到 target
falseVite 直接回 404
其他(含 true、undefined)照常轉發到 target

也就是說,回傳 true 並不會略過代理,常見的誤解需要特別留意。下圖是這四種回傳值的實際結果:

Vite 8.3.5、Chrome 158、Windows 11 的瀏覽器執行結果:bypass 回傳字串、true、false、undefined 的狀態與回應內容
圖中為腳本回傳的原始結果。

5. 轉發失敗時的回應、逾時與自簽憑證

代理連不上目標、憑證驗證失敗或逾時時,底層會發出 error 事件;Vite 預設會在終端機印出 http proxy error,並回應 502 Bad Gateway。http-proxy-3 的 README 也建議自行監聽 error 事件。下圖整理了幾種情境的結果,包含 secure、proxyTimeout、WebSocket 升級,以及後端未處理預檢時的 Console 錯誤:

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

三、 實作範本:支援 WebSocket 的完整配置

以下範本對應 REST API 與 WebSocket 同時轉發的完整配置。Production 環境的 API 網址切換作法見前述延伸文章。

範本一:支援 WebSocket 與自訂路徑正規化的 vite proxy 完整配置

此範本同時處理一般 RESTful API 與 WebSocket 即時通訊轉發,並在 configure 中監聽事件。請求 /api/users/7 時,rewrite 會把路徑改成 /v1/users/7,後端收到的 Host 為 target 的主機,並多了 proxyReq 事件附加的 X-Custom-Dev-Proxy 標頭。

// vite.config.ts (進階配置範本)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      // 一般 API 轉發
      '/api': {
        target: 'https://internal-api.example.com',
        changeOrigin: true,
        secure: true,
        rewrite: (path) => path.replace(/^\/api/, '/v1'),
        configure: (proxy, options) => {
          proxy.on('error', (err, req, res) => {
            console.error('Vite proxy error 發生:', err);
          });
          proxy.on('proxyReq', (proxyReq, req, res) => {
            // 可在此處動態附加自訂標頭
            proxyReq.setHeader('X-Custom-Dev-Proxy', 'Vite-Local-Agent');
          });
        },
      },
      // WebSocket 即時通訊轉發
      '/ws-stream': {
        target: 'wss://stream.example.com',
        ws: true,
        changeOrigin: true,
        secure: false,
      },
    },
  },
});

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

Q1:為什麼設定 vite proxy config 後,後端 API 仍收到前端的 Origin 或 Host,導致 CORS 或路由失敗?

  • 問題原因:changeOrigin 預設為 false,後端收到的 Host 仍是前端開發伺服器的位址。另外,changeOrigin 只改 Host,不會改 Origin:POST 等請求仍帶著 Origin: http://localhost:5173 到後端。
  • 解法:依 Host 路由的後端,在對應的代理區塊加上 changeOrigin: true;後端有 CORS 白名單時,則要把開發網址加進後端的允許來源。兩者是不同的問題,不要混為一談。

Q2:如何正確配置 vite proxy websocket 轉發以支援即時通訊連線?

  • 問題原因:target 為 http:// 或 https:// 時,沒有設定 ws 的代理不會處理 WebSocket 升級請求,瀏覽器的連線會一直等不到回應。
  • 解法:在代理物件中將 ws 屬性設為 true。Vite 官方文件的範例使用 ws: true 搭配 ws:// 的 target;從 Vite 原始碼的判斷條件來看,target 以 ws: 或 wss: 開頭時也會處理升級請求。為了讓設定一目了然,建議一律明確寫出 ws: true。
// vite proxy websocket 設定範例
proxy: {
  '/socket': {
    target: 'ws://localhost:5174',
    ws: true,
  }
}

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

  • 問題原因:Vite 提供的 server.proxy 是開發伺服器的功能。官方文件指出,若要在內部的 connect 應用加入自訂中介層,要寫成使用 configureServer 的外掛。一旦執行 vite build 產出靜態檔案並部署至靜態主機,代理邏輯就不存在了。
  • 解法:改由正式的 Web 伺服器(如 Nginx 反向代理)轉發 /api,或讓前端直接呼叫有 CORS 設定的後端網址。完整作法與排查步驟見 〈Vite 建置後 API 404 與 CORS 預檢失敗:Production 部署排查步驟〉。

Q4:當遇到 vite proxy error 與連線逾時(vite proxy timeout)時,如何判斷是路徑還是連線問題?

  • 問題原因:代理請求失敗時 Vite 回 502。連不上目標(ECONNREFUSED)、憑證驗證失敗(self-signed certificate)、對外請求逾時(socket hang up)都是這個狀態碼,原因要看終端機印出的訊息;路徑對不上則是後端回 404,而不是 502。
  • 解法:先看終端機的 http proxy error 訊息分辨原因。逾時可用 proxyTimeout(毫秒,對外代理請求的逾時)調整;路徑問題則檢查 rewrite 後送到後端的路徑,詳見第二節第 3 點。

Q5:如何在 vite proxy https 設定中略過自簽憑證(Self-signed Certificate)的安全驗證?

  • 問題原因:當本地或內部測試後端使用自簽憑證時,secure 為 true 的代理連線會驗證憑證而失敗,Vite 回 502,終端機顯示 Error: self-signed certificate。
  • 解法:在該路徑的代理配置中加入 secure: false,即可略過憑證驗證,只限開發環境使用。

Q6:vite proxy configure 如何取得底層代理實例來攔截與記錄請求?

  • 問題原因:開發過程中需要即時偵測所有經過代理的請求與回應內容以進行除錯。
  • 解法:使用 configure 屬性掛載回呼函式,第一個參數就是 http-proxy-3 的代理實例,可以監聽 proxyReq(請求送出前,可修改請求)、proxyRes(目標回應時)與 error 事件。這是代理實例的事件,不是 Vite 的中介層;若要在 Vite 的 connect 應用加中介層,請改用外掛的 configureServer。

Q7:vite proxy not working 的狀況下,如何利用 bypass 函式根據 Request 標頭動態決定是否進行代理?

  • 問題原因:某些情境下,開發者希望針對特定條件(例如 Accept 標頭包含 text/html 的頁面導覽請求)不走代理,直接交給 Vite 回傳頁面。
  • 解法:在代理設定中配置 bypass 函式。回傳字串時,Vite 會把請求網址改成該字串並交給後續中介層;回傳 false 會得到 404;回傳 true 或 undefined 仍會轉發。
// bypass 函式設定範例
proxy: {
  '/api': {
    target: 'https://api.example.com',
    bypass: (req, res, options) => {
      if (req.headers.accept?.includes('text/html')) {
        // 若請求 HTML 則不走代理,直接回傳 index.html
        return '/index.html';
      }
    }
  }
}

參考資料與延伸閱讀

發佈留言