Skip to main content
Adzbyte
DevelopmentWordPress

Fixing CORS Issues in Headless WordPress: A Complete Guide

Adrian Saycon
Adrian Saycon
March 4, 20263 min read
Fixing CORS Issues in Headless WordPress: A Complete Guide

You’ve set up your Next.js frontend, pointed it at your WordPress REST API, and immediately hit a wall of red text in the console: Access to fetch at 'https://wp.example.com/wp-json/...' from origin 'https://example.com' has been blocked by CORS policy. Welcome to the most common headless WordPress headache.

Why CORS Happens

CORS (Cross-Origin Resource Sharing) is a browser security feature. When your frontend at example.com makes a request to your WordPress API at wp.example.com, the browser checks if the API server explicitly allows that origin. If WordPress doesn’t send the right headers, the browser blocks the response — even if the server returned data successfully.

Key detail: CORS only applies to browser requests. Your Next.js server components and API routes can fetch from WordPress without any CORS issues because server-to-server requests skip the browser entirely. The problem hits when you fetch from client components or when the browser sends preflight requests.

The WordPress Fix: functions.php

Add CORS headers to your WordPress REST API responses:

// functions.php
add_action('rest_api_init', function () {
    remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
    add_filter('rest_pre_serve_request', function ($value) {
        $origin = get_http_origin();
        $allowed_origins = [
            'https://example.com',
            'https://www.example.com',
            'http://localhost:3000',
        ];

        if (in_array($origin, $allowed_origins, true)) {
            header('Access-Control-Allow-Origin: ' . $origin);
            header('Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE');
            header('Access-Control-Allow-Credentials: true');
            header('Access-Control-Allow-Headers: Authorization, Content-Type, X-WP-Nonce');
        }

        return $value;
    });
}, 15);

Notice I’m checking against a whitelist instead of using *. Using Access-Control-Allow-Origin: * won’t work if you need to send credentials (cookies, auth headers), which you almost certainly do for authenticated requests.

Handling Preflight Requests

Before sending a POST or any request with custom headers, the browser sends an OPTIONS request called a “preflight.” WordPress doesn’t handle OPTIONS requests well by default. Add this early in your theme or plugin:

// Handle preflight OPTIONS requests
add_action('init', function () {
    if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
        $origin = get_http_origin();
        $allowed_origins = [
            'https://example.com',
            'http://localhost:3000',
        ];

        if (in_array($origin, $allowed_origins, true)) {
            header('Access-Control-Allow-Origin: ' . $origin);
            header('Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE');
            header('Access-Control-Allow-Credentials: true');
            header('Access-Control-Allow-Headers: Authorization, Content-Type, X-WP-Nonce');
            header('Access-Control-Max-Age: 86400');
        }

        status_header(204);
        exit;
    }
});

The Access-Control-Max-Age header tells the browser to cache the preflight response for 24 hours, reducing the number of OPTIONS requests.

Nginx-Level Fix

If you control your server config, handling CORS at the Nginx level is cleaner and catches requests before PHP even runs:

# nginx site config
location /wp-json/ {
    set $cors_origin "";

    if ($http_origin ~* "^https?://(example.com|localhost:3000)$") {
        set $cors_origin $http_origin;
    }

    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' $cors_origin always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always;
        add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-WP-Nonce' always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        add_header 'Access-Control-Max-Age' 86400;
        return 204;
    }

    add_header 'Access-Control-Allow-Origin' $cors_origin always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;

    try_files $uri $uri/ /index.php?$args;
}

Debugging Checklist

When CORS still isn’t working after you’ve added headers, run through this:

  • Check the actual response headers — open Network tab in DevTools, find the failing request, look at the response headers. Are your CORS headers actually there?
  • Look for duplicate headers — if both Nginx and PHP send Access-Control-Allow-Origin, some browsers reject the response. Pick one layer.
  • Verify the origin matches exactlyhttps://example.com is not https://www.example.com. Include both in your allowed list.
  • Check for plugins overriding headers — security plugins like WordFence or iThemes can strip or override CORS headers.
  • Test with curlcurl -I -H "Origin: https://example.com" https://wp.example.com/wp-json/wp/v2/posts shows you exactly what headers the server returns.

The Server Component Escape Hatch

Here’s the pragmatic solution: do all your WordPress API calls in Next.js server components or route handlers. Server-to-server requests bypass CORS entirely. You only need CORS headers for client-side fetches, which you can often avoid by restructuring your data fetching to happen on the server.

Adrian Saycon

Written by

Adrian Saycon

A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.

Discussion (0)

Sign in to join the discussion

No comments yet. Be the first to share your thoughts.