Lập trình · 21/09/2026

CORS thực chiến: Vì sao API chạy bằng curl nhưng lỗi trên trình duyệt?

API trả JSON bình thường khi gọi bằng curl, nhưng cùng URL đó lại báo lỗi CORS trong ứng dụng web. Hai kết quả này không mâu thuẫn: trình duyệt kiểm soát việc JavaScript đọc phản hồi khác origin, còn curl không áp dụng chính sách đọc dữ liệu của trình duyệt. Muốn sửa đúng, cần tìm ra request nào bị chặn và response nào thiếu điều kiện cho phép.

CORS thực chiến: Vì sao API chạy bằng curl nhưng lỗi trên trình duyệt?

API trả JSON bình thường khi gọi bằng curl, nhưng cùng URL đó lại báo lỗi CORS trong ứng dụng web. Hai kết quả này không mâu thuẫn: trình duyệt kiểm soát việc JavaScript đọc phản hồi khác origin, còn curl không áp dụng chính sách đọc dữ liệu của trình duyệt. Muốn sửa đúng, cần tìm ra request nào bị chặn và response nào thiếu điều kiện cho phép.

1. Origin không chỉ là tên miền

Origin được xác định bởi giao thức, hostname và port. https://app.examplehttps://api.example khác origin; http://localhost:3000http://localhost:8000 cũng khác. Đường dẫn khác nhau trên cùng origin không tự tạo ra yêu cầu CORS.

CORS dùng HTTP headers để server cho biết origin nào được phép đọc phản hồi qua trình duyệt. Nó không thay thế xác thực, phân quyền hay giới hạn truy cập API từ client ngoài trình duyệt. Tổng quan tại hướng dẫn CORS của MDN.

2. Phân biệt request thật và preflight

Một số request cần trình duyệt gửi OPTIONS trước để hỏi server về method và headers sẽ dùng. Chẳng hạn, GET có header tự đặt X-Client-Version sẽ cần preflight nếu chưa có kết quả phù hợp trong preflight cache. POST dùng Content-Type: application/json cũng thường gặp cơ chế này.

Ví dụ frontend tại https://app.example muốn đọc catalog của https://api.example. Trao đổi dưới đây là minh họa rút gọn, không phải endpoint đang hoạt động:

OPTIONS /api/catalog HTTP/1.1
Host: api.example
Origin: https://app.example
Access-Control-Request-Method: GET
Access-Control-Request-Headers: x-client-version

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: X-Client-Version
Vary: Origin

Chỉ trả mã 204 chưa đủ: các header phải cho phép đúng origin, method và header được yêu cầu. Preflight thành công mới mở đường cho request thật trong trường hợp này. Xem tài liệu preflight.

3. Response thật vẫn cần CORS headers

GET /api/catalog HTTP/1.1
Host: api.example
Origin: https://app.example
X-Client-Version: web-1

HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://app.example
Vary: Origin

{"items":["Guide","Tutorial"]}

Nếu API chỉ thêm headers cho OPTIONS nhưng quên response GET, trình duyệt vẫn không cho JavaScript đọc dữ liệu. Khi hỗ trợ nhiều frontend, server kiểm tra Origin theo allowlist rồi trả một origin hợp lệ; không trả danh sách phân cách bằng dấu phẩy trong Access-Control-Allow-Origin.

Nếu giá trị phản hồi thay đổi theo Origin, dùng Vary: Origin và kiểm tra cấu hình cache/CDN. Không phản chiếu mọi Origin tùy ý. Xem tài liệu Access-Control-Allow-Origin.

4. Cookie cần một nhánh kiểm tra riêng

Với fetch khác origin có cookie, phía client thường cần credentials: "include". Server phải cho phép credentials và dùng origin cụ thể, không dùng wildcard *:

Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Credentials: true
Vary: Origin

Những headers này không ép trình duyệt gửi cookie. Còn phải xét domain, path, Secure, SameSite và chính sách cookie của trình duyệt. Cross-origin cũng không đồng nghĩa hoàn toàn với cross-site. Preflight thông thường không mang cookie đăng nhập, nên đừng yêu cầu session đăng nhập để trả lời OPTIONS; request nghiệp vụ thật vẫn phải xác thực và phân quyền. Xem tài liệu credentials.

5. Vì sao no-cors không phải cách sửa?

Đổi fetch sang mode: "no-cors" không cấp quyền đọc JSON. Với phản hồi khác origin, JavaScript thường nhận response dạng opaque và không đọc được body hay headers theo cách thông thường. Chế độ này còn giới hạn method và headers của request. Dùng nó để che thông báo lỗi sẽ khiến ứng dụng mất dữ liệu cần xử lý. Tham khảo Request.mode.

Cũng không nên xem việc tắt cơ chế bảo vệ của trình duyệt là bản sửa triển khai. Hãy sửa chính sách server hoặc bố trí proxy cùng origin phù hợp với kiến trúc sản phẩm.

6. Quy trình tìm lỗi có thể lặp lại

  1. Ghi lại origin thật: dùng địa chỉ trang frontend đang mở, bao gồm giao thức và port.
  2. Mở Network và Console: xác định OPTIONS có xuất hiện không; nếu có, kiểm tra status, redirect và response headers.
  3. Đối chiếu method và headers: phân biệt Access-Control-Request-Headers do browser gửi với Access-Control-Allow-Headers do server trả.
  4. Kiểm tra request thật: nếu GET/POST đã chạy nhưng JavaScript không đọc được phản hồi, kiểm tra CORS headers ở response đó, kể cả response lỗi.
  5. Kiểm tra từng tầng: xác định headers được thêm ở ứng dụng, reverse proxy hay CDN; tránh nhiều tầng cùng tạo header Allow-Origin trùng nhau.
  6. Thử origin ngoài allowlist: bảo đảm nó không được cấp quyền đọc và endpoint vẫn áp dụng quyền truy cập nghiệp vụ.

Để quan sát preflight bằng curl, thay URL bằng API thử nghiệm do bạn quản lý:

curl -i -X OPTIONS "https://api.example/api/catalog" -H "Origin: https://app.example" -H "Access-Control-Request-Method: GET" -H "Access-Control-Request-Headers: x-client-version"

Trong PowerShell có thể dùng curl.exe. Lệnh này giúp xem headers, không chứng minh trình duyệt đã chấp nhận CORS. Bước xác nhận cuối vẫn là gọi từ đúng frontend và đọc được phản hồi trong trình duyệt.

7. Hai chi tiết dễ bỏ sót

Nếu JavaScript cần đọc header riêng như X-Request-Id, server cần khai báo Access-Control-Expose-Headers: X-Request-Id. Allow-Headers cho phép gửi request header; Expose-Headers cho phép đọc response header. Xem tài liệu Expose-Headers.

Access-Control-Max-Age có thể giảm số lần preflight bằng cách cache kết quả cho phép, nhưng trình duyệt có giới hạn riêng. Khi đổi cấu hình, kết quả cache cũ có thể khiến việc đối chiếu khó hơn; đừng mặc định không thấy OPTIONS là lỗi. Xem tài liệu Max-Age.

8. Giữ đúng ranh giới trách nhiệm

CORS không phải biện pháp chống CSRF đầy đủ. Một số request có thể được gửi trước khi trình duyệt quyết định có cho JavaScript đọc phản hồi hay không. Thao tác thay đổi dữ liệu cần cơ chế bảo vệ phù hợp, đặc biệt khi dùng cookie; xem hướng dẫn phòng chống CSRF của OWASP.

Khi điều tra, hãy ghi riêng ba kết quả: server có nhận request không, server có xử lý thành công không và JavaScript có được đọc response không. Tách ba câu hỏi này sẽ giúp đội frontend và backend tìm đúng tầng cần sửa, thay vì chỉ thêm wildcard mỗi khi thấy chữ CORS.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.