CORS là gì? Origin (scheme + host + port) giải thích đơn giản【2026】
CORS bắt đầu từ một câu hỏi: hai URL có cùng Origin không? Origin = scheme + host + port. Khác một phần → cross-origin → trình duyệt chặn đọc response trừ khi server cho phép qua CORS. Postman 200 không chứng minh gì. Hiểu Origin trước — rồi mới đọc CORS localhost hoặc fix production.
Sinh viên và freelancer Việt Nam hay gặp console đỏ blocked by CORS policy ngay buổi đầu gọi API. Phản ứng phổ biến: “React sai”, “Thêm mode: 'no-cors'”, hoặc “Cài extension tắt CORS”. Ba hướng đó đều lệch vì chưa hiểu Origin.
Bài này không dạy cấu hình Express/Vite chi tiết. Mục tiêu: nắm CORS là gì, Origin đơn giản, và biết khi nào chuyển sang bài fix local / production.
Bài viết này giúp bạn
- Định nghĩa CORS và mối quan hệ với Same-Origin Policy
- Phân tích Origin theo scheme / host / port (bảng so sánh)
- Đọc đúng dòng lỗi console trước khi đổ tội framework
- Phân biệt simple request và preflight (OPTIONS) ở mức khái niệm
- Checklist 5 phút + lộ trình đọc tiếp (local → production)
CORS là gì?
CORS (Cross-Origin Resource Sharing) là cơ chế để trang ở origin A được phép đọc response từ origin B khi server B đồng ý.
Trình duyệt mặc định áp Same-Origin Policy (SOP): script trên trang chỉ được đọc tự do tài nguyên cùng origin. Không có SOP, site độc hại có thể fetch API ngân hàng của bạn (khi bạn đang đăng nhập) và đọc JSON.
CORS không xóa SOP. Nó là cửa sổ ngoại lệ có kiểm soát: server trả header kiểu Access-Control-Allow-Origin → trình duyệt mới giao response cho JavaScript.
- Request vẫn có thể tới server. CORS chủ yếu chặn JS đọc response, không phải lúc nào cũng chặn packet đi. Network có thể thấy 200 nhưng Console vẫn đỏ.
- Sửa ở client không đủ. Header CORS phải đến từ server API (hoặc proxy làm request cùng origin với app).
Origin = scheme + host + port
Hai URL cùng origin chỉ khi cả ba khớp:
| Thành phần | Ví dụ | Đổi = origin khác? |
|---|---|---|
| Scheme | http vs https | Có |
| Host | localhost vs 127.0.0.1 vs api.example.com | Có |
| Port | 5173 vs 8080 vs 443 (https mặc định) | Có |
Path (/api/users) và query (?id=1) không thuộc Origin. https://app.com/a và https://app.com/b là cùng origin.
Bảng so sánh nhanh
| URL A (trang) | URL B (API) | Kết luận |
|---|---|---|
https://shop.vn | https://shop.vn/api | Cùng origin — CORS không chặn đọc |
https://shop.vn | https://api.shop.vn | Khác host — cần CORS hoặc proxy |
http://localhost:5173 | http://localhost:8080 | Khác port — vẫn cross-origin |
http://localhost:5173 | http://127.0.0.1:5173 | Khác host — localhost ≠ 127.0.0.1 |
http://app.local | https://app.local | Khác scheme — http ≠ https |
Đây là lý do team outsourcing hay sốc: “Cùng máy, cùng localhost mà bị CORS?” — vì port khác = origin khác. Chi tiết proxy Vite/Next nằm ở CORS trên localhost.
Trình duyệt làm gì khi cross-origin?
Luồng rút gọn:
- Trang
https://app.vngọifetch('https://api.vn/users'). - Trình duyệt gắn header
Origin: https://app.vn. - Server trả response. Nếu không có
Access-Control-Allow-Originphù hợp (hoặc không khớp), JS không đọc được body — Console báo CORS. - Nếu có preflight (OPTIONS trước): server phải trả lời OPTIONS đúng, rồi mới đến GET/POST thật.
Simple vs preflight (đủ để nhận diện)
| Loại | Khi nào | Bạn thấy gì |
|---|---|---|
| Simple | GET/POST “đơn giản”, Content-Type kiểu form | Thường một request |
| Preflight | application/json, header Authorization, method PUT/DELETE… | OPTIONS rồi mới request thật |
Thiếu header trên OPTIONS hoặc trên request thật đều fail. Đừng chỉ nhìn status của GET mà bỏ qua OPTIONS đỏ.
Đoạn lỗi mẫu — đọc hai giá trị
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 on the requested resource.
Ghi ngay:
- Origin trang:
http://localhost:5173 - URL bị gọi:
http://localhost:8080/api/users
So sánh scheme/host/port → bạn đã biết vì sao bị coi là cross-origin. Bước tiếp theo mới là sửa ở đâu.
CORS không phải lỗi React / Axios
| Hiện tượng | Ý nghĩa thật |
|---|---|
| Postman 200, Chrome đỏ | CORS chỉ enforce trên browser |
mode: 'no-cors' | Response opaque — JS gần như không đọc JSON được |
| Extension “Allow CORS” | Chỉ máy bạn; teammate/staging vẫn fail |
Access-Control-Allow-Origin: * + cookie | Thường không đi cùng credentials; dễ “chạy local rồi prod vỡ” |
Thấy CORS → hỏi “Origin trang khác Origin API ở phần nào?” trước khi mở file React. 80% lần đầu, câu trả lời nằm ở port hoặc subdomain.
Case study ngắn
1) Sinh viên: frontend 5173, Spring Boot 8080
fetch('http://localhost:8080/api/hello') từ Vite → Console CORS. Postman gọi cùng URL → 200. Kết luận đúng: khác port. Hướng xử lý: proxy Vite hoặc cho phép origin http://localhost:5173 trên Spring — xem CORS localhost.
2) Freelancer: local ổn, staging fail
Local dùng proxy /api → cùng origin lúc dev. Staging gọi https://api.khach.com trực tiếp → không còn proxy. Lỗi “đột nhiên” trên staging là CORS production, không phải “React đổi version”. Đọc CORS Error production trước ngày demo.
Checklist 5 phút (trước khi Google lung tung)
- Chép đủ dòng Console (có
from originvàfetch at) - Viết Origin trang và Origin API cạnh nhau — khoanh chỗ khác
- Network: có OPTIONS đỏ không?
- Thử curl/Postman chỉ để xem status API — đừng kết luận “hết CORS”
- Không cài extension làm bước “fix chính thức”
- Chọn bài tiếp theo:
- Local / Vite / port → cors-local-dev
- Staging / domain thật → cors-error-fix
Khi debug response JSON đã qua CORS, có thể dùng JSON Formatter trên trình duyệt để đọc payload — công cụ không sửa CORS, chỉ giúp soi dữ liệu sau khi request đã thành công.
Lộ trình đọc tiếp
| Bạn đang ở đâu | Đọc tiếp |
|---|---|
| Mới gặp từ CORS / Origin | Bài này (xong) |
| Vite/Next + API local khác port | CORS trên localhost |
| Local ổn, staging/prod đỏ | CORS Error khi lên production |
| Muốn sâu OPTIONS / preflight | Bài preflight (khi cần header phức tạp) |
Tóm tắt
- CORS = cách server cho phép trang origin khác đọc response.
- Origin = scheme + host + port; path không tính.
- localhost khác port vẫn là cross-origin.
- Postman ≠ trình duyệt.
- Sửa ở server hoặc proxy, không phải “tắt CORS trên Chrome”.
- Hiểu Origin xong hãy vào bài local / production — đỡ mất nửa ngày copy header sai chỗ.