Gọi API xong console đỏ rực dòng CORS: chuyện quen thuộc với dev .NET
Bạn vừa thử gọi API từ frontend thì console hiện ngay dòng lỗi CORS màu đỏ. Request bị chặn, dữ liệu không về. Thế nhưng log phía server vẫn sạch sẽ, không có gì bất thường.
Cảm giác này quen lắm. Gần như dev .NET nào làm dự án thật cũng đã gặp ít nhất một lần. Đội ngũ chúng tôi cũng từng như vậy. Có lần cả buổi chiều chúng tôi ngồi cùng một bạn junior chỉ để tìm hiểu vì sao API chạy ngon trên Postman lại chết cứng khi gọi từ trình duyệt. Lỗi không nằm ở logic nghiệp vụ. Nó nằm ở một lớp bảo vệ của trình duyệt mà phần lớn chúng ta chưa để ý tới, cho đến khi bị chặn.
CORS là viết tắt của Cross-Origin Resource Sharing. Hiểu đơn giản, đây là cơ chế giúp trình duyệt quyết định một trang web có được phép lấy dữ liệu từ server khác hay không. Nếu domain của frontend khác domain của API, trình duyệt sẽ kiểm tra trước khi cho request đi qua.
Nửa ngày debug mà không biết mình đang tìm gì
Nhiều bạn mất nửa ngày, thậm chí cả ngày, chỉ để xử lý lỗi này mà chưa hiểu gốc rễ nằm ở đâu. Phần lớn thời gian trôi qua trong việc đoán mò: đổi URL thử xem, tắt firewall thử xem, thậm chí sửa cả những đoạn code chẳng liên quan gì đến CORS.
Lý do khá đơn giản. Không ai nói rõ từ đầu rằng đây là lỗi phía trình duyệt. Nó không phải lỗi của server, cũng không hẳn là lỗi ở chỗ gọi API. Khi đã nắm được điều này, hướng xử lý sẽ khác hẳn.
Thêm một yếu tố làm mọi chuyện phức tạp hơn: mỗi dự án có kiểu tích hợp riêng. Có team đặt frontend và backend chung một domain. Có team tách hẳn hai domain, hoặc đưa API lên subdomain riêng cho gọn. Mỗi kiểu setup kéo theo một cách cấu hình CORS khác nhau. Thiếu kiến thức nền ở đây chính là lý do không ít ứng dụng dính lỗi ngay khi bắt đầu tích hợp.
Vì sao trình duyệt chặn request khi frontend và API khác domain
Muốn xử lý được lỗi CORS, trước hết phải hiểu vì sao trình duyệt lại chặn. Đây không phải lỗi ngẫu nhiên mà là hành vi bảo mật được thiết kế có chủ đích. Trình duyệt mặc định áp dụng quy tắc same-origin policy: một trang chỉ được gọi dữ liệu từ đúng domain, đúng cổng và đúng giao thức mà nó đang chạy.
Hai origin tưởng cùng nhà nhưng trình duyệt coi là người lạ
Lấy một ví dụ dễ hình dung: frontend chạy ở app.vidu.com, còn API chạy ở api.vidu.com. Dù cùng một công ty vận hành, với trình duyệt đây vẫn là hai origin khác nhau. Nếu không có cơ chế cho phép, trình duyệt sẽ chặn request gửi từ app.vidu.com sang api.vidu.com.
Lý do rất thực tế. Nếu không chặn, một trang lạ nào đó có thể âm thầm gọi API nội bộ của công ty rồi lấy dữ liệu của người dùng đang đăng nhập. Quy tắc này bảo vệ người dùng, dù đôi khi nó khiến chúng ta khá khó chịu.
Vấn đề là nhiều API ASP.NET khi mới dựng chưa khai báo domain nào được phép gọi vào. Mặc định, ASP.NET không tự mở cửa cho ai. Đây là lựa chọn an toàn, nhưng dev mới dễ tưởng server đang lỗi. Request gửi lên, ASP.NET nhận và xử lý xong. Tuy nhiên response thiếu header Access-Control-Allow-Origin, nên trình duyệt chặn kết quả và không cho JavaScript đọc dữ liệu.
Preflight: câu hỏi trình duyệt gửi trước khi request thật
Có một chi tiết hay bị bỏ qua. Với các request như POST, PUT, DELETE, hoặc request kèm custom header, trình duyệt sẽ gửi trước một request dò đường gọi là preflight, dùng phương thức OPTIONS. Nó hỏi server một câu ngắn: sắp gửi request kiểu này, bạn có đồng ý không?
Nếu ASP.NET chưa cấu hình để trả lời, request thật sẽ không bao giờ được gửi đi. Lỗi CORS xuất hiện ngay cả khi endpoint hoàn toàn không có vấn đề gì. Chúng tôi từng gặp trường hợp dev đã sửa xong logic nhưng vẫn loay hoay cả buổi vì quên mất bước preflight này.
Preflight cần thiết cho bảo mật, nhưng cũng có cái giá. Nếu trình duyệt phải hỏi lại cho mọi request, mỗi lần gọi API sẽ tốn thêm một vòng round-trip. ASP.NET cho phép cache kết quả preflight trong một khoảng thời gian, nhờ đó trình duyệt không phải hỏi liên tục. Đây là điểm dễ bị bỏ qua khi tối ưu tốc độ phản hồi API. Nếu bạn muốn đo đạc và tinh chỉnh cẩn thận hơn, có thể tham khảo thêm các lưu ý về debug và tối ưu mã cho dev .NET.
Cách xử lý lỗi CORS trong ASP.NET Core
Tin tốt là xử lý lỗi CORS trong ASP.NET không khó, miễn là hiểu đúng bản chất. Việc cần làm là khai báo rõ những domain nào được phép gọi vào API. Thay vì để trình duyệt tự chặn vì thiếu thông tin, ASP.NET Core đã có sẵn middleware CORS. Bạn chỉ cần cấu hình đúng chỗ và đúng thứ tự là chạy.
Ba bước cấu hình CORS đúng ngay từ đầu
Bước đầu tiên là đăng ký chính sách trong phần khởi tạo dịch vụ, thường gọi bằng AddCors. Ở đây bạn đặt tên cho chính sách rồi liệt kê các domain frontend được phép truy cập bằng WithOrigins.
Nhiều bạn mới hay dùng AllowAnyOrigin cho nhanh. Cách này chạy được lúc test, nhưng lên production thì rủi ro, vì nó mở cửa cho mọi domain gọi vào. Đây cũng là điểm chúng tôi thường nhắc lại mỗi khi review code.
Bước thứ hai là thứ tự middleware, và đây là chỗ dễ sai nhất. Dòng UseCors phải đặt sau routing nhưng trước authorization. Đặt sai chỗ, chính sách coi như vô hiệu, dù bạn đã cấu hình đúng ở bước một. Chúng tôi từng thấy một dự án loay hoay cả tuần chỉ vì dòng UseCors nằm lộn xuống dưới UseAuthorization. Cấu hình thì đúng hết, nhưng sai thứ tự nên lỗi vẫn còn nguyên.
Bước thứ ba là xử lý method và header. Nếu API nhận POST kèm header Authorization, chính sách phải khai báo rõ những thứ đó được chấp nhận. Có hai lựa chọn. Một là liệt kê cụ thể bằng WithMethods và WithHeaders. Hai là dùng AllowAnyMethod và AllowAnyHeader cho thoáng hơn. Với API nội bộ ít rủi ro thì cách thoáng cũng ổn. Với API xử lý dữ liệu nhạy cảm, nên liệt kê rõ ràng và đừng mở rộng hơn mức cần thiết.
Cái bẫy khi kết hợp AllowAnyOrigin và AllowCredentials
Một cái bẫy khác khá phổ biến là kết hợp AllowAnyOrigin với AllowCredentials. Hai cấu hình này xung khắc nhau, và ASP.NET sẽ báo lỗi ngay khi ứng dụng khởi động. Nếu bạn cần gửi cookie hoặc token xác thực kèm request, bắt buộc phải khai báo origin cụ thể bằng WithOrigins.
Có một phản xạ sai mà chúng tôi gặp khá thường xuyên: thấy lỗi khó chịu, nhiều bạn vội bọc thêm try-catch quanh đoạn code, tưởng vậy là xong. Nhưng gốc rễ nằm ở cấu hình, không phải ở chỗ cần bắt exception. Nếu muốn phân biệt khi nào nên bắt lỗi thật sự và khi nào chỉ đang né vấn đề, hãy đọc thêm cách xử lý exception đúng cách thay vì try-catch tràn lan.
Domain dev và production hiếm khi giống nhau
Domain frontend ở môi trường dev thường khác hẳn lúc lên production. Localhost chạy cổng 3000 hay 4200 là chuyện thường ngày. Khi lên production, frontend nằm ở domain thật, đôi khi kèm subdomain riêng cho từng đối tác.
Nhiều đội quên cập nhật danh sách trong WithOrigins khi deploy. Kết quả là CORS chạy ngon trên máy dev nhưng chết ngay khi lên môi trường thật. Kinh nghiệm của đội ngũ chúng tôi là tách cấu hình CORS theo từng môi trường, đọc danh sách domain từ file cấu hình thay vì hard-code. Nhờ vậy, mỗi lần thêm đối tác mới không phải sửa code.
Kiểm tra lại trước khi bàn giao
Sau khi cấu hình xong, nên gọi thử request thật từ đúng domain frontend, đừng chỉ test trên Postman. Postman không áp dụng same-origin policy, nên dù cấu hình CORS sai, Postman vẫn gọi được bình thường, dễ khiến bạn tưởng đã xong việc.
Cách kiểm tra đáng tin hơn là mở DevTools trên đúng domain frontend, xem tab Network và kiểm tra response có header Access-Control-Allow-Origin đúng domain hay không.
Đừng tắt bảo mật CORS chỉ để hết lỗi cho nhanh
Có một cách “sửa” lỗi mà chúng tôi khuyên bạn đừng làm: mở AllowAnyOrigin kèm AllowAnyHeader cho toàn bộ API rồi coi như xong. Lỗi biến mất ngay, nhưng API sẽ mở cửa cho bất kỳ domain nào trên internet. Với API xử lý dữ liệu người dùng, đây là kiểu đánh đổi không đáng.
Lời khuyên của chúng tôi khá đơn giản. Dành mười lăm phút liệt kê chính xác những domain frontend thật sự cần gọi API, rồi khai báo đúng bằng WithOrigins ngay từ đầu dự án. Việc này nhanh hơn nhiều so với sửa lại chính sách khi ứng dụng đã lên production, nhất là khi có thêm đối tác cần tích hợp.
Nếu bạn đang gặp những lỗi vặt khác khi phát triển API bằng ASP.NET, có thể xem thêm top 10 sai lầm thường gặp về backend development khi làm .NET. Bài này gom nhiều lỗi hay lặp lại kèm cách xử lý cho từng trường hợp, khá hữu ích khi bạn cần rà soát nhanh một dự án.


