CORS và proxy: preflight, relay allowlist và lab hai origin
CORS và proxy là hai thứ hay bị gộp làm một khi trình duyệt báo lỗi "blocked by CORS policy". CORS là luật trình duyệt áp cho JavaScript, còn proxy đổi đường đi của gói tin. ProxyVN dựng hai origin trên máy, chạy 11 phép fetch trong Chrome 152, đọc log máy chủ từng ca để chỉ ra khi nào cần sửa header, khi nào cần relay và vì sao đổi IP không giúp gì.
CORS và proxy: lớp nào quyết định hành vi

Ba lớp khác nhau
Trước khi sửa bất cứ thứ gì, anh em cần biết lỗi đang xảy ra ở lớp nào. CORS, proxy đổi IP và relay phía máy chủ nằm ở ba lớp khác nhau. Nhầm lớp là lý do phổ biến nhất khiến người ta đi mua proxy để sửa lỗi mà proxy không chạm tới.
CORS là luật của trình duyệt
Trình duyệt áp chính sách cùng nguồn gốc cho mã JavaScript chạy trong trang. Nguồn gốc, hay origin, gồm giao thức, tên máy và cổng. http://127.0.0.1:9500 và http://localhost:9501 là hai origin khác nhau, dù cùng trỏ về một máy.
Theo hướng dẫn CORS của MDN, máy chủ dùng các header Access-Control-* để cho phép một origin khác đọc phản hồi. Trình duyệt mới là bên kiểm các header đó. Máy chủ không chặn gì cả, trình duyệt chặn JavaScript đọc phản hồi khi thiếu header cho phép.
Forward proxy chỉ đổi đường đi
Forward proxy như bài forward proxy là gì mô tả nhận request từ máy khách rồi gửi tiếp tới máy chủ. Máy chủ thấy IP của proxy thay cho IP người dùng. Origin của trang vẫn giữ nguyên, header Origin trình duyệt gửi đi cũng giữ nguyên.
Vì vậy cài proxy cho trình duyệt không làm phản hồi có thêm Access-Control-Allow-Origin. Trình duyệt vẫn kiểm cùng các header như khi đi thẳng. Lỗi CORS còn nguyên, chỉ khác là máy chủ ghi một IP khác vào log.
Relay là máy chủ của chính anh em
Relay, hay reverse proxy phía ứng dụng, là một đường dẫn trên chính origin của trang. JavaScript gọi /relay/books cùng origin, máy chủ của anh em gọi tiếp API bên kia từ phía server. Request từ server sang server không đi qua trình duyệt nên không có kiểm tra CORS.
Đây là cách hợp lệ khi API bên kia không cho phép origin của anh em mà anh em vẫn có quyền dùng API đó. Cái giá là relay trở thành một điểm phải bảo vệ. Một relay nhận URL bất kỳ là open relay, ai cũng mượn được máy chủ của anh em để gọi đi nơi khác.
Ranh giới với bài đã có
Bài Ollama proxy chỉ nhắc biến OLLAMA_ORIGINS của riêng Ollama. Bài lỗi 405 Method Not Allowed khi crawl có nhắc preflight như một nguyên nhân gần. Bài này đi vào cơ chế CORS, đo trên hai origin thật và chỉ ra ranh giới với proxy đổi IP.
Phân biệt CORS và proxy đổi IP với relay có allowlist
Ba khái niệm này hay được dùng lẫn trong các diễn đàn, nên phần này đặt chúng cạnh nhau. Bảng dưới so theo những câu hỏi anh em thật sự cần trả lời khi gặp lỗi. Sau bảng là các trường hợp hay nhầm.
Câu hỏi |
CORS |
Forward proxy đổi IP |
Relay có allowlist |
|---|---|---|---|
Ai thực thi |
Trình duyệt |
Máy chủ proxy |
Máy chủ của anh em |
Áp cho ai |
JavaScript trong trang |
Mọi client cấu hình proxy |
Request đi qua đường dẫn relay |
Đổi IP thấy ở máy chủ đích |
Không |
Có |
Có, thành IP máy chủ relay |
Sửa được lỗi "blocked by CORS policy" |
Sửa header phía API |
Không |
Có, vì request thành cùng origin |
Rủi ro chính |
Mở quá rộng origin |
Lộ tài khoản proxy |
Thành open relay, SSRF |
Các trường hợp hay nhầm
Trường hợp thứ nhất là dùng curl hay Python thấy chạy được, rồi kết luận API không có vấn đề. curl không có khái niệm origin của trang nên không kiểm CORS. Kết quả curl đúng chỉ chứng minh máy chủ trả dữ liệu, không chứng minh trình duyệt cho JavaScript đọc.
Trường hợp thứ hai là tưởng đổi IP sẽ "vượt" CORS. CORS không xét IP người gọi, mà xét origin của trang và header phản hồi. Lab bên dưới gọi qua proxy có xác thực và nhận đúng phản hồi thiếu header như khi gọi thẳng.
Trường hợp thứ ba là dùng các dịch vụ "CORS proxy" công khai, loại nhận URL rồi thêm header dấu sao. Dữ liệu và cookie của người dùng đi qua máy chủ của bên thứ ba. Với ứng dụng thật, anh em nên tự dựng relay có allowlist thay vì gửi dữ liệu qua dịch vụ không kiểm soát.
CORS chỉ được sửa ở hai chỗ: header của API đích, hoặc đưa request về cùng origin bằng relay. Mọi cách khác đều là đổi chỗ vấn đề, không giải nó.
Thiết kế lab CORS và proxy: hai origin, một relay, một forward proxy
Lab đặt cả hai phía trên một máy để mọi bên đều có log. Mục tiêu là đặt cạnh nhau phản hồi máy chủ thật sự gửi và điều trình duyệt cho JavaScript thấy. Cổng dùng trong lab là 9500 tới 9505.
Thành phần và phiên bản
- Origin A: http://127.0.0.1:9500, máy chủ Python 3.12.10 phục vụ trang có hàm fetch thử, kèm đường dẫn /relay.
- API B: http://localhost:9501, máy chủ Python trả JSON, mỗi đường dẫn có cấu hình CORS khác nhau.
- Forward proxy HTTP có xác thực ở cổng 9503, dùng script lab của ProxyVN.
- Trình duyệt: khung trình duyệt của ứng dụng Claude, chuỗi user agent báo Chrome 152.0.7977.167 trên Windows 10.
- curl 8.21.0 bản mingw để gọi ngoài trình duyệt.
Cấu hình CORS của API B
Mỗi đường dẫn của B mô phỏng một kiểu cấu hình hay gặp. /api/ok chỉ trả Access-Control-Allow-Origin khi Origin đúng là http://127.0.0.1:9500, kèm Vary: Origin. /api/nocors không trả header CORS nào. /api/star trả dấu sao.
/api/cred phản chiếu nguyên giá trị Origin kèm Access-Control-Allow-Credentials true, kiểu cấu hình nguy hiểm vẫn gặp trong thực tế. /api/put trả lời preflight cho phép GET và PUT, cho phép hai header content-type và x-lab-token, Max-Age 60 giây. Đoạn mã xử lý CORS của B rút gọn như sau.
def _cors(self):
o = self.headers.get("Origin")
p = self.path.split("?")[0]
if p in ("/api/ok", "/api/put"):
return {"Access-Control-Allow-Origin": o, "Vary": "Origin"} if o == A_ORIGIN else {}
if p == "/api/star":
return {"Access-Control-Allow-Origin": "*"}
if p == "/api/cred": # phan chieu Origin: chi de minh hoa, khong dung that
return {"Access-Control-Allow-Origin": o or "",
"Access-Control-Allow-Credentials": "true", "Vary": "Origin"}
return {}
Relay chỉ cho đường dẫn đã khai báo
Relay trên A không nhận URL từ client. Nó giữ một từ điển ánh xạ từ đường dẫn công khai sang URL phía sau. Đường dẫn nào không có trong từ điển thì trả 403, kèm một dòng log để biết ai đang thử.
RELAY_ALLOW = {"/relay/books": "http://localhost:9501/api/nocors"}
up = RELAY_ALLOW.get(self.path.split("?")[0])
if not up:
log(kind="relay-deny", path=self.path)
return self._send(403, "relay: duong dan khong nam trong allowlist")
with urllib.request.urlopen(up, timeout=5) as r:
data = r.read()
Thiết kế này cố ý không chuyển tiếp query string, cookie hay header Authorization của client. Muốn chuyển gì thì khai báo rõ trong mã. Một relay chuyển tiếp mọi thứ sẽ nhận luôn cookie phiên của người dùng và gửi sang bên thứ ba.
Kết quả kỳ vọng
Trước khi chạy, ProxyVN ghi kết quả kỳ vọng theo tài liệu MDN cho từng ca. Ca đúng origin phải đọc được, ca thiếu header phải lỗi, ca dấu sao kèm credentials phải lỗi. Ca PUT có header lạ phải dừng ở preflight, và relay phải trả dữ liệu cho đường dẫn đã khai báo.
Tái lập luồng CORS và proxy: ca thành công và ca lỗi có kiểm soát
Phép thử chạy 11 lệnh fetch từ trang của origin A, trong cùng một phiên trình duyệt ngày 29/10/2026. Mỗi lệnh ghi kết quả JavaScript nhận được. Máy chủ B ghi mọi request tới, nên có thể đặt hai phía cạnh nhau.
Mã chạy trong trang
Hàm run bọc fetch, trả về trạng thái hoặc tên lỗi. Các lệnh gọi dưới đây chạy tuần tự để log dễ đọc.
window.run = async function (name, url, opt) {
try { const r = await fetch(url, opt || {}); const t = await r.text();
return name + " OK " + r.status + " " + t.slice(0, 60); }
catch (e) { return name + " LOI " + e.name + ": " + e.message; }
};
const B = "http://localhost:9501";
await run("2 nocors", B + "/api/nocors");
await run("4 star+credentials", B + "/api/star", {credentials: "include"});
await run("6 put-preflight-dung", B + "/api/put", {method: "PUT",
headers: {"Content-Type": "application/json", "X-Lab-Token": "abc"}, body: "{}"});
await run("7 put-header-la", B + "/api/put", {method: "PUT", headers: {"X-Other": "1"}, body: "{}"});
await run("10 relay-deny", "/relay?url=http://example.com");
Bảng kết quả trong Chrome 152
Ca |
Kết quả JavaScript |
Request thật tới B |
Thông báo console |
|---|---|---|---|
1. /api/ok, đúng origin |
200, đọc được |
GET |
Không |
2. /api/nocors |
TypeError: Failed to fetch |
GET, B trả 200 |
No 'Access-Control-Allow-Origin' header |
3. /api/star |
200, đọc được |
GET |
Không |
4. /api/star kèm credentials |
TypeError |
GET, B trả 200 |
Không được dùng dấu sao khi credentials là include |
5. /api/cred phản chiếu |
200, đọc được |
GET |
Không |
6. PUT có header được phép |
200, đọc được |
OPTIONS rồi PUT |
Không |
7. PUT có header X-Other |
TypeError |
Chỉ OPTIONS |
x-other không có trong Access-Control-Allow-Headers |
8. PUT tới /api/ok |
TypeError |
Chỉ OPTIONS |
PUT không có trong Access-Control-Allow-Methods |
9. /relay/books |
200, đọc được |
GET từ phía server, không có Origin |
Không |
10. /relay?url=... |
403 do relay |
Không |
Phản hồi 403 |
11. /api/nocors, mode no-cors |
Status 0, thân rỗng |
GET |
Không |
Thông báo console ở cột cuối là bản rút gọn của dòng lỗi thật. Ví dụ ca 4, Chrome ghi rằng giá trị Access-Control-Allow-Origin không được là dấu sao khi chế độ credentials của request là include. Ca 7 ghi request header field x-other không được phép bởi Access-Control-Allow-Headers trong phản hồi preflight.
Ca 2 và ca 4: máy chủ đã trả lời

Máy chủ đã trả lời
Điểm cần nhìn kỹ là ca 2 và ca 4. Log của B ghi đủ hai request GET, B đã trả 200 kèm dữ liệu. Trình duyệt nhận đủ phản hồi rồi mới quyết định không đưa cho JavaScript.
Lỗi CORS ở request đơn giản không có nghĩa request không tới máy chủ. Máy chủ đã chạy xong việc, chỉ JavaScript không được đọc kết quả.
Hệ quả thực tế là request GET có tác dụng phụ vẫn thực hiện tác dụng phụ đó, dù trang báo lỗi. Đây là lý do MDN nhắc máy chủ vẫn phải tự chống CSRF. CORS không phải hàng rào chặn request tới, nó là luật đọc phản hồi.
Ca 7 và ca 8: dừng ở preflight

Preflight bị từ chối
Hai ca PUT bị lỗi khác hẳn. Log của B chỉ có OPTIONS, không có PUT. Trình duyệt hỏi trước bằng preflight, thấy phương thức hoặc header không được phép, nên không gửi request thật.
Log preflight còn cho biết trình duyệt định gửi gì. Ca 6 có Access-Control-Request-Headers là content-type,x-lab-token. Ca 7 là x-other. Ca 8 là content-type, vì application/json không thuộc ba kiểu Content-Type được miễn preflight.
Theo MDN và chuẩn Fetch của WHATWG, nơi hiện định nghĩa CORS, ba kiểu đó là application/x-www-form-urlencoded, multipart/form-data và text/plain. Đặt Content-Type là application/json là đủ để có preflight. Nhiều lỗi "GET chạy, POST JSON không chạy" bắt nguồn từ đây.
Ca 5: chạy được nhưng sai thiết kế
Ca 5 thành công vì B phản chiếu nguyên Origin kèm Allow-Credentials. Trình duyệt hài lòng vì giá trị khớp origin của trang. Nhưng cấu hình này cho phép mọi website đọc dữ liệu có cookie của người dùng, tương đương mở toang API.
Ca này nằm trong lab để anh em thấy "chạy được" không đồng nghĩa "đúng". Cách đúng là so Origin với danh sách cố định như /api/ok đang làm. Origin nào không có trong danh sách thì không trả header.
Forward proxy trong lab CORS và proxy: đổi IP không đổi kết quả
Ca tiếp theo kiểm câu hỏi chính của bài: gọi qua proxy có khác gì không. Vì khung trình duyệt của lab không cho đặt proxy riêng, ProxyVN dùng curl qua proxy có xác thực. Ý định là xem máy chủ trả gì khi request đi qua proxy.
Lệnh và phản hồi
curl.exe -s -i -x "http://user1:p%40ss%232026@127.0.0.1:9503" ^
-H "Origin: http://127.0.0.1:9500" http://127.0.0.1:9501/api/nocors
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 47
{"api": "B", "path": "/api/nocors", "books": 3}
Log proxy ghi request GET tới /api/nocors, xác thực đạt. Log B ghi request có Origin http://127.0.0.1:9500. Phản hồi vẫn không có Access-Control-Allow-Origin, giống hệt khi gọi thẳng.
Nếu đây là trình duyệt, kết quả vẫn là lỗi CORS như ca 2. Proxy chuyển tiếp nguyên header, nên điều kiện trình duyệt kiểm không đổi. curl đọc được chỉ vì curl không kiểm CORS.
Khi nào proxy có mặt trong câu chuyện CORS
Proxy liên quan tới CORS ở một trường hợp: proxy công ty can thiệp phản hồi, ví dụ bỏ header hoặc trả trang chặn. Khi đó phản hồi trình duyệt nhận khác phản hồi máy chủ gửi. Lỗi CORS xuất hiện vì trang chặn của proxy không có header cho phép.
Cách nhận biết là so thân phản hồi trong tab Network với phản hồi khi gọi thẳng. Nếu thân là trang HTML của proxy thay vì JSON, nguyên nhân nằm ở proxy. Các bài về no_proxy hướng dẫn cách cho tên miền nội bộ đi thẳng khi cần.
Crawler phía server không cần lo CORS
Script Python, Node hay Go chạy trên máy chủ không bị CORS. Nếu anh em đang crawl dữ liệu bằng script và cần đổi IP, proxy là công cụ đúng. Lỗi CORS chỉ xuất hiện khi logic đó chạy trong trang web, trên trình duyệt người dùng.
Với crawler cần IP Việt Nam ổn định qua HTTP hoặc SOCKS5, anh em có thể mua proxy tốc độ cao tại trang chủ ProxyVN, dùng proxy dân cư tĩnh, datacenter hoặc proxy xoay tùy mức chặn của trang đích. Còn trang web gọi API khác origin thì vẫn phải sửa header hoặc dựng relay như bài này.
Đọc log và phản hồi để phân biệt lỗi CORS và proxy
Thông báo "Failed to fetch" của JavaScript giống nhau cho mọi ca lỗi. Muốn biết nguyên nhân, anh em phải đọc ở ba chỗ khác: console, tab Network và log máy chủ. Bảng dưới gom dấu hiệu của từng nguyên nhân gần nhau.
Dấu hiệu |
Nguyên nhân có khả năng |
Bước kiểm tiếp |
|---|---|---|
Console ghi blocked by CORS policy, máy chủ có log 200 |
Thiếu header cho phép |
Thêm origin vào danh sách phía API |
Chỉ thấy OPTIONS trong log máy chủ |
Preflight bị từ chối |
Đọc Access-Control-Request-Method và Request-Headers |
Console ghi lỗi dấu sao với credentials |
Allow-Origin dấu sao kèm cookie |
Trả origin cụ thể, thêm Allow-Credentials |
net::ERR_PROXY_CONNECTION_FAILED |
Proxy chết hoặc sai cổng |
Kiểm proxy, không liên quan CORS |
Phản hồi là trang HTML lạ |
Proxy hoặc tường lửa chặn |
So với phản hồi khi đi thẳng |
Console ghi violates Content Security Policy |
CSP connect-src của trang |
Sửa CSP, không phải CORS |
Máy chủ không có log nào |
DNS, mạng, CSP hoặc tiện ích chặn |
Thử curl tới cùng URL |
Console và tab Network
Console Chrome ghi rõ lý do bị chặn, kèm tên header thiếu. Đây là nguồn đầu tiên nên đọc. Tab Network hiện cả request OPTIONS, anh em bấm vào để xem header phản hồi preflight.
Request bị CORS chặn trong tab Network thường có trạng thái "CORS error". Anh em vẫn xem được header phản hồi ở đó, dù JavaScript không đọc được. So header thật với điều kiện trong bảng là cách nhanh nhất tìm chỗ sai.
Log máy chủ
Log máy chủ trả lời câu hỏi request có tới hay không. Lab ghi Origin, Sec-Fetch-Site và Sec-Fetch-Mode cho mỗi request. Request fetch từ trang có Sec-Fetch-Mode là cors, request từ relay không có các header này.
Ca 11 dùng mode no-cors, log B ghi Sec-Fetch-Mode là no-cors và không có Origin. JavaScript nhận phản hồi opaque, status 0, thân rỗng. no-cors không phải cách lách CORS, nó chỉ cho gửi request mà không đọc được gì.
Phân biệt với CSP connect-src
Một lỗi nhìn rất giống CORS là trang bị Content Security Policy chặn kết nối. Với CSP, trình duyệt chặn trước khi gửi, nên log máy chủ trống. Console ghi violates the following Content Security Policy directive thay vì blocked by CORS policy.
Log máy chủ trống là dấu hiệu nghiêng về CSP, log có 200 là dấu hiệu nghiêng về CORS. Cách đọc chi tiết CSP nằm ở bài riêng về lỗi CSP connect-src của ProxyVN.
Cấu hình relay CORS và proxy an toàn cho endpoint được phép
Khi đã quyết định dùng relay, phần cấu hình quyết định relay an toàn hay thành lỗ hổng. Phần này đưa cấu hình Nginx mẫu và những điều relay không nên làm. Cấu hình Nginx chưa được chạy trong lab, ProxyVN ghi rõ là ví dụ theo tài liệu.
Nginx với location cố định
Mỗi API được phép có một location riêng, đích proxy_pass viết cứng. Client không có cách nào đổi đích. Theo tài liệu proxy_pass của Nginx, proxy_pass có URI phía sau sẽ thay phần đường dẫn khớp location.
location = /relay/books {
limit_except GET { deny all; }
proxy_pass https://api.vi-du.test/v1/books;
proxy_set_header Host api.vi-du.test;
proxy_set_header Cookie "";
proxy_set_header Authorization "";
proxy_ssl_server_name on;
proxy_connect_timeout 5s;
proxy_read_timeout 15s;
}
location /relay/ { return 403; }
limit_except chỉ cho GET qua. Hai dòng proxy_set_header xóa Cookie và Authorization của người dùng trước khi gửi đi. location /relay/ cuối cùng trả 403 cho mọi đường dẫn relay chưa khai báo.
Những điều relay không nên làm

Relay chỉ cho đường dẫn đã khai báo
- Không nhận URL đích từ query string hay thân request.
- Không chuyển tiếp cookie phiên của người dùng sang API bên thứ ba.
- Không gọi tới địa chỉ nội bộ như 127.0.0.1, 169.254.169.254 hay dải 10.0.0.0/8 khi đích có thể thay đổi.
- Không trả nguyên header của API bên kia nếu có Set-Cookie của tên miền khác.
- Không để relay không giới hạn tốc độ, mỗi người dùng nên có trần request.
Mục thứ ba liên quan tới tấn công SSRF, được OWASP mô tả trong bảng tóm tắt phòng chống SSRF. Relay nhận URL tùy ý có thể bị dùng để đọc dịch vụ nội bộ hoặc metadata của máy ảo đám mây. Allowlist cố định chặn được lớp tấn công này ngay từ thiết kế.
Khóa API và relay
Một lý do tốt để dùng relay là giấu khóa API khỏi trình duyệt. Relay thêm khóa ở phía server, JavaScript không bao giờ thấy khóa. Đây cũng là cách nên làm với mọi dịch vụ trả Access-Control-Allow-Origin dấu sao nhưng yêu cầu khóa.
Khi relay thêm khóa, mỗi người dùng trang đều dùng chung quota của khóa đó. Anh em cần giới hạn tốc độ theo người dùng ở relay. Thiếu giới hạn, một người có thể đốt hết quota của cả ứng dụng.
Relay và proxy đổi IP cùng lúc
Relay chạy trên máy chủ có thể dùng proxy cho request đi ra, ví dụ khi API đích chỉ cho phép dải IP nhất định. Proxy lúc này nằm sau relay, ở chặng server tới API. CORS đã được giải ở chặng trình duyệt tới relay.
Trong Nginx, chặng đi ra qua proxy HTTP không phải cấu hình mặc định. Thường anh em đặt một tiến trình relay viết bằng Python hay Node, dùng biến HTTPS_PROXY cho chặng đi ra. Bài định dạng proxy có cách viết URL proxy kèm tài khoản cho các thư viện này.
Sửa cấu hình CORS và proxy phía API theo bằng chứng
Khi anh em là chủ API, sửa header là cách gọn nhất. Phần này đi qua từng header theo thứ tự hay sai. Mỗi chỉnh sửa nên kèm một ca thử trong lab để xác nhận.
Access-Control-Allow-Origin và Vary
Trang tham chiếu Access-Control-Allow-Origin của MDN ghi header này chỉ nhận một origin, dấu sao hoặc null. Trả đúng origin đã kiểm, không phản chiếu tùy ý. Khi giá trị thay đổi theo Origin, thêm Vary: Origin để cache không trả nhầm phản hồi của origin này cho origin khác. Bài Vary header khi dùng proxy cache giải thích vì sao thiếu Vary làm cache trả sai.
Dấu sao chỉ hợp cho dữ liệu công khai, không có cookie. Theo MDN, request có credentials không được nhận phản hồi Allow-Origin dấu sao. Ca 4 của lab cho thấy Chrome chặn đúng như vậy.
Preflight: Methods, Headers và Max-Age
Phản hồi OPTIONS phải liệt kê phương thức và header mà ứng dụng thật sự dùng. Đọc Access-Control-Request-Headers trong log để biết trình duyệt định gửi header nào. Đừng trả dấu sao cho Allow-Headers khi có credentials, MDN ghi rõ dấu sao không được dùng trong trường hợp đó.
Access-Control-Max-Age cho trình duyệt lưu kết quả preflight. Lab đặt 60 giây để dễ thử lại. Trên hệ thống thật có thể đặt dài hơn, nhưng trình duyệt có trần riêng nên giá trị quá lớn không có tác dụng thêm.
Expose-Headers
Mặc định JavaScript chỉ đọc được vài header an toàn của phản hồi khác origin. Header riêng như mã request hay số credit còn lại phải liệt kê trong Access-Control-Expose-Headers. Phản hồi thật của ScrapingBee ở bài khác là ví dụ, họ liệt kê năm header Spb-* trong Expose-Headers.
Thiếu Expose-Headers không gây lỗi CORS, chỉ làm r.headers.get trả null. Trong lab, B không khai báo Expose-Headers, nên JavaScript chỉ đọc được Content-Type và Content-Length của phản hồi, đúng nhóm header an toàn mặc định. Lỗi này hay bị bỏ qua vì request vẫn thành công. Kiểm bằng cách in toàn bộ header JavaScript đọc được.
CORS và proxy của dev server khi phát triển frontend
Nhiều anh em gặp CORS lần đầu khi chạy frontend ở cổng 5173 và backend ở cổng 8000 trên cùng máy. Hai cổng là hai origin, nên mọi fetch từ frontend sang backend đều bị kiểm CORS. Các công cụ build frontend có sẵn tính năng proxy để xử lý đúng trường hợp này.
Proxy của dev server là relay thu nhỏ
Tính năng proxy của dev server hoạt động giống relay ở trên. Trình duyệt gọi /api trên cổng của dev server, cùng origin với trang. Dev server nhận request rồi gửi tiếp sang backend từ phía Node, không qua kiểm CORS của trình duyệt.
Theo tài liệu server.proxy của Vite, cấu hình là một bảng ánh xạ từ tiền tố đường dẫn sang đích. Đoạn dưới là ví dụ theo tài liệu, ProxyVN chưa chạy Vite trong lab này.
// vite.config.js
export default {
server: {
proxy: {
"/api": {
target: "http://127.0.0.1:8000",
changeOrigin: true,
},
},
},
}
changeOrigin đổi header Host của request đi ra thành host của đích. Một số backend kiểm Host và từ chối request mang Host của dev server. Tùy chọn này không thêm header CORS nào vào phản hồi.
Vì sao chạy được ở máy, lỗi khi lên môi trường thật
Proxy của dev server chỉ tồn tại khi chạy lệnh dev. Khi build ra tệp tĩnh và đặt lên máy chủ thật, không còn ai chuyển /api sang backend. Nếu frontend và backend nằm ở hai tên miền khác nhau, lỗi CORS quay lại ngay lần đầu triển khai.
Cấu hình proxy của dev server không đi theo bản build, nên môi trường thật cần relay riêng hoặc header CORS đúng. Cách gọn nhất là đặt frontend và backend sau cùng một reverse proxy như Nginx, mỗi bên một tiền tố đường dẫn. Khi đó cả hai cùng origin và CORS không còn liên quan.
Biến môi trường proxy của máy phát triển
Dev server gửi request đi ra bằng Node. Nếu máy có biến HTTP_PROXY trỏ tới proxy công ty, một số công cụ sẽ cho cả request tới 127.0.0.1 đi qua proxy. Proxy công ty không tới được máy của anh em, nên dev server báo lỗi kết nối, dễ bị nhầm với lỗi CORS.
Cách xử lý là thêm localhost và 127.0.0.1 vào NO_PROXY. Lỗi kiểu này hiện trong terminal của dev server, không hiện trong console trình duyệt. Đọc đúng chỗ giúp anh em không mất công sửa header CORS cho một lỗi mạng.
Checklist CORS và proxy trước khi đưa lên môi trường thật
Danh sách sau gom những bước đã dùng trong lab. Mỗi dòng ứng với một ca lỗi có thật ở trên. Anh em dò theo thứ tự khi gặp lỗi mới.
- Xác định origin của trang: giao thức, tên máy, cổng. localhost và 127.0.0.1 là hai origin.
- Đọc nguyên văn thông báo console, ghi tên header bị nhắc tới.
- Kiểm log máy chủ: có GET, chỉ có OPTIONS, hay không có gì.
- Nếu chỉ có OPTIONS, so Request-Method và Request-Headers với phản hồi preflight.
- Nếu có credentials, Allow-Origin phải là origin cụ thể kèm Allow-Credentials true.
- Nếu log trống, kiểm CSP connect-src và kết nối mạng trước khi nghĩ tới CORS.
- Không dùng proxy đổi IP để sửa lỗi CORS.
- Relay chỉ dùng allowlist cố định, xóa cookie và Authorization của người dùng.
- Ghi lại phiên bản trình duyệt khi báo lỗi, vì thông báo console khác nhau giữa các bản.
Giới hạn của lab
Lab chạy trên một trình duyệt nhân Chromium bản 152 và máy chủ Python. Chưa thử Firefox, Safari, HTTPS, cookie SameSite và Private Network Access với địa chỉ công khai. Cấu hình Nginx là mẫu theo tài liệu, chưa chạy. Ca forward proxy dùng curl vì khung trình duyệt lab không đặt được proxy riêng.
Một chi tiết phụ quan sát được trong lab: relay gọi localhost mất khoảng 2 giây cho request đầu, trong khi gọi thẳng chỉ vài mili giây. Máy chủ B chỉ nghe IPv4, còn localhost có thể phân giải ra ::1 trước. Đổi đích relay sang 127.0.0.1 tránh độ trễ này.
Câu hỏi thường gặp về CORS và proxy
Cài proxy cho trình duyệt có hết lỗi CORS không?
Không. Proxy không thêm header Access-Control-Allow-Origin và không đổi origin của trang. Lỗi CORS vẫn còn nguyên.
Vì sao Postman và curl gọi được mà trình duyệt thì không?
Postman và curl không chạy trong trang web nên không kiểm CORS. Trình duyệt kiểm vì phải bảo vệ dữ liệu của người dùng khỏi trang khác.
Có nên dùng tiện ích tắt CORS khi phát triển?
Chỉ dùng tạm trên máy phát triển với hồ sơ trình duyệt riêng. Tắt CORS làm mọi trang khác cũng đọc được dữ liệu từ các API anh em đang đăng nhập.
Relay có làm chậm ứng dụng không?
Có thêm một chặng mạng. Nếu relay và API cùng vùng, độ trễ thêm thường nhỏ. Lab ghi độ trễ lớn chỉ do localhost phân giải IPv6 trước.
mode no-cors có giúp đọc dữ liệu không?
Không. Phản hồi no-cors là opaque, JavaScript nhận status 0 và thân rỗng. Nó chỉ dùng cho request không cần đọc kết quả.
CORS và proxy: những điều cần nhớ
CORS và proxy nằm ở hai lớp khác nhau: trình duyệt quyết định JavaScript có được đọc phản hồi, còn proxy chỉ đổi đường đi. Sửa lỗi bằng header đúng origin, preflight đủ phương thức và header, hoặc relay có allowlist cố định. Cần proxy ổn định cho crawler phía server sau khi đã tách xong CORS và proxy, liên hệ Proxy.vn - Nhà cung cấp dịch vụ proxy chất lượng hàng đầu Việt Nam.