Skip to content

Commit 9c72ab9

Browse files
feat: configure cors allowed headers (#6837)
## Description Currently, the Payload doesn't support to extend the Allowed Headers in CORS context. With this PR, `cors` property can be an object with `origins` and `headers`. - [x] I have read and understand the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository. ## Type of change - [ ] Chore (non-breaking change which does not add functionality) - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Change to the [templates](https://github.com/payloadcms/payload/tree/main/templates) directory (does not affect core functionality) - [ ] Change to the [examples](https://github.com/payloadcms/payload/tree/main/examples) directory (does not affect core functionality) - [x] This change requires a documentation update ## Checklist: - [x] I have added tests that prove my fix is effective or that my feature works - [x] Existing test suite passes locally with my changes - [x] I have made corresponding changes to the documentation Co-authored-by: Alessio Gravili <alessio@gravili.de>
1 parent f494eba commit 9c72ab9

File tree

6 files changed

+92
-11
lines changed

6 files changed

+92
-11
lines changed

docs/configuration/overview.mdx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ The following options are available:
7373
| **`serverURL`** | A string used to define the absolute URL of your app including the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port |
7474
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
7575
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
76-
| **`cors`** | Either a whitelist array of URLS to allow CORS requests from, or a wildcard string (`'*'`) to accept incoming requests from any domain. |
76+
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More](#cors) |
7777
| **`localization`** | Opt-in and control how Payload handles the translation of your content into multiple locales. [More details](./localization). |
7878
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
7979
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
@@ -179,3 +179,37 @@ When `PAYLOAD_CONFIG_PATH` is set, Payload will use this path to load the config
179179
Payload collects **completely anonymous** telemetry data about general usage. This data is super important to us and helps us accurately understand how we're growing and what we can do to build the software into everything that it can possibly be. The telemetry that we collect also help us demonstrate our growth in an accurate manner, which helps us as we seek investment to build and scale our team. If we can accurately demonstrate our growth, we can more effectively continue to support Payload as free and open-source software. To opt out of telemetry, you can pass `telemetry: false` within your Payload Config.
180180

181181
For more information about what we track, take a look at our [privacy policy](/privacy).
182+
183+
## Cross-origin resource sharing (CORS)
184+
185+
Cross-origin resource sharing (CORS) can be configured with either a whitelist array of URLS to allow CORS requests from, a wildcard string (`*`) to accept incoming requests from any domain, or a object with the following properties:
186+
187+
| Option | Description |
188+
| --------- | --------------------------------------------------------------------------------------------------------------------------------------- |
189+
| `origins` | Either a whitelist array of URLS to allow CORS requests from, or a wildcard string (`'*'`) to accept incoming requests from any domain. |
190+
| `headers | A list of allowed headers that will be appended in `Access-Control-Allow-Headers`. |
191+
192+
Here's an example showing how to allow incoming requests from any domain:
193+
194+
```ts
195+
import { buildConfig } from 'payload/config'
196+
197+
export default buildConfig({
198+
// ...
199+
cors: '*'
200+
})
201+
```
202+
203+
Here's an example showing how to append a new header (`x-custom-header`) in `Access-Control-Allow-Headers`:
204+
205+
```ts
206+
import { buildConfig } from 'payload/config'
207+
208+
export default buildConfig({
209+
// ...
210+
cors: {
211+
origins: ['http://localhost:3000']
212+
headers: ['x-custom-header']
213+
}
214+
})
215+
```

packages/next/src/utilities/headersWithCors.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { PayloadRequest } from 'payload'
1+
import type { CORSConfig, PayloadRequest } from 'payload'
22

33
type CorsArgs = {
44
headers: Headers
@@ -9,15 +9,36 @@ export const headersWithCors = ({ headers, req }: CorsArgs): Headers => {
99
const requestOrigin = req?.headers.get('Origin')
1010

1111
if (cors) {
12+
const defaultAllowedHeaders = [
13+
'Origin',
14+
'X-Requested-With',
15+
'Content-Type',
16+
'Accept',
17+
'Authorization',
18+
'Content-Encoding',
19+
'x-apollo-tracing',
20+
]
21+
1222
headers.set('Access-Control-Allow-Methods', 'PUT, PATCH, POST, GET, DELETE, OPTIONS')
13-
headers.set(
14-
'Access-Control-Allow-Headers',
15-
'Origin, X-Requested-With, Content-Type, Accept, Authorization, Content-Encoding, x-apollo-tracing',
16-
)
1723

18-
if (cors === '*') {
24+
if (typeof cors === 'object' && 'headers' in cors) {
25+
headers.set(
26+
'Access-Control-Allow-Headers',
27+
[...defaultAllowedHeaders, ...cors.headers].filter(Boolean).join(', '),
28+
)
29+
} else {
30+
headers.set('Access-Control-Allow-Headers', defaultAllowedHeaders.join(', '))
31+
}
32+
33+
if (cors === '*' || (typeof cors === 'object' && 'origins' in cors && cors.origins === '*')) {
1934
headers.set('Access-Control-Allow-Origin', '*')
20-
} else if (Array.isArray(cors) && cors.indexOf(requestOrigin) > -1) {
35+
} else if (
36+
(Array.isArray(cors) && cors.indexOf(requestOrigin) > -1) ||
37+
(!Array.isArray(cors) &&
38+
typeof cors === 'object' &&
39+
'origins' in cors &&
40+
cors.origins.indexOf(requestOrigin) > -1)
41+
) {
2142
headers.set('Access-Control-Allow-Credentials', 'true')
2243
headers.set('Access-Control-Allow-Origin', requestOrigin)
2344
}

packages/payload/src/config/schema.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,14 @@ export default joi.object({
104104
),
105105
collections: joi.array(),
106106
cookiePrefix: joi.string(),
107-
cors: [joi.string().valid('*'), joi.array().items(joi.string())],
107+
cors: [
108+
joi.string().valid('*'),
109+
joi.array().items(joi.string()),
110+
joi.object().keys({
111+
headers: joi.array().items(joi.string()),
112+
origins: [joi.string().valid('*'), joi.array().items(joi.string())],
113+
}),
114+
],
108115
csrf: joi.array().items(joi.string().allow('')).sparse(),
109116
custom: joi.object().pattern(joi.string(), joi.any()),
110117
db: joi.any(),

packages/payload/src/config/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,11 @@ export type SharpDependency = (
440440
options?: sharp.SharpOptions,
441441
) => sharp.Sharp
442442

443+
export type CORSConfig = {
444+
headers?: string[]
445+
origins: '*' | string[]
446+
}
447+
443448
/**
444449
* This is the central configuration
445450
*
@@ -592,7 +597,7 @@ export type Config = {
592597
*/
593598
cookiePrefix?: string
594599
/** Either a whitelist array of URLS to allow CORS requests from, or a wildcard string ('*') to accept incoming requests from any domain. */
595-
cors?: '*' | string[]
600+
cors?: '*' | CORSConfig | string[]
596601
/** A whitelist array of URLs to allow Payload cookies to be accepted from as a form of CSRF protection. */
597602
csrf?: string[]
598603

test/config/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,8 @@ export default buildConfigWithDefaults({
113113
typescript: {
114114
outputFile: path.resolve(dirname, 'payload-types.ts'),
115115
},
116+
cors: {
117+
origins: '*',
118+
headers: ['x-custom-header'],
119+
},
116120
})

test/config/int.spec.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import type { BlockField, Payload } from 'payload'
22

3+
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
4+
35
import { initPayloadInt } from '../helpers/initPayloadInt.js'
46
import configPromise from './config.js'
57

8+
let restClient: NextRESTClient
69
let payload: Payload
710

811
describe('Config', () => {
912
beforeAll(async () => {
10-
;({ payload } = await initPayloadInt(configPromise))
13+
;({ payload, restClient } = await initPayloadInt(configPromise))
1114
})
1215

1316
afterAll(async () => {
@@ -91,4 +94,11 @@ describe('Config', () => {
9194
})
9295
})
9396
})
97+
98+
describe('cors config', () => {
99+
it('includes a custom header in Access-Control-Allow-Headers', async () => {
100+
const response = await restClient.GET(`/pages`)
101+
expect(response.headers.get('Access-Control-Allow-Headers')).toContain('x-custom-header')
102+
})
103+
})
94104
})

0 commit comments

Comments
 (0)