'Fixing the 'Access-Control-Allow-Origin' header contains multiple values error in NGINX

I'm setting up a Wordpress website (e.g. http://api.example.com) in order to consumer its API from another static, HTML/JS website (e.g. https://test.example.com).

Both website are hosted on an Nginx server, and are each configured with a conf file and function properly on their own. nginx -t yields no error and I have full access to both websites as expected.

Unfortunately I'm facing issues with CORS. When attempting to read media (images, videos) content from api.example.com, test.example.com yields the following error in the browser console:

Access to XMLHttpRequest at 
'https://api.example.com/wp-json/custom-post/v1/some-data/' 
from origin 'https://test.example.com' has been blocked by CORS policy: 

The 'Access-Control-Allow-Origin' header contains multiple values
'https://test.example.com, https://test.example.com', 
but only one is allowed.

Furthermore, on Chrome, this error is followed by a CORB error (Cross-Origin Read Blocking (CORB) blocked cross-origin response https://api.example.com/wp-json with MIME type application/json.).

I have noticed when inspecting the request header in the browser, redundant values for these properties:

Access-Control-Allow-Credentials: true, true
Access-Control-Allow-Origin: https://test.example.com, https://test.example.com

Sensing there might be a redundant Access-Control-Allow-Origin somewhere, I have looked for it in the nginx.conf file and all conf files in sites-enabled, to no avail. I've also looked within the source code of the Wordpress application, used plugins included, for an injection of this header. There were none to be found.

Finally, I have tried to remove the one line in api.example.com.conf adding the Access-Control-Allow-Origin header — it yields the pure and simple No 'Access-Control-Allow-Origin' header is present on the requested resource error for media content in the browser console. Interestingly, it doesn't yield the CORB error for JSON anymore, and test.example.com is able to read textual content from JSON files.

Here's the content of the api.example.com.conf file:

server {
    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

    root /var/www/example.com/backend;
    server_name api.example.com;
    access_log /var/log/nginx/unicorn_access.log;
    error_log /var/log/nginx/unicorn_error.log;

    charset                     utf-8;
    gzip                        off;

    # Set CORS policy
    set $cors_origin            "";
    set $cors_cred              "";
    set $cors_header            "";
    set $cors_method            "";

    if ($http_origin ~ '^https?:\/\/(localhost|test.example\.com)$') {
        set $cors_origin        $http_origin;
        set $cors_cred          true;
        set $cors_header        $http_access_control_request_headers;
        set $cors_method        $http_access_control_request_method;
    }

    add_header Access-Control-Allow-Origin      $cors_origin;
    add_header Access-Control-Allow-Credentials $cors_cred;
    add_header Access-Control-Allow-Headers     $cors_header;
    add_header Access-Control-Allow-Methods     $cors_method;

    location / {
        index                   index.php index.html;
        try_files               $uri $uri/ /index.php?$args;
    }

    client_max_body_size        50m;

    # Add trailing slash to */wp-admin requests.
    rewrite /wp-admin$ $scheme://$host$uri/ permanent;

    # Prevents hidden files (beginning with a period) from being served
    location ~ /\. {
        access_log              off;
        log_not_found           off;
        deny                    all;
    }

    # Send 'expires' headers and turn off 404 logging
    location ~* ^.+.(xml|ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|css|rss|atom|js|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
        access_log              off;
        log_not_found           off;
        expires                 max;
    }

    # Pass all .php files onto a php-fpm or php-cgi server
    location ~ \.php$ {
        try_files               $uri =404;
        include                 /etc/nginx/fastcgi_params;
        fastcgi_read_timeout    3600s;
        fastcgi_buffer_size     128k;
        fastcgi_buffers         4 128k;
        fastcgi_param           SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass            unix:/run/php/php7.2-fpm.sock;
        fastcgi_index           index.php;
    }

    # Robots
    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    # Restrictions
    location ~* /(?:uploads|files)/.*\.php$ {
        deny all;
    }
}

I expect test.example.com to consume any content from api.example.com, but I can't get to do so fully.

Thanks for your help!



Solution 1:[1]

It was due to the rest_send_cors_headers filter the REST API hooks into rest_pre_serve_request. It sent CORS headers with API requests.

It can be shut down with the following action:

add_action('rest_api_init', function() {
    remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
}, 15);

Solution 2:[2]

function custom_rest_api_init() {
    remove_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
    add_filter( 'rest_pre_serve_request', function( $value ) {
        header( 'Access-Control-Allow-Origin: *' );
        header( 'Access-Control-Allow-Methods: OPTIONS, GET, POST, PUT, PATCH, DELETE' );
        header( 'Access-Control-Allow-Credentials: true' );
        header( 'Access-Control-Expose-Headers: Link', false );
        header( 'Access-Control-Allow-Headers: X-Requested-With' );
        // header( 'Vary: Origin', false );

        return $value;
    } );
}
add_action( 'rest_api_init', 'custom_rest_api_init', 15 );

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Davy
Solution 2 Emin Temiz