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: falseThen rebuild caches:
drush cache:rebuildThe keys to get right:
allowedOrigins: the frontend origin, scheme included, no trailing slash. List every origin that needs access, such as production, previews andhttp://localhost:3000for 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 sendVary: Origin, and the cache keeps nothing.allowedMethods: addPOSTandPATCHonly if the site writes through JSON:API, such as forms. The browser checks the method it is about to send against this list.allowedHeaders:['*']coversAuthorizationandContent-Type. Drupal answers a preflight with the headers the browser asked for, so list headers only to narrow it.supportsCredentials:falseunless the browser sends cookies to Drupal. Iftrue, 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-controlThat 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-controlA 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. |