Drupal CORS Configuration for APIs and Decoupled Applications

Drupal

Drupal CORS Configuration for APIs and Decoupled Applications

A practical guide to configuring Drupal CORS for JSON:API, REST, decoupled frontends, credentialed requests, preflight debugging, and secure production deployments.

When Drupal powers a decoupled frontend, mobile application, partner integration, or JavaScript-heavy API consumer, you will eventually meet CORS. The browser blocks the frontend, the network tab shows a mysterious preflight error, and the API works perfectly in Postman. That is the classic CORS moment.

CORS stands for Cross-Origin Resource Sharing. It is a browser security mechanism that controls whether JavaScript loaded from one origin can read responses from another origin. For Drupal APIs, CORS is usually configured in services.yml, and the safest setup depends on your authentication model, frontend domains, CDN, and deployment environments.

CORS flow between decoupled frontend browser and Drupal API
CORS is enforced by the browser. Drupal sends headers; the browser decides whether frontend JavaScript can read the response.

What CORS Is And Is Not

CORS is not an API firewall. It does not stop curl, Postman, bots, server-side scripts, or another backend from calling Drupal. It only controls browser-based cross-origin reads.

For example:

  • https://app.example.com is your React or Next.js frontend.
  • https://api.example.com is your Drupal API.
  • The browser sees those as different origins because the hostnames are different.
  • Drupal must send CORS headers before the browser lets frontend JavaScript read the response.

Authentication and authorization still belong to Drupal: permissions, routes, access checks, OAuth, cookies, CSRF tokens, or whatever security model your API uses.

Where Drupal Configures CORS

Drupal ships the CORS configuration keys in web/sites/default/default.services.yml. In a real site, copy default.services.yml to services.yml if it does not already exist, then edit the cors.config section.

This file is not ordinary Drupal configuration. It is not exported with drush config:export. Treat it like environment-specific infrastructure configuration, similar to settings.php.

Drupal services.yml CORS configuration example
The core CORS keys live under parameters: cors.config in services.yml.

A Practical CORS Example For A Decoupled App

For a frontend at https://app.example.com calling Drupal at https://api.example.com, a focused configuration might look like this:

parameters:
  cors.config:
    enabled: true
    allowedHeaders:
      - Authorization
      - Content-Type
      - X-CSRF-Token
    allowedMethods:
      - GET
      - POST
      - PATCH
      - DELETE
      - OPTIONS
    allowedOrigins:
      - 'https://app.example.com'
    allowedOriginsPatterns: []
    exposedHeaders: false
    maxAge: 1000
    supportsCredentials: true

After changing services.yml, rebuild cache:

drush cache:rebuild

If Drupal runs in containers, make sure the updated file is actually inside the running container or baked into the image. Many “CORS did not change” problems are really deployment problems.

Understanding The Important Keys

KeyPurposePractical Advice
enabledTurns Drupal CORS handling on or off.Set to true only when cross-origin browser access is required.
allowedOriginsExact origins allowed to read responses.Prefer explicit frontend domains.
allowedOriginsPatternsRegex-style origin matching.Use sparingly for preview environments or controlled subdomains.
allowedMethodsHTTP methods allowed by CORS.Only list methods your API supports.
allowedHeadersRequest headers the browser may send.Include Authorization, Content-Type, and X-CSRF-Token only if needed.
exposedHeadersResponse headers frontend JavaScript may read.Expose only headers the frontend actually needs.
maxAgeHow long browsers may cache preflight results.Use a moderate value; lower it while debugging.
supportsCredentialsAllows credentialed requests such as cookies.Requires explicit origins, HTTPS, and careful CSRF planning.

Preflight Requests

For simple GET requests, the browser may send the actual request directly. For requests with JSON bodies, custom headers, authorization headers, or methods like PATCH and DELETE, the browser usually sends an OPTIONS request first. That is called a preflight request.

The preflight asks Drupal:

  • Is this origin allowed?
  • Is this method allowed?
  • Are these request headers allowed?
CORS preflight curl debugging command for Drupal API
Debug preflight separately. If OPTIONS fails, the browser never sends the real API request.

You can test a preflight request with curl:

curl -i -X OPTIONS 'https://api.example.com/jsonapi/node/article' \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: GET' \
  -H 'Access-Control-Request-Headers: Authorization, Content-Type'

Look for response headers such as:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true

Credentials, Cookies, And CSRF

If your decoupled app uses bearer tokens, CORS is mostly about allowing the Authorization header from trusted origins.

If your decoupled app uses Drupal session cookies, the setup is more sensitive:

  • supportsCredentials must be true.
  • The frontend request must use credentials: 'include'.
  • allowedOrigins must be explicit. A wildcard origin is not valid with credentials.
  • Cookies need appropriate SameSite, Secure, and domain settings.
  • State-changing requests need CSRF token handling.

Example browser request:

await fetch('https://api.example.com/jsonapi/node/article', {
  method: 'GET',
  credentials: 'include',
  headers: {
    'Accept': 'application/vnd.api+json'
  }
});

For authenticated API writes, token-based authentication is often simpler to reason about than cross-site cookies. Cookies can work, but they require stricter review.

Wildcard Origins

allowedOrigins: ['*'] is tempting during development. It can be acceptable for a fully public, read-only API that does not use credentials and does not expose sensitive data. It is usually wrong for authenticated decoupled apps.

Do not combine wildcard origins with credentials. Browsers do not allow Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. More importantly, it signals that the API boundary has not been thought through.

Multiple Environments

Real projects often need different origins by environment:

  • Local frontend: http://localhost:3000
  • Dev frontend: https://dev-app.example.com
  • Stage frontend: https://stage-app.example.com
  • Production frontend: https://app.example.com

Do not accidentally deploy local or dev origins to production unless they are intentionally supported. Keep services.yml environment-aware through deployment templates, container build arguments, mounted files, or hosting-specific configuration.

CORS With CDN And Reverse Proxies

If Drupal sits behind Akamai, Cloudflare, Fastly, Varnish, or an application load balancer, decide where CORS headers are added. You have two common patterns:

  1. Drupal controls CORS: proxies pass through OPTIONS and CORS response headers unchanged.
  2. Edge controls CORS: CDN or gateway responds to preflight and injects headers consistently.

Both can work. The dangerous version is when Drupal and the edge both add different CORS headers. Browsers are not forgiving when response headers conflict.

Security Checklist

Security checklist for Drupal CORS configuration
CORS should be narrow, intentional, and paired with real Drupal access control.
  • Use explicit origins for authenticated APIs.
  • Do not use wildcard origins with credentials.
  • Keep allowed methods limited to what your API needs.
  • Keep allowed headers limited to what the frontend sends.
  • Do not expose response headers unless the frontend needs them.
  • Use HTTPS for production origins.
  • Confirm authentication and permissions still protect the API.
  • Verify OPTIONS requests are not blocked by CDN, WAF, or web server rules.
  • Review production services.yml separately from local development settings.

Common Problems

It works in Postman but not in the browser

Postman is not subject to browser CORS enforcement. Test with browser dev tools and preflight curl commands.

The API route returns 200, but the browser still blocks it

The real request may be fine, but the preflight response may be missing CORS headers. Check the OPTIONS request in the Network tab.

Authorization header is blocked

Add Authorization to allowedHeaders. Also confirm the browser preflight asks for the same spelling and that proxies do not strip the header.

PATCH or DELETE fails

Add the method to allowedMethods, and confirm the route actually supports that method in Drupal.

Cookies are not sent

Set supportsCredentials: true, use an explicit origin, configure frontend fetch with credentials: 'include', and review cookie SameSite and Secure attributes.

Reference Links

Final Takeaway

Drupal CORS configuration should be precise, environment-aware, and aligned with your authentication model. For public read-only APIs, a broad origin policy may be reasonable. For authenticated decoupled applications, use exact origins, limited headers, limited methods, and explicit credential handling.

Most CORS bugs are solved by answering four questions: which frontend origin is calling Drupal, which method is it using, which headers is it sending, and whether credentials are involved. Configure only those needs, rebuild cache, and test the preflight request directly.

Keep reading

Drupal Sep 7, 2026 6 min read

Build a Drupal Chatbot with Local AI

Learn how a Drupal chatbot finds relevant published articles, uses Ollama to generate answers, and displays source links while keeping inference on your own hardware.