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.
The Druxt Drupal module enables CORS when it is otherwise disabled,
which covers anonymous reads because Drupal core's stock configuration
allows every origin. The module does not set the allowed methods, so
preflighted requests (form submissions, and any request carrying an
Authorization header) can still fail until you configure the block
below explicitly. Explicit configuration always wins, and is what
production sites should run.
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:
allowedOriginsis your frontend origin, scheme included, no trailing slash. List each environment that needs access (production, previews,http://localhost:3000, the Nuxt dev server's default, for local development), or use['*']while developing. Never deploy'*'together with credentials support.allowedMethodsneeds more than GET only if the site writes through JSON:API (forms).OPTIONSmust stay: browsers send it as the preflight.supportsCredentialsstaysfalsefor anonymous reads. Set ittrueonly for cookie-based authenticated flows, in which caseallowedOriginsmust list explicit origins, never'*'.
Verify
Ask Drupal for a resource with an Origin header, and check the
response:
curl -sI -H "Origin: https://www.example.com" \
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.
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. |