你是否常在開發時遇到「CORS 被封鎖」的錯誤?本文深入解析跨來源資源共享(CORS)是什麼、為什麼會出錯,並教你如何透過後端設定與前端 Proxy 有效解決,完整附上 Hono、Vite、Next.js 實作範例。
Written by: Chia1104 CC BY-NC-SA 4.0
大家在做前端開發時應該都遇過類似問題,那就是 CORS ,之前在面試時也很常被問到這問題,『遇到 CORS 該怎麼辦,要如何解決』。

'https://example.com' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.這錯誤大家一定都見過,很常照字面意思有些人會直接在前端設定 mode="no-cors",但是這是錯的 !!
若把 fetch 的 mode 設定成 no-cors 其實只是讓瀏覽器取消 preflight request,並且回應是不透明的,這意味著它的 headers 和 body 不能被 JavaScript 所存取。
const opaqueResponse = await fetch(url, {
mode: "no-cors", // [!code --]
})CORS(跨來源資源共享)是一種瀏覽器安全機制,用於限制網頁從不同來源請求資源,這意味這 CORS 只會發生在瀏覽器的環境。
一般情境下瀏覽器都會在每個 http request 前先發一個 preflight request,來取得該 api 所允許的資源存取。

並且該方法為 OPTIONS 的請求,可以注意他這裡會回應 Access-Control-Allow-Origin: http://localhost:3001 這就代表該資源(api)只允許這個 origin 做請求,當瀏覽器知道當下該 client 的 origin 若不是 http://localhost:3001 就會回 CORS 的錯誤。

而這個設定(Access-Control-Allow-Origin)就是後端 api 需要設定的。
Access-Control-Allow-Credentials: 是否允許 client 設定 credentials MDNAccess-Control-Allow-Headers: 允許 client 攜帶哪些 headers,像是 X-Custom-Header MDNAccess-Control-Allow-Methods: 允許 client 發哪些 http 請求的方法,包含一般的 GET, POST, PATCH, PUT, DELETE MDNAccess-Control-Allow-Origin: 允許哪些 client 的 origin 做請求 MDNAccess-Control-Expose-Headers: server 允許 client 端存取的 headers,例如 Retry-After MDN再回來看『遇到 CORS 該怎麼辦,要如何解決』,其實答案很明顯了,那就是請後端做 CORS 的設定。
這裡以 Hono 做範例。
import { Hono } from "hono";
const app = new Hono();
/**
* 針對 `/` 路徑設定 CORS
*/
app.options("/", (c) => {
c.header("Access-Control-Allow-Origin", "*");
c.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
c.header("Access-Control-Allow-Headers", "Content-Type");
c.status(204);
return c.body(null);
});
app.get("/", (c) => c.text("Hono!"));
export default app;這裡要注意,每個路徑都需要有對應的 OPTIONS routes,當然現在這類框架都有對應的套件幫助我們在 middleware 那層統一設定 CORS 了,這樣也不需要個別在對應的 routes 做修改。
import { cors } from "hono/cors";
import { env } from "@/env";
const getCORSAllowedOrigin = (): string[] | string => {
if (!env.CORS_ALLOWED_ORIGIN) return "*";
return (
env.CORS_ALLOWED_ORIGIN?.split(",").map((item) => {
return item.trim();
}) ?? "*"
);
};
app.use(
cors({
origin: getCORSAllowedOrigin(),
credentials: true,
// default settings
})
);最後就是如何單從前端來解,其實這問題應該最常發生在本地的開發階段,有時候若是因為安全疑慮,後端無法設定 localhost 供使用的話,也可透過自行架 proxy server 來解,因為 CORS 不會發生在 server 跟 server 之間的互動。
Vite 也是現在的主流 full-stack 開發工具,而他的 vite.config.mts 就有提供 proxy 的功能。
/// <reference types="vitest" />
import { defineConfig, loadEnv, type ConfigEnv } from 'vite';
import https from 'https';
export default ({ mode }: ConfigEnv) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
return defineConfig({
server: {
proxy: {
'/proxy-api': {
target: process.env.VITE_APP_APIBASE,
changeOrigin: true,
rewrite: path => path.replace(/^\/proxy-api/, ''),
agent: new https.Agent(),
},
},
},
});
};前端在 fetch 的時候再用 /proxy-api 的路徑 prefix 即可。
Next.js 雖然是個 react 全端框架,可以自己實作 api,但這樣就會需要自己處理 proxy 前後的 request 跟 response,這裡我們可以直接在 next.config.ts 裡做設定,直接把 /proxy-api/* 的路徑改寫成後端的 api 路徑。
const nextConfig: NextConfig = {
rewrites: async () => [
{
source: '/proxy-api/:path*',
destination: `${env.NEXT_PUBLIC_APP_AIP_HOST}/:path*`,
},
],
};
export default nextConfig;