CORS trên localhost | Vite proxy vs header trước khi đổ lỗi production

(Cập nhật: 19 tháng 7, 2026 ) CORS localhost Vite Next.js proxy Access-Control-Allow-Origin frontend
Kết luận

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 proxyheader, 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.

FrontendAPICùng origin?
http://localhost:5173http://localhost:5173/apiCó (cùng port)
http://localhost:5173http://localhost:8080/apiKhông — port khác
http://127.0.0.1:5173http://localhost:5173Không — host khác
https://app.localhttp://localhost:8080Không — scheme + host
Hiểu nhầm phổ biến

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ứ:

  1. Origin trang (http://localhost:5173)
  2. 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

Proxy vs Access-Control header (dev)
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 và *

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,OPTIONS
  • Access-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 /api8080, 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

  1. Console có chữ CORS / Access-Control-Allow-Origin không? Nếu không — có thể 401, SSL, DNS; đừng sửa CORS trước.
  2. Origin frontend và URL API khác port/host/scheme chưa?
  3. Đang dùng absolute http://localhost:8080 trong khi đã cấu hình proxy chưa?
  4. Đã restart Vite/Next sau khi đổi vite.config / rewrites chưa?
  5. Postman 200 ≠ browser OK — thử curl -i -H "Origin: http://localhost:5173" http://localhost:8080/api/... và xem header trả về.
  6. 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ốngChọn
Không sửa được APIVite proxy / Next rewrite + fetch('/api/...')
Kiểm soát backendcors / 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.