Cross-Origin Resource Sharing (CORS)
CORS is a browser security feature that restricts cross-origin HTTP requests initiated from scripts running in the browser. This guide explains how to enable CORS in your Drupal backend and how to configure the SDK on the client.
Client example
import { DrupalSDK } from "drupal-js-sdk";
import { FetchClient } from "@drupal-js-sdk/xhr";
const sdk = new DrupalSDK({ baseURL: "https://example.com" });
// Optional: override client
sdk.setClientService(new FetchClient({ baseURL: "https://example.com" }));
What and Why?
Cross-Origin Resource Sharing (CORS)1 controls which websites can call your Drupal backend from the browser. If your frontend runs on a different origin (scheme/host/port), you must enable and configure CORS on the Drupal side.
Configure Drupal (services.yml)
CORS in Drupal is configured2 in sites/default/services.yml (or an environment-specific override).
Example
Minimal example (GET only, public APIs):
# sites/default/services.yml
cors.config:
enabled: true
allowedOrigins: ['http://localhost:5173']
allowedHeaders: ['x-requested-with', 'content-type', 'accept']
allowedMethods: ['GET', 'OPTIONS']
exposedHeaders: []
maxAge: 86400
supportsCredentials: false
Cookie/session-based example (credentials, unsafe methods):
# sites/default/services.yml
cors.config:
enabled: true
allowedHeaders: ['x-csrf-token','authorization','content-type','accept','origin','x-requested-with', 'access-control-allow-origin','x-allowed-header']
allowedMethods: ['POST', 'GET', 'OPTIONS', 'DELETE', 'PUT', 'PATCH']
allowedOrigins: ['http://localhost:5173']
allowedOriginsPatterns: []
exposedHeaders: []
maxAge: false
supportsCredentials: true
Tip
For testing, you can use regex pattern for allowed origins on your developemnt server.
# sites/default/services.yml
cors.config:
enabled: true
allowedHeaders: ['x-csrf-token','authorization','content-type','accept','origin','x-requested-with', 'access-control-allow-origin','x-allowed-header']
allowedMethods: ['POST', 'GET', 'OPTIONS', 'DELETE', 'PUT', 'PATCH']
allowedOrigins: []
allowedOriginsPatterns: [
# To allow all subdomains in example.com.
'#^http://[a-z-]*\.example.com$#',
# To allow all localhost and all ports.
'#^http(s)?://(.+\.)?localhost(:\d{1,5})?$#',
]
exposedHeaders: []
maxAge: false
supportsCredentials: true
Warning
Wildcard origins + credentials
Browsers reject Access-Control-Allow-Origin: * when Access-Control-Allow-Credentials: true.
Use explicit origins (exact host, protocol and port).
Tip
CSRF tokens for write requests
For POST/PATCH/DELETE, include an X-CSRF-Token header. Drupal exposes a token endpoint at /session/token by default.
Frontend configuration
Note
SameSite cookies
When using cross-site cookies, ensure your session cookie is set with `SameSite=None; Secure` and served over HTTPS. Otherwise, browsers will not send the cookie.
Getting a CSRF token (Drupal)
const res = await fetch("https://api.example.com/session/token", {
credentials: "include",
});
const token = await res.text();
await fetch("https://api.example.com/jsonapi/node/article", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/vnd.api+json",
"X-CSRF-Token": token,
},
body: JSON.stringify({
data: {
type: "node--article",
attributes: { title: "Hello world" },
},
}),
});
Verify and troubleshoot
- Preflight (OPTIONS) must succeed with:
Access-Control-Allow-Originmatching your exact originAccess-Control-Allow-Methodsincluding the intended verbAccess-Control-Allow-Headersincluding custom headers (e.g.,authorization,x-csrf-token)-
Access-Control-Allow-Credentials: trueif using cookies -
Quick checks:
- Origins must match (protocol + host + port).
http://localhost:5173≠http://localhost:3000. - Don’t use
*with credentials. - Ensure reverse proxies/CDNs preserve the
Originheader and CORS response headers. -
Clear caches (
drush cr) after changingservices.yml. -
Curl preflight example:
curl -i -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type, x-csrf-token" \
https://api.example.com/jsonapi/node/article
Common pitfalls
- Using
*for origins while sending cookies or Authorization - Missing
OPTIONSinallowedMethods - Forgetting
authorizationorx-csrf-tokeninallowedHeaders - Cookie
SameSite=Laxpreventing cross-site requests (useSameSite=None; Secureover HTTPS)
-
Cross-Origin Resource Sharing (CORS): https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS ↩
-
Opt-in CORS support (Change record on Drupal.org): https://www.drupal.org/node/2715637 ↩