Skip to content
Products
AI Website Builder New VPS Hosting Cloud Servers Web Hosting Dedicated Servers Domains
Company
About Documentation Support Center Contact Get Started Login Client Area
ALL SYSTEMS OPERATIONAL
WordPress

Headless WordPress: Fixing CORS & JWT Errors in WPGraphQL

Getwebup 6 min read

You've pointed a Next.js or Gatsby front-end at your WordPress backend, everything works fine from Postman, and then the browser console lights up with a CORS error the moment you try it from the actual frontend domain. This is one of the most common snags we see from developers building headless sites on Getwebup, and it's rarely a WordPress "bug" - it's a handful of settings that WPGraphQL and JWT auth don't configure for you by default.

What "Headless WordPress" Actually Means Here

In a headless setup, WordPress stays on your Getwebup hosting purely as a content API - editors still log into wp-admin as normal - but the public-facing site is a separate app (Next.js, Gatsby, Nuxt, whatever) hosted elsewhere, usually on Vercel or Netlify. That app talks to WordPress over /graphql (via the WPGraphQL plugin) instead of loading PHP templates directly. Because the request now crosses two different domains - cms.yourdomain.com and yourdomain.com - every browser enforces CORS, and every authenticated request needs a token instead of a WordPress session cookie.

Symptom: What You'll Actually See

The errors show up in the frontend's browser console or build logs, not in WordPress itself, which is why they're confusing to diagnose:

  • Access to fetch at 'https://cms.yourdomain.com/graphql' from origin 'https://yourdomain.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
  • GraphQL queries work fine unauthenticated, but any mutation or logged-in query returns 401 Unauthorized or "Internal server error"
  • The JWT login mutation returns "wp_jwt_auth_bad_config" or just a blank 500
  • Draft/preview links sent from WordPress open the PHP theme on the CMS domain instead of the actual frontend
  • Featured images and internal links in GraphQL responses point at cms.yourdomain.com instead of resolving on the frontend

Cause 1: WPGraphQL Doesn't Send CORS Headers by Default

WordPress's REST API sends permissive CORS headers out of the box for logged-out requests. WPGraphQL deliberately doesn't, since a GraphQL endpoint that accepts mutations is a much bigger attack surface than a read-only REST route. Nothing is broken - it's just strict by default, and you need to opt your frontend's origin in explicitly.

Fix: Add CORS Headers for Your Frontend's Origin Only

Do this as an mu-plugin (or in a small site-specific plugin) rather than the theme's functions.php, so it survives a theme switch:

<?php
// wp-content/mu-plugins/graphql-cors.php
add_action( 'graphql_response_headers_to_send', function( $headers ) {
    $allowed_origin = 'https://yourdomain.com'; // your frontend, not the CMS domain
    $headers['Access-Control-Allow-Origin']      = $allowed_origin;
    $headers['Access-Control-Allow-Credentials']  = 'true';
    $headers['Access-Control-Allow-Headers']      = 'Content-Type, Authorization';
    return $headers;
} );

add_action( 'graphql_process_http_request', function() {
    if ( 'OPTIONS' === $_SERVER['REQUEST_METHOD'] ) {
        header( 'Access-Control-Allow-Origin: https://yourdomain.com' );
        header( 'Access-Control-Allow-Headers: Content-Type, Authorization' );
        status_header( 200 );
        exit;
    }
} );

Resist the temptation to set Access-Control-Allow-Origin: * just to make the error go away. Combined with Allow-Credentials: true that's actually invalid per the CORS spec (browsers will reject it), and if you drop the credentials flag instead, you've opened your GraphQL mutations to any site on the internet. List every real origin you use - production, staging, and localhost:3000 for local dev - as separate allowed values, not a wildcard.

Cause 2: JWT Auth Plugin Missing Its Secret Key

If you're using WPGraphQL JWT Authentication (or a similar plugin) so the frontend can log in editors or fetch draft previews, it needs a signing secret that doesn't exist in WordPress core. Skip this step and the login mutation fails immediately with a config error, regardless of how correct the username and password are.

Fix: Define the Secret in wp-config.php

Add this above the line that says /* That's all, stop editing! */, using a long random string - not your database password or anything reused elsewhere:

define( 'GRAPHQL_JWT_AUTH_SECRET_KEY', 'a-long-random-string-only-this-site-uses' );

You can generate one quickly from the WordPress secret-key API or with openssl rand -base64 32 over SSH. Changing this value later invalidates every issued token, which is useful if you ever suspect a leak, but expect every logged-in frontend session to need a fresh login afterward.

Cause 3: The Authorization Header Gets Stripped Before PHP Sees It

On shared cPanel hosting running PHP as a CGI/FastCGI handler (the default on most Apache-based plans), the Authorization header is silently dropped before it reaches WordPress. The frontend sends the JWT correctly, the browser network tab shows it going out, but every authenticated GraphQL call still comes back 401 as if no token was sent at all.

Fix: Forward the Header via .htaccess

Add this rule to the WordPress root .htaccess, above the standard WordPress rewrite block:

RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [E=HTTP_AUTHORIZATION:%1]

If you're on a Getwebup VPS running LiteSpeed or Nginx instead of stock Apache, the fix is a server-config line rather than .htaccess - LiteSpeed passes the header through by default, and on Nginx you'd add fastcgi_param HTTP_AUTHORIZATION $http_authorization; inside the PHP location block. Check which stack your plan runs before assuming the Apache fix applies.

By default, clicking "Preview" in wp-admin opens the CMS domain's PHP theme, and image URLs returned by WPGraphQL are absolute links back to cms.yourdomain.com. Neither is wrong, exactly - WordPress just doesn't know a second, separate frontend exists.

Fix

  • Filter preview_post_link to point at your frontend's preview route instead of the theme, passing the post ID and a preview token as query parameters your frontend checks against the GraphQL API.
  • For media, either leave the absolute CMS URLs as-is (simplest, and fine for most sites - the browser just loads images cross-origin) or set WPGRAPHQL_MEDIA_BASE_URL-style rewriting in your frontend's image loader if you specifically need them served from your own domain for caching or a CDN.

Prevention Checklist

PracticeWhy it matters
List explicit allowed origins, never a wildcardA wildcard plus credentials either breaks per spec or exposes mutations to any site
Store the JWT secret only in wp-config.php, not in a plugin optionOptions tables get exported in migrations and backups far more often than wp-config.php
Rotate the JWT secret if you ever move hosts or suspect a leakInstantly invalidates every issued token without needing to touch user passwords
Confirm your hosting stack (Apache/LiteSpeed/Nginx) before applying header fixesThe Authorization-header fix is different on each, and the wrong one does nothing
Add a query depth/complexity limit plugin once mutations are publicAn open GraphQL endpoint without limits is an easy target for expensive, nested queries

Frequently asked questions

Why does WPGraphQL need separate CORS handling if the WordPress REST API already works cross-origin?

The REST API ships permissive default headers for logged-out GET requests, but WPGraphQL treats every request - including mutations - the same way and stays locked down until you explicitly allow your frontend's origin. It's intentional caution, not a bug.

Is it safe to just set Access-Control-Allow-Origin to a wildcard (*) to stop the errors?

No. Combined with Allow-Credentials it's invalid and browsers will reject it anyway, and without credentials it opens your GraphQL mutations to requests from any website. List your real origins (production, staging, localhost) explicitly instead.

My JWT login mutation works locally but fails only on Getwebup shared hosting - why?

Shared cPanel hosting running PHP as CGI/FastCGI strips the Authorization header before WordPress sees it. Add the RewriteRule shown above to your .htaccess to forward it, or the equivalent fastcgi_param line if you're on an Nginx-based VPS plan.

Do I need to rebuild anything if I rotate the GRAPHQL_JWT_AUTH_SECRET_KEY?

No rebuild needed on the frontend, but every previously issued token becomes invalid immediately, so any logged-in editor sessions or cached tokens will need to log in again through the mutation.

Should featured image URLs from WPGraphQL point at my frontend domain instead of the WordPress domain?

Not necessarily. Serving them straight from the WordPress domain works fine for most sites since the browser loads them cross-origin without any CORS restriction on images. Only rewrite them if you specifically need everything served from one domain for a CDN or caching setup.

#headless-wordpress #wpgraphql #cors #jwt-authentication #graphql #wordpress

Keep reading

Chat with Support