• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Debugging Guide: Diagnosing REST API CORS authorization failures in multi-site network environments with modern tools

Debugging Guide: Diagnosing REST API CORS authorization failures in multi-site network environments with modern tools

Understanding CORS in WordPress Multi-Site

Cross-Origin Resource Sharing (CORS) is a fundamental browser security mechanism that restricts web pages from making requests to a different domain than the one that served the web page. When developing REST APIs, especially within a WordPress multi-site environment where distinct subdomains or subdirectories act as separate origins, CORS misconfigurations are a frequent source of authorization failures. This guide focuses on diagnosing and resolving these issues using advanced techniques and modern tooling.

Identifying the CORS Failure

The first step is to accurately identify that a CORS issue is indeed the culprit. Browser developer tools are your primary weapon here. Open your browser’s developer console (usually F12), navigate to the “Network” tab, and trigger the API request that is failing. Look for requests that are marked with a red status code (e.g., 403 Forbidden, or sometimes even a network error). Crucially, examine the “Response Headers” of the failing request. You’re looking for the absence of, or incorrect values for, specific CORS headers:

  • Access-Control-Allow-Origin: This header indicates whether the resource can be shared with the requesting origin.
  • Access-Control-Allow-Methods: Specifies the HTTP methods allowed for the resource.
  • Access-Control-Allow-Headers: Lists the HTTP headers that can be used in the request.
  • Access-Control-Allow-Credentials: Indicates whether the browser should send cookies or authorization headers with the request.

If the request fails and these headers are missing or incorrect, you’re likely dealing with a CORS problem. A common symptom is a console error message like: “Access to fetch at 'https://api.example.com/...' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.“

Server-Side CORS Configuration in WordPress

In WordPress, CORS headers are typically managed by the web server (Nginx/Apache) or by plugins. For REST API endpoints, especially those registered via register_rest_route, you need to ensure the correct headers are being sent. The most common approach is to hook into the `rest_pre_serve_request` filter or, more broadly, `send_headers`.

Consider a scenario where your API is hosted on api.example.com and your frontend application is on app.example.com. You need to allow requests from app.example.com to api.example.com.

Using `rest_pre_serve_request` for Specific Endpoints

This filter allows you to intercept the response before it’s sent, giving you fine-grained control. It’s particularly useful if you only want to apply CORS headers to specific REST API routes.

add_filter( 'rest_pre_serve_request', function( $response, $handler, $request ) {
    // Check if it's a valid REST API response and not an error
    if ( is_wp_error( $response ) || ! $response instanceof WP_HTTP_Response ) {
        return $response;
    }

    // Define allowed origins. In a multi-site, you might dynamically get this.
    $allowed_origins = array(
        'https://app.example.com',
        'https://staging.app.example.com',
        // Add other allowed origins as needed
    );

    // Get the current origin from the request header
    $origin = isset( $_SERVER['HTTP_ORIGIN'] ) ? sanitize_url( $_SERVER['HTTP_ORIGIN'] ) : '';

    // Check if the origin is allowed
    if ( in_array( $origin, $allowed_origins ) ) {
        header( "Access-Control-Allow-Origin: " . $origin );
    } else {
        // Optionally, you can allow all origins for development, but be cautious in production
        // header( "Access-Control-Allow-Origin: *" );
    }

    // Allow specific methods
    header( "Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS" );

    // Allow specific headers
    header( "Access-Control-Allow-Headers: Content-Type, Authorization, X-WP-Nonce" );

    // Allow credentials (if your API uses cookies or authentication tokens)
    // Be very careful with this in production.
    // header( "Access-Control-Allow-Credentials: true" );

    // Handle OPTIONS requests (preflight)
    if ( 'OPTIONS' === $_SERVER['REQUEST_METHOD'] ) {
        status_header( 204 ); // No Content
        exit;
    }

    return $response;
}, 10, 3 );

Important Considerations:

  • Dynamic Origins: In a multi-site setup, the origin might change based on the site being accessed. You’ll need logic to determine the correct allowed origin, potentially by inspecting the request path or using site-specific configurations.
  • Preflight Requests (OPTIONS): Browsers send an `OPTIONS` request before the actual request (for methods other than GET/HEAD or when custom headers are used). Your server must respond to these with the appropriate CORS headers and a 204 status code. The code above handles this.
  • Credentials: If your API requires authentication (e.g., cookies, JWT tokens), you must set Access-Control-Allow-Credentials: true. This also means you cannot use a wildcard (*) for Access-Control-Allow-Origin; you must specify the exact allowed origin.
  • `X-WP-Nonce`: For authenticated requests made via JavaScript, WordPress often uses `X-WP-Nonce` for security. Ensure this header is allowed.

Global CORS Headers via Web Server Configuration

For more consistent CORS handling across your entire application, or if you want to offload this from PHP, configuring your web server is often more performant. This is especially relevant if your WordPress installation is behind a load balancer or reverse proxy.

Nginx Configuration Example

Add these directives within your server block, or a specific location block if you only want to apply them to your API endpoints (e.g., location ~ ^/wp-json/.*).

location / {
    # ... other Nginx directives ...

    # CORS Headers
    add_header 'Access-Control-Allow-Origin' '$http_origin' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-WP-Nonce' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always; # Use with caution

    # Handle OPTIONS requests
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '$http_origin';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-WP-Nonce';
        add_header 'Access-Control-Max-Age' 1728000; # Cache preflight response for 20 days
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Content-Length' 0;
        return 204;
    }

    # ... other Nginx directives ...
}

Nginx Notes:

  • $http_origin: Nginx variable that captures the `Origin` header from the request.
  • always: Ensures the header is added regardless of the response code.
  • Dynamic Origin Matching: For stricter control, you might need to use map directives or Lua scripting to dynamically set Access-Control-Allow-Origin based on the $http_origin.

Apache Configuration Example

Add these directives to your .htaccess file or your virtual host configuration.

<IfModule mod_headers.c>
    # Allow all origins for development (use with caution)
    # Header set Access-Control-Allow-Origin "*"

    # For specific origins (more secure)
    # You'll need mod_rewrite to dynamically set this based on HTTP_ORIGIN
    # Example using RewriteCond (can become complex)
    # RewriteEngine On
    # RewriteCond %{HTTP:Origin} ^(https?:\/\/((www\.)?app\.example\.com|staging\.app\.example\.com))$ [NC]
    # RewriteRule .* - [E=HTTP_ORIGIN:%1]
    # Header set Access-Control-Allow-Origin %{HTTP:HTTP_ORIGIN} env=HTTP_ORIGIN

    # General CORS headers
    Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
    Header set Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-WP-Nonce"
    Header set Access-Control-Allow-Credentials "true" env=HTTP_ORIGIN # Only if HTTP_ORIGIN is set

    # Handle OPTIONS requests
    RewriteEngine On
    RewriteCond %{REQUEST_METHOD} OPTIONS
    RewriteRule ^(.*)$ $1 [R=204,L]
</IfModule>

<IfModule mod_rewrite.c>
    # For OPTIONS requests, ensure correct headers are sent
    <IfModule mod_headers.c>
        Header set Access-Control-Allow-Origin "*" env=OPTIONS_REQUEST
        Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" env=OPTIONS_REQUEST
        Header set Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-WP-Nonce" env=OPTIONS_REQUEST
        Header set Access-Control-Max-Age "1728000" env=OPTIONS_REQUEST
    </IfModule>
    RewriteCond %{REQUEST_METHOD} OPTIONS
    RewriteRule ^(.*)$ $1 [R=204,L]
</IfModule>

Apache Notes:

  • mod_headers and mod_rewrite are required.
  • Dynamically setting Access-Control-Allow-Origin based on the Origin header in Apache can be more verbose than in Nginx. The example shows a basic approach; complex scenarios might require more advanced RewriteCond rules or mod_authz_host.
  • The env=HTTP_ORIGIN and env=OPTIONS_REQUEST are crucial for conditional header setting.

Debugging Authorization Headers

CORS is often intertwined with authorization. If your API requires an `Authorization` header (e.g., Bearer token) or cookies, ensure these are correctly handled by your CORS configuration.

Bearer Tokens

When using Bearer tokens, the `Authorization` header must be explicitly allowed in your CORS configuration (as shown in the examples above). The browser will only send this header if `Access-Control-Allow-Headers` includes `Authorization` and, if credentials are involved, `Access-Control-Allow-Credentials` is set to `true`.

// Example JavaScript fetch with Bearer token
fetch('https://api.example.com/my-endpoint', {
    method: 'GET',
    headers: {
        'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
        'Content-Type': 'application/json'
    }
})
.then(response => {
    if (!response.ok) {
        // Check for CORS-related errors here if status is not ok
        console.error('API Error:', response.status, response.statusText);
        return response.json().then(data => { throw data; });
    }
    return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Fetch Error:', error));

If the request fails with a 401 or 403 and you’re sending an `Authorization` header, verify that both the server-side CORS configuration and your client-side request are correctly set up.

Cookies and Nonces

For requests that rely on cookies (e.g., logged-in users in WordPress), you must set Access-Control-Allow-Credentials: true. This is critical. Additionally, the Access-Control-Allow-Origin header cannot be a wildcard (`*`) when credentials are used; it must be the specific origin.

WordPress’s REST API often uses `X-WP-Nonce` for authenticated requests. Ensure this header is included in your Access-Control-Allow-Headers.

// Example JavaScript fetch with cookies and nonce
// Assumes nonce is available in a JS variable, e.g., wpApiSettings.nonce
fetch('https://api.example.com/my-endpoint', {
    method: 'POST',
    headers: {
        'X-WP-Nonce': wpApiSettings.nonce, // WordPress nonce
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ key: 'value' })
})
.then(response => {
    if (!response.ok) {
        console.error('API Error:', response.status, response.statusText);
        return response.json().then(data => { throw data; });
    }
    return response.json();
})
.catch(error => console.error('Fetch Error:', error));

If you encounter issues with cookie-based authentication, double-check that your web server or PHP code is correctly setting Access-Control-Allow-Credentials: true and that the Access-Control-Allow-Origin is specific and matches the requesting origin.

Multi-Site Specific Challenges

WordPress multi-site environments introduce complexity due to the distributed nature of sites, often residing on different subdomains or paths but sharing a common codebase. This means your CORS logic needs to be aware of the current site’s origin.

Dynamic Origin Handling

When using the PHP approach, you can leverage WordPress’s multi-site functions to determine the correct origin.

add_filter( 'rest_pre_serve_request', function( $response, $handler, $request ) {
    // ... (initial checks as before) ...

    $allowed_origins = array();

    // Get all site URLs
    if ( is_multisite() ) {
        $sites = wp_get_sites();
        foreach ( $sites as $site ) {
            $site_url = get_site_url( $site->blog_id );
            if ( $site_url ) {
                $allowed_origins[] = esc_url_raw( $site_url );
            }
        }
    } else {
        $allowed_origins[] = esc_url_raw( home_url() );
    }

    // Add your API domain if it's separate
    $allowed_origins[] = 'https://api.example.com'; // If API is on a different domain

    $origin = isset( $_SERVER['HTTP_ORIGIN'] ) ? sanitize_url( $_SERVER['HTTP_ORIGIN'] ) : '';

    if ( in_array( $origin, $allowed_origins ) ) {
        header( "Access-Control-Allow-Origin: " . $origin );
    } else {
        // Log disallowed origins for debugging
        error_log("Disallowed CORS origin: " . $origin);
    }

    // ... (rest of the CORS headers and OPTIONS handling) ...

    return $response;
}, 10, 3 );

This code snippet iterates through all registered sites in a multi-site network and adds their respective URLs to the list of allowed origins. This ensures that requests originating from any of your WordPress sites are permitted.

Subdomain vs. Subdirectory Installs

The way WordPress multi-site is configured (subdomains vs. subdirectories) impacts how origins are perceived. If you use subdomains (e.g., site1.example.com, site2.example.com), each is a distinct origin. If you use subdirectories (e.g., example.com/site1/, example.com/site2/), they are all considered the same origin (example.com) from a browser’s perspective, simplifying CORS.

When your API is on a separate domain (e.g., api.example.com) and your frontends are on subdomains (site1.example.com), you must explicitly list each subdomain as an allowed origin. Wildcard subdomains (e.g., *.example.com) are generally not supported by the Access-Control-Allow-Origin header for security reasons.

Advanced Debugging Tools and Techniques

Beyond browser developer tools, several other methods can help pinpoint CORS issues.

`curl` for Server-Side Testing

You can simulate browser requests using curl to bypass browser-specific CORS logic and directly inspect server responses.

# Test a GET request, simulating an origin
curl -I -H "Origin: https://app.example.com" "https://api.example.com/wp-json/my-plugin/v1/data"

# Test an OPTIONS request
curl -I -X OPTIONS -H "Origin: https://app.example.com" -H "Access-Control-Request-Method: GET" -H "Access-Control-Request-Headers: Content-Type, Authorization" "https://api.example.com/wp-json/my-plugin/v1/data"

# Test with authentication (e.g., Bearer token)
curl -I -H "Origin: https://app.example.com" -H "Authorization: Bearer YOUR_ACCESS_TOKEN" "https://api.example.com/wp-json/my-plugin/v1/data"

Examine the output headers from curl. If the expected CORS headers are missing or incorrect, the problem lies in your server-side configuration or PHP code.

Proxying Requests (Development)

During development, you can configure your frontend development server (e.g., Webpack Dev Server, Vite) to proxy API requests to your backend. This bypasses the browser’s same-origin policy for development purposes.

// Example Webpack Dev Server proxy configuration (webpack.config.js)
module.exports = {
  // ... other config
  devServer: {
    proxy: {
      '/wp-json': {
        target: 'https://api.example.com', // Your WordPress API URL
        changeOrigin: true,
        pathRewrite: { '^/wp-json': '/wp-json' },
      },
    },
  },
};

While this is a development convenience, it doesn’t solve the underlying CORS issue for production. It’s useful for isolating whether the problem is with your frontend’s request or the backend’s response.

Logging Disallowed Origins

As shown in the PHP example, adding logging for disallowed origins can be invaluable. In a production environment, you can monitor these logs to identify unexpected or malicious requests attempting to bypass CORS.

// Inside the 'else' block for disallowed origins
if ( ! in_array( $origin, $allowed_origins ) ) {
    // Use a dedicated log file or WordPress's debug log
    error_log( sprintf(
        'CORS Policy Violation: Origin "%s" not allowed for request to "%s". Allowed: %s',
        $origin,
        $_SERVER['REQUEST_URI'],
        implode( ', ', $allowed_origins )
    ) );
    // Optionally, return a 403 Forbidden response here if not handled by WP core
    // return new WP_Error( 'rest_cors_forbidden', 'CORS policy violation', array( 'status' => 403 ) );
}

This logging helps you understand traffic patterns and identify if a specific site within your multi-site network is misconfigured or if external domains are attempting unauthorized access.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Debugging and Resolving deep-seated hook priority conflicts in third-party Shopify headless API connectors
  • How to construct high-throughput import engines for large vendor commission records sets using custom XML/JSON parsers
  • Optimizing p99 database query response latency in multi-site Service Provider custom tables
  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in custom product catalogs
  • WordPress Development Recipe: Leveraging Nullsafe operator pipelines to build type-safe, auto-wired hooks

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (658)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (872)
  • PHP (5)
  • PHP Development (48)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (155)
  • WordPress Plugin Development (177)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Debugging and Resolving deep-seated hook priority conflicts in third-party Shopify headless API connectors
  • How to construct high-throughput import engines for large vendor commission records sets using custom XML/JSON parsers
  • Optimizing p99 database query response latency in multi-site Service Provider custom tables

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala