Skip to content

Configure CORS in Drupal

Allow the browser to talk to the backend directly by serving CORS headers from Drupal, instead of proxying through the frontend.

Before you start: read Request topology to confirm CORS is what you need. Only browser requests are subject to CORS; build and server rendering failures have other causes.

When the frontend and backend are on different origins, the browser blocks cross-origin JSON:API requests unless Drupal answers with CORS headers. This is the direct fix. The alternative, proxying through the frontend, avoids CORS but only works while a Nuxt server is running. A generated static site needs CORS.

If cors.config is not enabled in sites/default/services.yml, the Druxt module enables it and sets an empty allowedHeaders or allowedMethods to *. With core's default allowedOrigins of *, every origin can then read and write. Enable it yourself and the module changes nothing. Production sites should set their own origins and methods.

Before drupal/druxt 1.3.0 the module left allowedMethods empty, so reads worked and writes failed. Upgrade with composer require drupal/druxt:^1.3.1, or configure the block.

Enable cors.config

Drupal core includes the configuration, disabled, in sites/default/default.services.yml. Copy the file to sites/default/services.yml if it does not exist, and set the cors.config block:

parameters:
  cors.config:
    enabled: true
    allowedHeaders: ['*']
    allowedMethods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS']
    allowedOrigins: ['https://www.example.com']
    exposedHeaders: false
    maxAge: 1000
    supportsCredentials: false

Then rebuild caches:

drush cache:rebuild

The keys to get right:

  • allowedOrigins: the frontend origin, scheme included, no trailing slash. List every origin that needs access, such as production, previews and http://localhost:3000 for the Nuxt dev server. ['*'] is fine while developing. A server-rendered site that uses the server cache needs one origin, or * without credentials: anything else makes Drupal send Vary: Origin, and the cache keeps nothing.
  • allowedMethods: add POST and PATCH only if the site writes through JSON:API, such as forms. The browser checks the method it is about to send against this list.
  • allowedHeaders: ['*'] covers Authorization and Content-Type. Drupal answers a preflight with the headers the browser asked for, so list headers only to narrow it.
  • supportsCredentials: false unless the browser sends cookies to Drupal. If true, list origins explicitly, never '*'.

Verify

Ask Drupal for a resource with an Origin header, and check the response:

curl -s -o /dev/null -D - -H "Origin: https://www.example.com" \
  https://cms.example.com/jsonapi | grep -i access-control

That covers simple reads. Requests with non-simple methods or headers (writes, Authorization) go through an OPTIONS preflight, so test that too, and check the allow-origin, allow-methods and allow-headers values that come back:

curl -si -X OPTIONS \
  -H "Origin: https://www.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: authorization,content-type" \
  https://cms.example.com/jsonapi | grep -i access-control

A working configuration answers with access-control-allow-origin: https://www.example.com. No access-control-* headers at all means the block is not loading: confirm the file is sites/default/services.yml, the site was cache-rebuilt, and your hosting platform does not strip the headers.

Check the headers, not the status code. A preflight the browser will refuse still answers 204 No Content, and curl does not enforce CORS, so a request that works by hand can still fail in the browser.

Per-environment configuration

Origins differ per environment, and services.yml is not part of config sync. A clean pattern used by production Druxt sites: keep a services.yml per environment (say sites/default/envs/<env>/services.yml) and include the right one from settings.php, so development can stay permissive while production lists exact origins.

When to prefer the proxy

Situation Use
Generated static site (nuxt generate) CORS. The proxy would need a server.
Nuxt server, one frontend origin Either. The proxy leaves the backend untouched.
Several frontends sharing one backend CORS, with each origin listed.
You cannot change the backend The proxy, and a server-rendered deployment.