CORS Explained — Preflight, Simple Requests, and How to Actually Fix It
A bilingual deep-dive into CORS: the Same-Origin Policy, simple vs preflight requests, every Access-Control header, credentials mode, which requests bypass CORS checks, and how to solve CORS in real web apps.
The One Thing Everyone Gets Wrong {Điều ai cũng hiểu sai}
Before anything else, internalize this {Trước hết, hãy khắc cốt điều này}: CORS does not block your request from being sent. It blocks your JavaScript from reading the response. {CORS không chặn request của bạn được gửi đi. Nó chặn JavaScript của bạn đọc response.}
The cross-origin request usually reaches the server and runs {Request khác origin thường vẫn tới server và chạy} (your POST may even create a record!) {(cái POST của bạn thậm chí có thể tạo record!)} — the browser just refuses to hand the response back to your code {browser chỉ từ chối trả response lại cho code của bạn} unless the server opts in with the right headers {trừ khi server đồng ý bằng header đúng}. Understanding this removes 90% of the confusion {Hiểu điều này xoá bỏ 90% sự bối rối}.
The Same-Origin Policy (SOP) {Chính sách Same-Origin (SOP)}
CORS exists to relax the Same-Origin Policy {CORS tồn tại để nới lỏng Same-Origin Policy}, a browser security rule that isolates sites from each other {một quy tắc bảo mật của browser cô lập các site khỏi nhau}. Two URLs share an origin only if all three match {Hai URL chung một origin chỉ khi cả ba khớp}:
scheme :// host : port
origin = https :// app.example.com: 443
https://app.example.com/a vs https://app.example.com/b → SAME origin {cùng}
https://app.example.com vs http://app.example.com → different (scheme) {khác scheme}
https://app.example.com vs https://api.example.com → different (host) {khác host}
https://app.example.com vs https://app.example.com:8080 → different (port) {khác port}
Without CORS, a script on app.example.com couldn’t read data from api.example.com {Không có CORS, script trên app.example.com không đọc được data từ api.example.com} — even though both are yours {dù cả hai đều của bạn}. CORS is the mechanism the server uses to say “this other origin is allowed to read my responses.” {CORS là cơ chế server dùng để nói “origin kia được phép đọc response của tôi.”}
Simple Requests {Request đơn giản}
Some requests are considered “simple” {Một số request được coi là “đơn giản”} and are sent directly, with no preflight {và được gửi trực tiếp, không preflight}. A request is simple only if all of these hold {Một request là đơn giản chỉ khi tất cả điều sau đúng}:
- Method is
GET,HEAD, orPOST{Method làGET,HEAD, hoặcPOST} - Headers are limited to CORS-safelisted ones {Header chỉ giới hạn ở các header an toàn}:
Accept,Accept-Language,Content-Language,Content-Type(with limits) {(có giới hạn)}, and a few others {và vài cái khác} Content-Typeis one of only three values {Content-Typelà một trong đúng ba giá trị}:application/x-www-form-urlencodedmultipart/form-datatext/plain
- No custom headers (e.g.
Authorization,X-Token) {Không header tuỳ chỉnh (vdAuthorization,X-Token)} - No
ReadableStreambody, and no listeners onXMLHttpRequest.upload{Không bodyReadableStream, không listener trênXMLHttpRequest.upload}
// SIMPLE — sent directly {Đơn giản — gửi trực tiếp}
fetch("https://api.example.com/data"); // GET, no custom headers
// NOT simple → triggers preflight {Không đơn giản → kích hoạt preflight}
fetch("https://api.example.com/data", {
method: "POST",
headers: { "Content-Type": "application/json" }, // JSON is NOT one of the 3! {không thuộc 3 giá trị!}
body: JSON.stringify({ x: 1 }),
});
The classic gotcha {Cái bẫy kinh điển}: sending JSON (
application/json) always triggers a preflight {gửi JSON luôn luôn kích hoạt preflight}, because it’s not a safelistedContent-Type{vì nó không phảiContent-Typean toàn}.
Even a simple request is still subject to CORS {Ngay cả request đơn giản vẫn chịu CORS}: the server must return Access-Control-Allow-Origin {server vẫn phải trả Access-Control-Allow-Origin} or the browser blocks reading the response {nếu không browser chặn đọc response}.
Preflight Requests {Request Preflight}
When a request is not simple {Khi request không đơn giản}, the browser sends an automatic preflight first {browser tự gửi một preflight trước} — an OPTIONS request that asks permission {một request OPTIONS để xin phép} before the real request {trước request thật}:
# 1. Browser sends preflight automatically {Browser tự gửi preflight}
OPTIONS /data HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
# 2. Server must approve {Server phải chấp thuận}
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
# 3. Only THEN does the browser send the real request {Chỉ SAU ĐÓ browser mới gửi request thật}
POST /data HTTP/1.1
Origin: https://app.example.com
Content-Type: application/json
...
A preflight is triggered by {Một preflight bị kích hoạt bởi}: a non-simple method (PUT, DELETE, PATCH) {method không đơn giản}, any custom header {bất kỳ header tuỳ chỉnh}, or a non-safelisted Content-Type (like application/json) {hoặc Content-Type không an toàn}.
Access-Control-Max-Age lets the browser cache the preflight result {cho browser cache kết quả preflight} so it doesn’t re-ask for every request {để không phải hỏi lại mỗi request} (capped by the browser, e.g. ~2 hours in Chrome) {(bị browser giới hạn, vd ~2 tiếng ở Chrome)}.
Live Demo {Demo trực tiếp}
CORS rules are easier to internalize when you can poke at them {Quy tắc CORS dễ thấm hơn khi bạn tự nghịch}. The demo below is a Request Inspector {Demo dưới là một Request Inspector}: pick a method, Content-Type, custom headers, and credentials {chọn method, Content-Type, header tuỳ chỉnh, credentials}, and it tells you whether the request is simple or preflighted {và nó cho biết request là simple hay preflight}, shows the generated OPTIONS {hiện OPTIONS được sinh ra}, and lists the response headers the server must return {và liệt kê header server cần trả về}. There’s also a bypass checker for <img>/<script>/no-cors {Cũng có một bypass checker cho <img>/<script>/no-cors}.
Open the full demo {Mở demo đầy đủ}: /tools/cors-demo/.
The CORS Response Headers {Các Header Response của CORS}
All control lives in the server’s response headers {Mọi điều khiển nằm ở header response của server}:
| Header | Purpose {Mục đích} |
|---|---|
Access-Control-Allow-Origin | Which origin may read the response {Origin nào được đọc response}. A specific origin or * {Một origin cụ thể hoặc *} |
Access-Control-Allow-Methods | Allowed methods (preflight response) {Method cho phép (trong response preflight)} |
Access-Control-Allow-Headers | Allowed request headers (preflight) {Header request cho phép (preflight)} |
Access-Control-Allow-Credentials | true to allow cookies/auth {true để cho phép cookie/auth} |
Access-Control-Expose-Headers | Which response headers JS may read {Header response nào JS được đọc} (default: a safelist only) {(mặc định: chỉ một safelist)} |
Access-Control-Max-Age | How long to cache the preflight {Cache preflight bao lâu} |
Reading Response Headers {Đọc Header Response}
By default, JS can only read a safelisted set of response headers {Mặc định, JS chỉ đọc được một tập header response an toàn} (Cache-Control, Content-Type, etc.) {}. To read a custom one like X-Total-Count {Để đọc một header tuỳ chỉnh như X-Total-Count}, the server must expose it {server phải expose nó}:
Access-Control-Expose-Headers: X-Total-Count, X-Request-Id
Credentials Mode — The Cookie Trap {Chế độ Credentials — Cái bẫy Cookie}
By default, cross-origin fetch/XHR do not send cookies {Mặc định, fetch/XHR khác origin không gửi cookie}. You must opt in {Bạn phải bật lên}:
fetch("https://api.example.com/me", {
credentials: "include", // send cookies cross-origin {gửi cookie khác origin}
});
When credentials are included, the rules get stricter {Khi có credentials, quy tắc chặt hơn}:
# ❌ INVALID — wildcard is forbidden with credentials {cấm dùng wildcard với credentials}
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
# ✅ VALID — must echo the EXACT origin {phải echo CHÍNH XÁC origin}
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
With credentials: "include" {Với credentials: "include"}:
Access-Control-Allow-Origincannot be*{không thể là*} — it must be the exact origin {phải là origin chính xác}.Access-Control-Allow-Credentials: trueis required {bắt buộc}.Access-Control-Allow-Headers/-Methodsalso can’t use*{cũng không thể dùng*} (it’s treated literally) {(bị hiểu theo nghĩa đen)}.- Add
Vary: Originso caches don’t serve one origin’s CORS response to another {ThêmVary: Originđể cache không phục vụ response CORS của origin này cho origin khác}.
Which Requests Bypass CORS Checks? {Request nào bỏ qua kiểm tra CORS?}
This is the part that trips people up {Đây là phần khiến nhiều người vấp}. CORS governs reading responses in scripts {CORS quản lý việc đọc response trong script}. Many cross-origin requests happen freely {Nhiều request khác origin diễn ra tự do} — they just can’t be read by JS {chúng chỉ không thể bị JS đọc}.
1. Resource-Loading Tags (no read access anyway) {Thẻ tải tài nguyên (vốn không đọc được)}
These send cross-origin requests without CORS, because the browser uses the result, not your script {Các thẻ này gửi request khác origin không CORS, vì browser dùng kết quả, không phải script của bạn}:
<img src="https://other.com/pic.jpg" /> <!-- displays, JS can't read pixels {không đọc pixel} -->
<script src="https://cdn.com/lib.js"></script> <!-- executes {thực thi} -->
<link rel="stylesheet" href="https://cdn.com/app.css" />
<video src="https://other.com/clip.mp4"></video>
<iframe src="https://other.com/page"></iframe> <!-- renders, JS can't read contents {không đọc nội dung} -->
This is also why CDNs, tracking pixels, and <form> cross-site POSTs work without CORS {Đây cũng là vì sao CDN, tracking pixel, và <form> POST xuyên site hoạt động không cần CORS} — and why CORS alone does not stop CSRF {và vì sao chỉ CORS không chặn được CSRF}.
2. no-cors Fetch Mode → Opaque Response {Mode no-cors → Response mờ}
const res = await fetch("https://other.com/data", { mode: "no-cors" });
// res.type === "opaque" — status 0, body unreadable {không đọc được}
console.log(res.status); // 0
await res.text(); // "" (empty) {rỗng}
The request goes out {Request vẫn đi ra}, but you get an opaque response you can’t inspect {nhưng bạn nhận một response mờ không thể xem}. It’s only useful for “fire and forget” or caching in a Service Worker {Chỉ hữu ích cho “fire and forget” hoặc cache trong Service Worker}.
3. Same-Origin Requests {Request cùng origin}
No CORS at all — fully readable {Không CORS gì cả — đọc được hoàn toàn}. The fastest “fix” for CORS is to make the request same-origin (via a proxy) {Cách “sửa” CORS nhanh nhất là biến request thành same-origin (qua proxy)}.
Summary {Tóm tắt}
Cross-origin request happens? {Request khác origin xảy ra?} → almost always YES {hầu như LUÔN}
JS can READ the response? {JS ĐỌC được response?} → only if CORS allows it {chỉ khi CORS cho phép}
<img>/<script>/<link>/<iframe> → request sent, response NOT readable by JS (no CORS needed)
fetch mode: "no-cors" → request sent, opaque response (unreadable)
fetch mode: "cors" (default) → response readable ONLY with proper CORS headers
same-origin → no CORS involved
The Browser’s Decision Flow {Luồng quyết định của Browser}
Put together, the browser runs roughly this algorithm for a cross-origin fetch {Gộp lại, browser chạy đại khái thuật toán này cho một fetch khác origin}:
1. Is it same-origin? → yes: read freely, no CORS {đọc tự do}
2. Is the request mode "no-cors"? → yes: send it, return an OPAQUE response {trả response mờ}
3. Is it a "simple" request?
no → send a PREFLIGHT (OPTIONS) first
→ server approves Methods/Headers? no → BLOCKED {chặn}
yes → skip preflight
4. Send the actual request (with cookies if credentials:"include")
5. Does the response have Access-Control-Allow-Origin matching the origin?
no → BLOCKED (CORS error) {chặn}
6. Credentials included?
yes → need Allow-Credentials:true AND exact origin (not "*") → else BLOCKED
7. Otherwise → READ the response ✓ {đọc response}
Visualize It {Trực quan hoá}
The flow diagram below lights up the exact path for any request/server combination {Sơ đồ luồng dưới đây làm sáng chính xác đường đi cho mọi tổ hợp request/server} — toggle same-origin, method, Content-Type, credentials, and the server’s headers to watch a request land on READ, BLOCKED, or OPAQUE {bật/tắt same-origin, method, Content-Type, credentials, và header server để xem request kết thúc ở READ, BLOCKED, hay OPAQUE}.
Open the full flow demo {Mở demo luồng đầy đủ}: /tools/cors-flow-demo/.
Fetch Request Modes {Các Mode của Fetch Request}
The mode option on a request decides how CORS applies {Tuỳ chọn mode của request quyết định CORS áp dụng thế nào}:
mode | Behavior {Hành vi} |
|---|---|
cors (default {mặc định}) | Cross-origin allowed if the server sends CORS headers; response readable {Cho phép khác origin nếu server gửi header CORS; response đọc được} |
no-cors | Cross-origin allowed but response is opaque (unreadable) {Cho phép khác origin nhưng response mờ (không đọc được)} |
same-origin | Cross-origin requests fail outright {Request khác origin thất bại hẳn} |
navigate | Used by the browser for top-level navigations (not by fetch) {Browser dùng cho điều hướng cấp cao nhất (không phải fetch)} |
How to Actually Fix CORS {Cách thực sự sửa CORS}
CORS is configured on the server that owns the resource {CORS được cấu hình trên server sở hữu tài nguyên}. The frontend can’t fix it alone {Frontend không tự sửa được} (other than proxying) {(ngoài việc proxy)}.
Solution 1: Set Headers on the API Server {Giải pháp 1: Set Header trên API Server}
Express {Express}:
import cors from "cors";
app.use(
cors({
origin: "https://app.example.com", // not "*" if you use cookies {không dùng "*" nếu có cookie}
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 86400,
})
);
// The cors middleware also answers OPTIONS preflights automatically
// {middleware cors cũng tự trả lời preflight OPTIONS}
nginx {nginx}:
location /api/ {
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Vary "Origin" always;
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Max-Age 86400 always;
return 204; # answer preflight {trả lời preflight}
}
}
If you support multiple origins {Nếu hỗ trợ nhiều origin}, don’t hardcode one — validate the incoming
Originagainst an allowlist and echo it back {đừng hardcode một cái — kiểm traOriginđến với một allowlist rồi echo lại}. Never blindly reflect any origin with credentials {Đừng bao giờ phản chiếu mọi origin một cách mù quáng khi có credentials}.
Solution 2: Proxy (Make It Same-Origin) {Giải pháp 2: Proxy (Biến thành Same-Origin)}
If you can’t change the API server (third-party API) {Nếu bạn không sửa được API server (API bên thứ ba)}, route requests through your own origin {định tuyến request qua origin của bạn}.
Dev (Vite proxy) {Dev (Vite proxy)}:
// vite.config.js — browser calls /api, Vite forwards it (no CORS in the browser)
// {browser gọi /api, Vite chuyển tiếp (không CORS ở browser)}
export default {
server: {
proxy: {
"/api": { target: "https://api.thirdparty.com", changeOrigin: true },
},
},
};
Production (reverse proxy / BFF) {Production (reverse proxy / BFF)}: serve the app and the API under the same origin {phục vụ app và API dưới cùng origin} (e.g. app.com and app.com/api behind nginx, or a Backend-for-Frontend) {(vd app.com và app.com/api sau nginx, hoặc một Backend-for-Frontend)}. CORS disappears entirely because requests are same-origin {CORS biến mất hoàn toàn vì request là same-origin}.
Decoding the Error Message {Giải mã thông báo lỗi}
"No 'Access-Control-Allow-Origin' header is present on the requested resource"
→ Server didn't send the header at all. {Server không gửi header.}
"The value of 'Access-Control-Allow-Origin' ... must not be the wildcard '*'
when the request's credentials mode is 'include'"
→ You used credentials + "*". Echo the exact origin. {Dùng credentials + "*". Echo origin cụ thể.}
"Method PUT is not allowed by Access-Control-Allow-Methods in preflight response"
→ Add PUT to Access-Control-Allow-Methods. {Thêm PUT vào Allow-Methods.}
"Request header field authorization is not allowed by Access-Control-Allow-Headers"
→ Add Authorization to Access-Control-Allow-Headers. {Thêm Authorization vào Allow-Headers.}
A telltale sign {Một dấu hiệu nhận biết}: the network tab shows the request succeeded (200) {tab network hiện request thành công (200)} but the console still shows a CORS error {nhưng console vẫn báo lỗi CORS} → the response came back fine, the browser just blocked reading it {response về bình thường, browser chỉ chặn đọc nó}.
Edge Cases & Related Headers {Trường hợp đặc biệt & Header liên quan}
CORS doesn’t live alone {CORS không sống một mình}. A few neighbors and edge cases trip people up {Vài “hàng xóm” và trường hợp đặc biệt khiến nhiều người vấp}.
CORS with Redirects {CORS với Redirect}
A preflight (OPTIONS) must not redirect {Một preflight (OPTIONS) không được redirect} — the browser fails it {browser sẽ làm nó thất bại}. For the actual request {Với request thật}, redirects to another origin must themselves pass CORS at each hop {redirect tới origin khác phải tự pass CORS ở mỗi chặng}, and the final response’s Allow-Origin must still match {và Allow-Origin của response cuối vẫn phải khớp}. Avoid redirecting CORS requests when you can {Tránh redirect request CORS khi có thể}.
Private Network Access (PNA) {Truy cập mạng riêng (PNA)}
A newer protection {Một lớp bảo vệ mới hơn}: a public website making requests to a private/local network (e.g. 192.168.x.x, localhost) {một website công khai gọi tới mạng riêng/local (vd 192.168.x.x, localhost)} triggers a special preflight {kích hoạt một preflight đặc biệt}:
Access-Control-Request-Private-Network: true
# server must answer:
Access-Control-Allow-Private-Network: true
This stops malicious public sites from poking your router or local dev servers {Điều này ngăn site công khai độc hại “chọc” router hoặc dev server local của bạn}.
CORP, COEP, COOP — The Cross-Origin Trio {CORP, COEP, COOP — Bộ ba Cross-Origin}
These are separate from CORS but often confused with it {Chúng tách biệt với CORS nhưng hay bị nhầm}:
| Header | What it does {Làm gì} |
|---|---|
Cross-Origin-Resource-Policy (CORP) | Lets a resource say “who may embed/load me” (same-origin, same-site, cross-origin) {Cho một tài nguyên nói “ai được nhúng/tải tôi”} |
Cross-Origin-Embedder-Policy (COEP) | A document demands all its subresources opt in (CORP/CORS) {Một document yêu cầu mọi subresource phải opt-in} |
Cross-Origin-Opener-Policy (COOP) | Isolates your browsing context from windows it opens {Cô lập browsing context của bạn khỏi cửa sổ nó mở} |
Together, COEP + COOP unlock cross-origin isolation {Cùng nhau, COEP + COOP mở khoá cross-origin isolation}, required for powerful features like SharedArrayBuffer and high-resolution timers {cần cho các tính năng mạnh như SharedArrayBuffer và timer độ phân giải cao}.
ORB / CORB — Opaque Response Blocking {ORB / CORB — Chặn response mờ}
CORB (now evolving into ORB) is a browser defense {CORB (đang tiến hoá thành ORB) là một phòng thủ của browser} that blocks cross-origin responses of sensitive types (HTML, JSON, XML) {chặn response khác origin của các kiểu nhạy cảm (HTML, JSON, XML)} from ever entering a process where they could be read via side channels (Spectre) {khỏi việc lọt vào một process nơi chúng có thể bị đọc qua side channel (Spectre)}. It’s automatic {Nó tự động} — just make sure your APIs send correct Content-Type headers {chỉ cần đảm bảo API gửi đúng header Content-Type}.
The crossorigin Attribute & Canvas Tainting {Thuộc tính crossorigin & Canvas Tainting}
When you load a cross-origin image into a <canvas> {Khi bạn nạp ảnh khác origin vào <canvas>}, the canvas becomes tainted {canvas trở nên tainted (nhiễm)} and getImageData()/toDataURL() throw {và getImageData()/toDataURL() ném lỗi}. To read pixels, the image must be served with CORS and requested with the attribute {Để đọc pixel, ảnh phải được phục vụ với CORS và được yêu cầu với thuộc tính}:
<img crossorigin="anonymous" src="https://cdn.com/pic.jpg" />
<!-- server must send Access-Control-Allow-Origin {server phải gửi Allow-Origin} -->
The same applies to web fonts (@font-face requires CORS for cross-origin fonts) {Tương tự với web font (@font-face cần CORS cho font khác origin)} and <script crossorigin> for detailed error reporting {và <script crossorigin> để báo lỗi chi tiết}.
WebSocket & Server-Sent Events {WebSocket & Server-Sent Events}
WebSockets are not subject to CORS {WebSocket không chịu CORS} — they have their own origin model {chúng có mô hình origin riêng}; the server must check the Origin header itself {server phải tự kiểm tra header Origin}. EventSource (SSE), however, is a fetch {Tuy nhiên EventSource (SSE) là một fetch} and follows CORS rules {nên theo quy tắc CORS}.
Network Error vs CORS Error {Lỗi mạng vs Lỗi CORS}
A failed CORS read surfaces as a generic TypeError: Failed to fetch {Một CORS read thất bại hiện ra dưới dạng TypeError: Failed to fetch chung chung} — JS can’t distinguish “blocked by CORS” from “network down” for security reasons {JS không phân biệt được “bị CORS chặn” với “mạng hỏng” vì lý do bảo mật}. Check the Network tab + console {Hãy xem tab Network + console}: if the request shows 200 but the console logs a CORS message, it’s a header problem, not connectivity {nếu request hiện 200 nhưng console báo CORS, đó là lỗi header, không phải kết nối}.
Timing-Allow-Origin {Timing-Allow-Origin}
By default, the Resource Timing API hides detailed timings of cross-origin resources {Mặc định, Resource Timing API giấu thông tin timing chi tiết của tài nguyên khác origin}. To expose them {Để hiển thị chúng}, the resource sends Timing-Allow-Origin: https://app.example.com (or *) {tài nguyên gửi Timing-Allow-Origin: https://app.example.com (hoặc *)}.
Pitfalls & Security {Lỗi & Bảo mật}
| Pitfall {Lỗi} | Fix {Sửa} |
|---|---|
* + credentials {* + credentials} | Echo the exact origin + Allow-Credentials: true {Echo origin chính xác} |
| Preflight returns 4xx/redirect {Preflight trả 4xx/redirect} | OPTIONS must return 2xx with headers; no redirects {OPTIONS phải trả 2xx kèm header; không redirect} |
| Custom response header unreadable {Header response tuỳ chỉnh không đọc được} | Add it to Access-Control-Expose-Headers {Thêm vào Expose-Headers} |
| Cache serves wrong CORS headers {Cache phục vụ sai header CORS} | Send Vary: Origin {Gửi Vary: Origin} |
Reflecting any Origin {Phản chiếu mọi Origin} | Validate against an allowlist {Kiểm tra với allowlist} — reflecting Origin + credentials = data leak {= rò rỉ dữ liệu} |
| Thinking CORS stops CSRF {Nghĩ CORS chặn CSRF} | It doesn’t — use SameSite cookies + CSRF tokens {Không — dùng cookie SameSite + token CSRF} |
Security note {Lưu ý bảo mật}: CORS is not a server-side access control {CORS không phải kiểm soát truy cập phía server}. It only constrains browsers {Nó chỉ ràng buộc browser}. Non-browser clients (curl, servers) ignore it entirely {Client không phải browser (curl, server) bỏ qua hoàn toàn}. Always enforce real authn/authz on the server {Luôn thực thi authn/authz thật trên server}.
Quick Reference {Tham khảo nhanh}
Mental model {Mô hình tư duy}:
CORS blocks READING the response, not SENDING the request.
Simple request (no preflight) {Request đơn giản (không preflight)}:
- GET/HEAD/POST
- only safelisted headers
- Content-Type ∈ {x-www-form-urlencoded, multipart/form-data, text/plain}
Triggers preflight (OPTIONS) {Kích hoạt preflight}:
- PUT/PATCH/DELETE, custom headers, or Content-Type: application/json
Server response headers {Header response server}:
Access-Control-Allow-Origin (exact origin or *)
Access-Control-Allow-Methods (preflight)
Access-Control-Allow-Headers (preflight)
Access-Control-Allow-Credentials (true → no "*" allowed)
Access-Control-Expose-Headers (read custom response headers)
Access-Control-Max-Age (cache preflight)
Vary: Origin (when echoing origin)
Bypass / not read-checked {Bypass / không kiểm tra đọc}:
<img>/<script>/<link>/<iframe>, fetch mode "no-cors" (opaque), same-origin
Fixes {Cách sửa}:
1. Set Access-Control-* on the API server
2. Proxy → make it same-origin (dev proxy / reverse proxy / BFF)
CORS feels hostile until the model clicks {CORS có vẻ “thù địch” cho đến khi mô hình “thông”}: it’s the browser protecting users {nó là browser bảo vệ người dùng}, and the server holds the key {và server giữ chìa khoá}. Set the right headers — or move the request same-origin — and it melts away {Đặt đúng header — hoặc đưa request về same-origin — và nó tan biến}.