jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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.

18 MIN READ

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, or POST {MethodGET, HEAD, hoặc POST}
  • 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-Type is one of only three values {Content-Type là một trong đúng ba giá trị}:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • No custom headers (e.g. Authorization, X-Token) {Không header tuỳ chỉnh (vd Authorization, X-Token)}
  • No ReadableStream body, and no listeners on XMLHttpRequest.upload {Không body ReadableStream, không listener trên XMLHttpRequest.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 safelisted Content-Type {vì nó không phải Content-Type an 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}:

HeaderPurpose {Mục đích}
Access-Control-Allow-OriginWhich 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-MethodsAllowed methods (preflight response) {Method cho phép (trong response preflight)}
Access-Control-Allow-HeadersAllowed request headers (preflight) {Header request cho phép (preflight)}
Access-Control-Allow-Credentialstrue to allow cookies/auth {true để cho phép cookie/auth}
Access-Control-Expose-HeadersWhich 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-AgeHow 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

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-Origin cannot be * {không thể*} — it must be the exact origin {phải là origin chính xác}.
  • Access-Control-Allow-Credentials: true is required {bắt buộc}.
  • Access-Control-Allow-Headers/-Methods also can’t use * {cũng không thể dùng *} (it’s treated literally) {(bị hiểu theo nghĩa đen)}.
  • Add Vary: Origin so caches don’t serve one origin’s CORS response to another {Thêm Vary: 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}:

modeBehavior {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-corsCross-origin allowed but response is opaque (unreadable) {Cho phép khác origin nhưng response mờ (không đọc được)}
same-originCross-origin requests fail outright {Request khác origin thất bại hẳn}
navigateUsed 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 Origin against an allowlist and echo it back {đừng hardcode một cái — kiểm tra Origin đế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.comapp.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ó}.


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

HeaderWhat 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 đượ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) 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}.