CORS trên localhost | Vite proxy vs header trước khi đổ lỗi production
Port khác = origin khác. localhost:5173 gọi localhost:8080 bị CORS dù cùng máy. Dev: Vite/Next proxy (không sửa API) hoặc Access-Control-Allow-Origin trên backend (kiểm soát được server). Proxy chỉ che lúc local — không thay cấu hình CORS production. Extension tắt CORS chỉ để xác nhận, không phải giải pháp team.
Team outsourcing Việt Nam hay gặp cảnh này: mở Vite, fetch('http://localhost:8080/api/users'), console đỏ blocked by CORS policy. Ai đó mở Postman — 200. Kết luận vội: “API ổn, chắc React sai” hoặc tệ hơn: “Cứ vậy, lên production rồi tính”.
Bài này tập trung CORS trên localhost với Vite / Next — phân biệt proxy và header, và biết khi nào lỗi local không liên quan production. Nếu local đã ổn nhờ proxy nhưng staging fail, đọc CORS Error khi lên production.
Bài viết này giúp bạn
- Hiểu vì sao cùng localhost vẫn bị chặn
- Chọn proxy vs header theo quyền đụng backend
- Cấu hình mẫu Vite, Next.js rewrite, Express cors
- Tránh extension /
*bừa bãi làm hỏng ngày bàn giao - Checklist 5 phút trước khi đổ tội production
Vì sao localhost vẫn bị CORS?
Trình duyệt áp Same-Origin Policy: hai URL cùng origin chỉ khi scheme + host + port khớp hết.
| Frontend | API | Cùng origin? |
|---|---|---|
http://localhost:5173 | http://localhost:5173/api | Có (cùng port) |
http://localhost:5173 | http://localhost:8080/api | Không — port khác |
http://127.0.0.1:5173 | http://localhost:5173 | Không — host khác |
https://app.local | http://localhost:8080 | Không — scheme + host |
Postman, curl, Insomnia không báo CORS. CORS là ràng buộc trình duyệt. API trả 200 trong Postman không chứng minh browser sẽ nhận được response.
Đọc lỗi cho đúng
Console thường có dạng:
Access to fetch at 'http://localhost:8080/api/users' from origin 'http://localhost:5173'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
Ghi lại hai thứ:
- Origin trang (
http://localhost:5173) - URL request (
http://localhost:8080/...)
Nếu Network tab có OPTIONS rồi mới GET/POST → đó là preflight (thường vì Content-Type: application/json hoặc header Authorization). Cả OPTIONS và request thật đều cần header CORS đúng — thiếu một bên là fail.
Response JSON lỗi đôi khi bị trình duyệt “che” thành CORS. Dùng JSON Formatter trên body copy từ curl (không qua browser) để xem status/body thật trước khi đổ lỗi frontend.
Hai hướng xử lý local: proxy vs header
| Tiêu chí | Vite / Next proxy | Header trên API |
|---|---|---|
| Khi nào chọn | Không sửa được backend / mock cũ / API khách | Bạn (hoặc BE) kiểm soát Express/Nest/FastAPI |
| Trình duyệt thấy | Request tới cùng origin frontend → không kích CORS | Cross-origin có Allow-Origin khớp |
| Code frontend | Relative path: fetch('/api/users') | URL tuyệt đối tới :8080 vẫn được nếu header đúng |
| Ảnh hưởng production | Không tự 'sửa' CORS prod — chỉ che lúc dev | Cùng pattern allowlist có thể tái dùng cho origin prod |
| Rủi ro | Quên cấu hình prod → local OK, staging fail | Allow * + cookie / quên OPTIONS |
Quy tắc thực dụng:
- Freelance nhận API có sẵn, chỉ làm SPA → proxy
- Full-stack hoặc BE cùng team → header (và nhớ origin staging/prod sớm)
- Đã proxy xong mà vẫn muốn chắc prod → đọc CORS Error khi lên production trước ngày demo khách
Cách 1: Vite proxy (không đụng backend)
vite.config.ts:
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
});
Frontend gọi:
// Trước (cross-origin) — dễ CORS
await fetch('http://localhost:8080/api/users');
// Sau (same-origin qua proxy)
await fetch('/api/users');
Luồng: trình duyệt → http://localhost:5173/api/users → Vite forward → http://localhost:8080/api/users. Browser chỉ thấy same-origin với app nên không kiểm CORS.
Sau khi sửa config: restart npm run dev. Đổi target mà không restart là lỗi hay gặp trên team mới.
Next.js (App Router) — rewrite tương đương
next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'http://localhost:8080/api/:path*',
},
];
},
};
module.exports = nextConfig;
Cùng ý tưởng: browser gọi /api/... trên port Next; server Next forward sang backend. Restart next dev sau khi đổi rewrite.
Cách 2: Cho phép origin trên backend
Khi bạn kiểm soát API, trả header cho đúng origin frontend:
// Express
const cors = require('cors');
app.use(cors({
origin: 'http://localhost:5173',
credentials: true, // chỉ bật nếu thật sự gửi cookie
}));
Hoặc allowlist nhiều origin (Vite + Next cùng lúc):
const allowlist = [
'http://localhost:5173',
'http://localhost:3000',
];
app.use(cors({
origin(origin, cb) {
if (!origin || allowlist.includes(origin)) return cb(null, true);
return cb(new Error('Not allowed by CORS'));
},
}));
credentials: 'include' không đi cùng Access-Control-Allow-Origin: *. Phải ghi origin cụ thể và Access-Control-Allow-Credentials: true. Wildcard + cookie = trình duyệt từ chối.
Preflight nhanh (JSON / Authorization)
Nếu gửi Content-Type: application/json hoặc header tùy chỉnh, browser gửi OPTIONS trước. Middleware cors của Express thường xử lý sẵn; tự viết header thì nhớ:
Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,OPTIONSAccess-Control-Allow-Headers: Content-Type, Authorization- Trả OPTIONS với 204/200 kèm CORS header (kể cả trên lỗi 4xx — nếu không, console chỉ hiện CORS, che mất 401 thật)
Cách 3: Extension tắt CORS — chỉ để xác nhận
Extension kiểu “Allow CORS” khiến máy bạn bỏ qua kiểm tra. Dùng đúng một lần: bật → request xanh → tắt → kết luận “đúng là CORS”.
Không đưa vào README team. Teammate không cài extension sẽ fail; staging/prod user thật không tắt CORS. Che lỗi cấu hình đến sát deadline là kiểu hay gặp trên dự án outsourcing ngắn hạn.
Case study: hai lỗi local khác nhau
Case A — Junior gọi absolute URL dù đã có proxy
Vite đã proxy /api → 8080, nhưng code vẫn:
fetch('http://localhost:8080/api/orders');
Request bỏ qua proxy → vẫn cross-origin → CORS. Sửa: chỉ dùng /api/orders. Kiểm Network: Request URL phải là http://localhost:5173/api/..., không phải :8080.
Case B — 127.0.0.1 vs localhost
BE allow http://localhost:5173, nhưng mở app bằng http://127.0.0.1:5173. Origin khác chuỗi ký tự → bị chặn. Thống nhất một host trong team (ưu tiên localhost) và đưa cả hai vào allowlist nếu cần.
Checklist trước khi đổ tội production
- Console có chữ CORS /
Access-Control-Allow-Originkhông? Nếu không — có thể 401, SSL, DNS; đừng sửa CORS trước. - Origin frontend và URL API khác port/host/scheme chưa?
- Đang dùng absolute
http://localhost:8080trong khi đã cấu hình proxy chưa? - Đã restart Vite/Next sau khi đổi
vite.config/rewriteschưa? - Postman 200 ≠ browser OK — thử
curl -i -H "Origin: http://localhost:5173" http://localhost:8080/api/...và xem header trả về. - Local chỉ ổn nhờ proxy → chưa chứng minh prod ổn. Lên CORS Error khi lên production trước ngày bàn giao.
Tóm tắt chọn hướng
| Tình huống | Chọn |
|---|---|
| Không sửa được API | Vite proxy / Next rewrite + fetch('/api/...') |
| Kiểm soát backend | cors / middleware + allowlist origin local |
| Chỉ cần xác nhận “có phải CORS không” | Extension tạm → tắt ngay |
| Local OK, staging đỏ | Không phải bài này — xem production CORS |
CORS local không khó — khó là nhầm proxy với cấu hình production. Xử lý đúng lúc dev, rồi cấu hình header cho origin thật trước khi khách mở URL staging.