Fix lỗi Nginx CORS: No 'Access-Control-Allow-Origin' Header Is Present

intermediate Nginx2026-07-04| Nginx 1.14+ trên Ubuntu 20.04/22.04, Debian, CentOS — hoạt động như reverse proxy phía trước Node.js, Flask, FastAPI, Go hoặc backend API bất kỳ

Error Message

Access to XMLHttpRequest at '...' from origin '...' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
#nginx#cors#http-headers#reverse-proxy#cross-origin#javascript#api

Lỗi Gặp Phải

Access to XMLHttpRequest at 'https://api.example.com/data' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Lỗi này xuất hiện trong console của trình duyệt khi frontend (chạy trên một domain) cố gọi đến backend trên domain, subdomain hoặc port khác — mà Nginx không trả về các header CORS cần thiết. Trình duyệt chặn response trước khi JavaScript của bạn kịp nhận được.

Nguyên Nhân

Trình duyệt thực thi chính sách Same-Origin Policy. Nếu origin của frontend khác với origin của backend (khác domain, subdomain hoặc port), trình duyệt sẽ kiểm tra header Access-Control-Allow-Origin trong response. Nginx mặc định không tự thêm header này — bạn phải cấu hình thủ công.

Cách Khắc Phục Từng Bước

1. Tìm File Cấu Hình Nginx

ls /etc/nginx/sites-enabled/
cat /etc/nginx/sites-enabled/your-api.conf

2. Thêm CORS Headers Vào Server Block

Một origin được phép (trường hợp phổ biến nhất)

server {
    listen 80;
    server_name api.example.com;

    location / {
        # Xử lý preflight OPTIONS request trước
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Accept' always;
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Accept' always;

        proxy_pass http://localhost:8080;
    }
}

Cờ always ở mỗi add_header là bắt buộc, không thể bỏ qua. Nếu thiếu, Nginx chỉ đính kèm header vào các response 2xx — các response lỗi (4xx, 5xx) sẽ không có header, khiến trình duyệt chỉ hiện lỗi CORS thay vì lỗi thực sự. Rất khó debug.

Nhiều origin được phép (dev + staging + prod)

Khi cần whitelist nhiều origin cụ thể, dùng block map — đặt bên ngoài block server, thường ở đầu file config hoặc trong /etc/nginx/conf.d/cors.conf:

map $http_origin $cors_origin {
    default                        "";
    "https://app.example.com"      $http_origin;
    "https://staging.example.com"  $http_origin;
    "http://localhost:3000"        $http_origin;
}

server {
    listen 80;
    server_name api.example.com;

    location / {
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' $cors_origin always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Accept' always;
            add_header 'Access-Control-Max-Age' 1728000;
            return 204;
        }

        add_header 'Access-Control-Allow-Origin' $cors_origin always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Accept' always;
        add_header 'Vary' 'Origin' always;

        proxy_pass http://localhost:8080;
    }
}

Header Vary: Origin quan trọng trong trường hợp này — nếu thiếu, CDN hoặc proxy có thể cache response với header của một origin rồi phục vụ nhầm cho origin khác.

Public API — cho phép mọi origin

add_header 'Access-Control-Allow-Origin' '*' always;

Chỉ dùng wildcard cho các API thực sự public. Trình duyệt sẽ từ chối * khi có credentials (cookie, Authorization header) trong request.

Request có cookie hoặc Authorization header

Bạn phải chỉ định đúng origin (không dùng wildcard) và thêm credentials header:

add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;

3. Kiểm Tra Cấu Hình và Reload

# Kiểm tra cú pháp trước
sudo nginx -t

# Reload mà không ngắt kết nối đang có
sudo nginx -s reload

Xác Nhận Đã Sửa Thành Công

curl — cách nhanh nhất để kiểm tra

# Kiểm tra GET request thông thường
curl -I \
  -H "Origin: https://app.example.com" \
  https://api.example.com/data

# Kiểm tra preflight OPTIONS request
curl -X OPTIONS \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Authorization" \
  -I https://api.example.com/data

Tìm dòng Access-Control-Allow-Origin: https://app.example.com trong kết quả. Nếu curl hiển thị đúng nhưng trình duyệt vẫn báo lỗi, hãy mở cửa sổ ẩn danh — trình duyệt đôi khi cache lại các lần preflight thất bại.

Kiểm tra bằng DevTools của trình duyệt

Tab Network → click vào request bị lỗi → tab Headers → phần Response Headers. Header phải xuất hiện ở đây. Nếu curl thấy nhưng DevTools không thấy, kiểm tra xem request có đi qua route khác không, hoặc có service worker nào đang chặn nó không.

Những Lỗi Hay Gặp

  • Location block con ghi đè header của block cha: Trong Nginx, nếu một block location lồng bên trong có bất kỳ directive add_header nào, toàn bộ add_header từ block cha sẽ bị xóa sạch — chúng không được gộp lại. Đây là hành vi gây bất ngờ nhất. Hãy thêm tất cả CORS headers trực tiếp vào từng block location cần dùng.
  • Backend cũng tự thêm CORS headers: Nếu ứng dụng (Node, Flask, v.v.) đã set CORS headers mà Nginx cũng thêm vào, response sẽ bị trùng lặp — Access-Control-Allow-Origin: *, https://app.example.com. Trình duyệt sẽ từ chối. Chỉ xử lý CORS ở đúng một nơi.
  • HTTP và HTTPS là hai origin khác nhau: http://app.example.comhttps://app.example.com là hai origin hoàn toàn khác nhau. Đảm bảo scheme trong config khớp chính xác với những gì trình duyệt gửi lên.
  • Wildcard subdomain không được hỗ trợ sẵn: Nginx không thể xử lý *.example.com theo cách tự nhiên. Dùng cách tiếp cận map ở trên và liệt kê từng subdomain cụ thể.
  • Thiếu handler cho preflight: Trình duyệt gửi OPTIONS request trước khi thực hiện POST/PUT/DELETE có custom headers. Nếu Nginx chuyển OPTIONS thẳng về backend mà backend không xử lý được, preflight sẽ thất bại và request thật sự không bao giờ được gửi đi.

Related Error Notes