If you’re building a React, Next.js, or Vue frontend that talks to WordPress as a backend, you’ve probably seen this before:
Access to fetch at ‘https://your-wp-site.com/wp-json/wp/v2/posts’ from origin ‘http://localhost:3000’ has been blocked by CORS policy.That’s not WordPress being picky — it’s your browser doing its job.
Let’s break down why that happens, what’s going on under the hood, and how to fix it with a simple plugin and a few lines of JavaScript.
Table of Contents
- Understanding CORS (and Why It Exists)
- How the Browser Handles It
- Enabling CORS for requests to WordPress from the outside
- What Happens when there’s a request from a different URL
- Wrapping Up
Understanding CORS (and Why It Exists)
Modern browsers enforce the same-origin policy, which prevents JavaScript from making requests to a different domain, scheme, or port.
This rule keeps users safe but makes “headless” setups tricky — your frontend (localhost:3000) can’t directly talk to your backend (your-wp-site.com) unless the server explicitly allows it.
That’s where CORS, or Cross-Origin Resource Sharing, comes in.
It’s an HTTP-based handshake between browser and server. The browser asks:
“Can I access this resource?”
And the server replies:
“Yes — if you’re from this origin and follow these rules.”
How the Browser Handles It
When your frontend calls another origin, the browser checks what kind of request it is.
- Simple requests (e.g.
GET, standard headers) are sent directly. - Preflighted requests (e.g.
POSTwithAuthorization) trigger an OPTIONS check before the real call.
Enabling CORS for requests to WordPress from the outside
By default, WordPress REST API only sends very limited CORS headers — just enough for basic same-origin requests.
If your frontend app is running on another domain (like localhost:3000 or frontend.yourdomain.com), you need to explicitly tell WordPress to allow requests from that origin.
CORS support on the server side is implemented entirely through HTTP response headers.
When the browser checks whether it can talk to your API, it looks for these headers in the server’s response.
Let’s walk through what needs to be in place and then see how to do it in a plugin.
The Required Response Headers
To make your WordPress server CORS-friendly, you must return the following headers for REST API routes:
| Header | What It Does | Example |
|---|---|---|
Access-Control-Allow-Origin | Tells the browser which origins (domains) are allowed to make requests. | Access-Control-Allow-Origin: https://myfrontend.com |
Access-Control-Allow-Methods | Lists which HTTP methods are permitted for cross-origin requests. | Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE |
Access-Control-Allow-Headers | Lists which custom headers a browser can send. Needed if your frontend uses headers like Authorization or Content-Type. | Access-Control-Allow-Headers: Authorization, Content-Type |
Access-Control-Allow-Credentials | Indicates whether cookies or credentials can be sent with requests. Required if using JWTs or authentication cookies. | Access-Control-Allow-Credentials: true |
If your server omits or mismatches any of these, the browser will block the request before your PHP code runs.
Adding These Headers in WordPress
WordPress already sends some CORS headers internally, but they’re not flexible enough for headless setups.
We can replace them with our own by hooking into the REST API response.
Create a new plugin:
- Inside your WordPress installation, go to
wp-content/plugins - Create a folder named
headless-cors-allow - Inside that folder, create a file named
headless-cors-allow.phpwith this code:
<?php
/**
* Plugin Name: Headless CORS Allow
* Description: Enables secure CORS for headless frontends.
* Author: Your Name
* Version: 1.0.0
*/
defined( 'ABSPATH' ) || exit;
add_action( 'rest_api_init', function() {
// Disable WordPress's default CORS headers.
remove_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
// Add custom CORS headers.
add_filter( 'rest_pre_serve_request', function( $value ) {
$allowed_origin = 'http://localhost:3000'; // Change this to your frontend URL.
header( "Access-Control-Allow-Origin: {$allowed_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' );
return $value;
} );
}, 15 );- Go to Plugins → Installed Plugins in your WordPress dashboard and activate Headless CORS Allow.
Choosing Your Allowed Origin
Always set $allowed_origin to the specific frontend domain that should communicate with WordPress, for example:
$allowed_origin = 'https://app.yourdomain.com';Avoid using a wildcard (*) when you’re sending credentials (cookies, JWT tokens, etc.), since browsers will refuse to send authentication data unless the origin is explicitly listed.
So in short:
Enabling CORS isn’t about adding JavaScript — it’s about teaching your server to say,
“Yes, I know this frontend, and it’s allowed to talk to me.”
What happens in the browser when there’s a cross-origin request
When you make a cross-origin request, the browser evaluates it first:
- If it’s safe, it sends it directly (simple request, no preflight)
- If it’s complex (like authenticated requests), it performs a preflight using the
OPTIONSmethod before the actual request.
Simple Request (No Preflight)
A simple request is one that:
- Uses
GET,HEAD, orPOST - Only uses safe headers (
Accept,Content-Type, etc.) - Doesn’t include credentials or custom headers
const apiBase = 'https://your-wp-site.com/wp-json/wp/v2/posts';
fetch( apiBase, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
} )
.then( ( response ) => response.json() )
.then( ( posts ) => console.log( '✅ Received posts:', posts ) )
.catch( ( error ) => console.error( '❌ Fetch error:', error ) );✅ The browser sends the request immediately since it meets all “simple” conditions.
Authenticated Request (With Preflight)
When the frontend sends a POST, PUT, or DELETE request — or includes custom headers such as Authorization — the browser doesn’t send that request to the server immediately.
Before doing so, it automatically performs a “preflight” check using the special HTTP method OPTIONS.
This invisible first request asks the server whether it’s safe and allowed to make the real call.
Think of it as the browser asking:
“Hey, WordPress — if I send a POST with this Authorization header, will you accept it?”
Only if the server responds with permission does the browser proceed to send your actual request.
We don’t have to code this step yourself — the browser inserts it automatically under the CORS specification.
For example, the following code sends a request to create a Post with Authorization Header. In our code, this looks like a single request — but under the hood, the browser is sending two:
const apiBase = 'https://your-wp-site.com/wp-json/wp/v2/posts';
fetch( apiBase, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer my-jwt-token',
},
body: JSON.stringify( {
title: 'New Post from the Headless Frontend',
status: 'draft',
} ),
} )
.then( ( response ) => response.json() )
.then( ( data ) => console.log( '✅ Created:', data ) )
.catch( ( error ) => console.error( '❌ Fetch error:', error ) );Our JavaScript only runs one fetch(), but before that POST is sent, the browser first issues an OPTIONS request to verify that the server allows it.
The preflight response must include:
Access-Control-Allow-Origin— which origins are allowedAccess-Control-Allow-Methods— which HTTP methods are acceptedAccess-Control-Allow-Headers— which custom headers (likeAuthorization) are permitted
If all of these match the browser’s expectations, it proceeds with the real POST request.
If not, it blocks the call before any data is sent — WordPress never sees it.
Why Preflight Exists
The preflight step is a security safeguard built into browsers. It prevents malicious sites from silently sending dangerous requests to other domains on a user’s behalf.
By forcing servers to explicitly declare what’s allowed, CORS ensures:
- Sensitive requests (
POST,PUT,DELETE) aren’t sent without consent - Only trusted origins can perform authenticated actions
- Browsers handle security at the network level, before your code even runs
Your WordPress plugin’s CORS headers are effectively saying:
“Yes, I trust this origin — and I allow it to use these methods and headers.”
Without those headers, even valid API requests from your own frontend will fail with a CORS error.
Wrapping Up
CORS isn’t about blocking you — it’s about protecting your users.
Once your WordPress site correctly advertises which origins can talk to it, cross-domain requests work smoothly and securely.
Before we finish, some security & best practices when developing CORS related code:
- Always whitelist specific origins, never
*. - Only include necessary headers in
Access-Control-Allow-Headers. - Avoid exposing private REST endpoints to public origins.
- Use DevTools → Network → OPTIONS to confirm your preflight responses.
Now you know how your frontend can fetch content, create posts and use authentication without any “Blocked by CORS policy” errors 🦾
Leave a Reply