Skip to content

Commit cb6a73e

Browse files
paulpopusjmikrut
andauthored
feat(storage-*): include modified headers into the response headers of files when using adapters (#12096)
This PR makes it so that `modifyResponseHeaders` is supported in our adapters when set on the collection config. Previously it would be ignored. This means that users can now modify or append new headers to what's returned by each service. ```ts import type { CollectionConfig } from 'payload' export const Media: CollectionConfig = { slug: 'media', upload: { modifyResponseHeaders: ({ headers }) => { const newHeaders = new Headers(headers) // Copy existing headers newHeaders.set('X-Frame-Options', 'DENY') // Set new header return newHeaders }, }, } ``` Also adds support for `void` return on the `modifyResponseHeaders` function in the case where the user just wants to use existing headers and doesn't need more control. eg: ```ts import type { CollectionConfig } from 'payload' export const Media: CollectionConfig = { slug: 'media', upload: { modifyResponseHeaders: ({ headers }) => { headers.set('X-Frame-Options', 'DENY') // You can directly set headers without returning }, }, } ``` Manual testing checklist (no CI e2es setup for these envs yet): - [x] GCS - [x] S3 - [x] Azure - [x] UploadThing - [x] Vercel Blob --------- Co-authored-by: James <james@trbl.design>
1 parent 055cc4e commit cb6a73e

File tree

17 files changed

+256
-99
lines changed

17 files changed

+256
-99
lines changed

docs/upload/overview.mdx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ _An asterisk denotes that an option is required._
116116
| **`withMetadata`** | If specified, appends metadata to the output image file. Accepts a boolean or a function that receives `metadata` and `req`, returning a boolean. |
117117
| **`hideFileInputOnCreate`** | Set to `true` to prevent the admin UI from showing file inputs during document creation, useful for programmatic file generation. |
118118
| **`hideRemoveFile`** | Set to `true` to prevent the admin UI having a way to remove an existing file while editing. |
119+
| **`modifyResponseHeaders`** | Accepts an object with existing `headers` and allows you to manipulate the response headers for media files. [More](#modifying-response-headers) |
119120

120121
### Payload-wide Upload Options
121122

@@ -453,7 +454,7 @@ To fetch files from **restricted URLs** that would otherwise be blocked by CORS,
453454

454455
Here’s how to configure the pasteURL option to control remote URL fetching:
455456

456-
```
457+
```ts
457458
import type { CollectionConfig } from 'payload'
458459

459460
export const Media: CollectionConfig = {
@@ -466,7 +467,7 @@ export const Media: CollectionConfig = {
466467
pathname: '',
467468
port: '',
468469
protocol: 'https',
469-
search: ''
470+
search: '',
470471
},
471472
{
472473
hostname: 'example.com',
@@ -519,3 +520,44 @@ _An asterisk denotes that an option is required._
519520
## Access Control
520521

521522
All files that are uploaded to each Collection automatically support the `read` [Access Control](/docs/access-control/overview) function from the Collection itself. You can use this to control who should be allowed to see your uploads, and who should not.
523+
524+
## Modifying response headers
525+
526+
You can modify the response headers for files by specifying the `modifyResponseHeaders` option in your upload config. This option accepts an object with existing headers and allows you to manipulate the response headers for media files.
527+
528+
### Modifying existing headers
529+
530+
With this method you can directly interface with the `Headers` object and modify the existing headers to append or remove headers.
531+
532+
```ts
533+
import type { CollectionConfig } from 'payload'
534+
535+
export const Media: CollectionConfig = {
536+
slug: 'media',
537+
upload: {
538+
modifyResponseHeaders: ({ headers }) => {
539+
headers.set('X-Frame-Options', 'DENY') // You can directly set headers without returning
540+
},
541+
},
542+
}
543+
```
544+
545+
### Return new headers
546+
547+
You can also return a new `Headers` object with the modified headers. This is useful if you want to set new headers or remove existing ones.
548+
549+
```ts
550+
import type { CollectionConfig } from 'payload'
551+
552+
export const Media: CollectionConfig = {
553+
slug: 'media',
554+
upload: {
555+
modifyResponseHeaders: ({ headers }) => {
556+
const newHeaders = new Headers(headers) // Copy existing headers
557+
newHeaders.set('X-Frame-Options', 'DENY') // Set new header
558+
559+
return newHeaders
560+
},
561+
},
562+
}
563+
```

packages/payload/src/uploads/endpoints/getFile.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@ export const getFileHandler: PayloadHandler = async (req) => {
3838

3939
if (collection.config.upload.handlers?.length) {
4040
let customResponse: null | Response | void = null
41+
const headers = new Headers()
42+
4143
for (const handler of collection.config.upload.handlers) {
4244
customResponse = await handler(req, {
4345
doc: accessResult,
46+
headers,
4447
params: {
4548
collection: collection.config.slug,
4649
filename,
@@ -95,7 +98,7 @@ export const getFileHandler: PayloadHandler = async (req) => {
9598
headers.set('Content-Type', fileTypeResult.mime)
9699
headers.set('Content-Length', stats.size + '')
97100
headers = collection.config.upload?.modifyResponseHeaders
98-
? collection.config.upload.modifyResponseHeaders({ headers })
101+
? collection.config.upload.modifyResponseHeaders({ headers }) || headers
99102
: headers
100103

101104
return new Response(data, {

packages/payload/src/uploads/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ export type UploadConfig = {
211211
req: PayloadRequest,
212212
args: {
213213
doc: TypeWithID
214+
headers?: Headers
214215
params: { clientUploadContext?: unknown; collection: string; filename: string }
215216
},
216217
) => Promise<Response> | Promise<void> | Response | void)[]
@@ -233,7 +234,7 @@ export type UploadConfig = {
233234
* Ability to modify the response headers fetching a file.
234235
* @default undefined
235236
*/
236-
modifyResponseHeaders?: ({ headers }: { headers: Headers }) => Headers
237+
modifyResponseHeaders?: ({ headers }: { headers: Headers }) => Headers | void
237238
/**
238239
* Controls the behavior of pasting/uploading files from URLs.
239240
* If set to `false`, fetching from remote URLs is disabled.

packages/plugin-cloud-storage/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export type StaticHandler = (
5858
req: PayloadRequest,
5959
args: {
6060
doc?: TypeWithID
61+
headers?: Headers
6162
params: { clientUploadContext?: unknown; collection: string; filename: string }
6263
},
6364
) => Promise<Response> | Response

packages/storage-azure/src/staticHandler.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ interface Args {
1414
}
1515

1616
export const getHandler = ({ collection, getStorageClient }: Args): StaticHandler => {
17-
return async (req, { params: { clientUploadContext, filename } }) => {
17+
return async (req, { headers: incomingHeaders, params: { clientUploadContext, filename } }) => {
1818
try {
1919
const prefix = await getFilePrefix({ clientUploadContext, collection, filename, req })
2020
const blockBlobClient = getStorageClient().getBlockBlobClient(
@@ -30,14 +30,34 @@ export const getHandler = ({ collection, getStorageClient }: Args): StaticHandle
3030

3131
const response = blob._response
3232

33+
let initHeaders: Headers = {
34+
...(response.headers.rawHeaders() as unknown as Headers),
35+
}
36+
37+
// Typescript is difficult here with merging these types from Azure
38+
if (incomingHeaders) {
39+
initHeaders = {
40+
...initHeaders,
41+
...incomingHeaders,
42+
}
43+
}
44+
45+
let headers = new Headers(initHeaders)
46+
3347
const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
3448
const objectEtag = response.headers.get('etag')
3549

50+
if (
51+
collection.upload &&
52+
typeof collection.upload === 'object' &&
53+
typeof collection.upload.modifyResponseHeaders === 'function'
54+
) {
55+
headers = collection.upload.modifyResponseHeaders({ headers }) || headers
56+
}
57+
3658
if (etagFromHeaders && etagFromHeaders === objectEtag) {
3759
return new Response(null, {
38-
headers: new Headers({
39-
...response.headers.rawHeaders(),
40-
}),
60+
headers,
4161
status: 304,
4262
})
4363
}
@@ -63,7 +83,7 @@ export const getHandler = ({ collection, getStorageClient }: Args): StaticHandle
6383
})
6484

6585
return new Response(readableStream, {
66-
headers: response.headers.rawHeaders(),
86+
headers,
6787
status: response.status,
6888
})
6989
} catch (err: unknown) {

packages/storage-gcs/src/staticHandler.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ interface Args {
1212
}
1313

1414
export const getHandler = ({ bucket, collection, getStorageClient }: Args): StaticHandler => {
15-
return async (req, { params: { clientUploadContext, filename } }) => {
15+
return async (req, { headers: incomingHeaders, params: { clientUploadContext, filename } }) => {
1616
try {
1717
const prefix = await getFilePrefix({ clientUploadContext, collection, filename, req })
1818
const file = getStorageClient().bucket(bucket).file(path.posix.join(prefix, filename))
@@ -22,13 +22,23 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat
2222
const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
2323
const objectEtag = metadata.etag
2424

25+
let headers = new Headers(incomingHeaders)
26+
27+
headers.append('Content-Length', String(metadata.size))
28+
headers.append('Content-Type', String(metadata.contentType))
29+
headers.append('ETag', String(metadata.etag))
30+
31+
if (
32+
collection.upload &&
33+
typeof collection.upload === 'object' &&
34+
typeof collection.upload.modifyResponseHeaders === 'function'
35+
) {
36+
headers = collection.upload.modifyResponseHeaders({ headers }) || headers
37+
}
38+
2539
if (etagFromHeaders && etagFromHeaders === objectEtag) {
2640
return new Response(null, {
27-
headers: new Headers({
28-
'Content-Length': String(metadata.size),
29-
'Content-Type': String(metadata.contentType),
30-
ETag: String(metadata.etag),
31-
}),
41+
headers,
3242
status: 304,
3343
})
3444
}
@@ -50,11 +60,7 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat
5060
})
5161

5262
return new Response(readableStream, {
53-
headers: new Headers({
54-
'Content-Length': String(metadata.size),
55-
'Content-Type': String(metadata.contentType),
56-
ETag: String(metadata.etag),
57-
}),
63+
headers,
5864
status: 200,
5965
})
6066
} catch (err: unknown) {

packages/storage-s3/src/staticHandler.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export const getHandler = ({
6161
getStorageClient,
6262
signedDownloads,
6363
}: Args): StaticHandler => {
64-
return async (req, { params: { clientUploadContext, filename } }) => {
64+
return async (req, { headers: incomingHeaders, params: { clientUploadContext, filename } }) => {
6565
let object: AWS.GetObjectOutput | undefined = undefined
6666
try {
6767
const prefix = await getFilePrefix({ clientUploadContext, collection, filename, req })
@@ -94,17 +94,31 @@ export const getHandler = ({
9494
Key: key,
9595
})
9696

97+
if (!object.Body) {
98+
return new Response(null, { status: 404, statusText: 'Not Found' })
99+
}
100+
101+
let headers = new Headers(incomingHeaders)
102+
103+
headers.append('Content-Length', String(object.ContentLength))
104+
headers.append('Content-Type', String(object.ContentType))
105+
headers.append('Accept-Ranges', String(object.AcceptRanges))
106+
headers.append('ETag', String(object.ETag))
107+
97108
const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
98109
const objectEtag = object.ETag
99110

111+
if (
112+
collection.upload &&
113+
typeof collection.upload === 'object' &&
114+
typeof collection.upload.modifyResponseHeaders === 'function'
115+
) {
116+
headers = collection.upload.modifyResponseHeaders({ headers }) || headers
117+
}
118+
100119
if (etagFromHeaders && etagFromHeaders === objectEtag) {
101120
return new Response(null, {
102-
headers: new Headers({
103-
'Accept-Ranges': String(object.AcceptRanges),
104-
'Content-Length': String(object.ContentLength),
105-
'Content-Type': String(object.ContentType),
106-
ETag: String(object.ETag),
107-
}),
121+
headers,
108122
status: 304,
109123
})
110124
}
@@ -125,12 +139,7 @@ export const getHandler = ({
125139
const bodyBuffer = await streamToBuffer(object.Body)
126140

127141
return new Response(bodyBuffer, {
128-
headers: new Headers({
129-
'Accept-Ranges': String(object.AcceptRanges),
130-
'Content-Length': String(object.ContentLength),
131-
'Content-Type': String(object.ContentType),
132-
ETag: String(object.ETag),
133-
}),
142+
headers,
134143
status: 200,
135144
})
136145
} catch (err) {

packages/storage-uploadthing/src/staticHandler.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ type Args = {
99
}
1010

1111
export const getHandler = ({ utApi }: Args): StaticHandler => {
12-
return async (req, { doc, params: { clientUploadContext, collection, filename } }) => {
12+
return async (
13+
req,
14+
{ doc, headers: incomingHeaders, params: { clientUploadContext, collection, filename } },
15+
) => {
1316
try {
1417
let key: string
18+
const collectionConfig = req.payload.collections[collection]?.config
1519

1620
if (
1721
clientUploadContext &&
@@ -21,7 +25,6 @@ export const getHandler = ({ utApi }: Args): StaticHandler => {
2125
) {
2226
key = clientUploadContext.key
2327
} else {
24-
const collectionConfig = req.payload.collections[collection]?.config
2528
let retrievedDoc = doc
2629

2730
if (!retrievedDoc) {
@@ -82,23 +85,32 @@ export const getHandler = ({ utApi }: Args): StaticHandler => {
8285
const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
8386
const objectEtag = response.headers.get('etag')
8487

88+
let headers = new Headers(incomingHeaders)
89+
90+
headers.append('Content-Length', String(blob.size))
91+
headers.append('Content-Type', blob.type)
92+
93+
if (objectEtag) {
94+
headers.append('ETag', objectEtag)
95+
}
96+
97+
if (
98+
collectionConfig?.upload &&
99+
typeof collectionConfig.upload === 'object' &&
100+
typeof collectionConfig.upload.modifyResponseHeaders === 'function'
101+
) {
102+
headers = collectionConfig.upload.modifyResponseHeaders({ headers }) || headers
103+
}
104+
85105
if (etagFromHeaders && etagFromHeaders === objectEtag) {
86106
return new Response(null, {
87-
headers: new Headers({
88-
'Content-Length': String(blob.size),
89-
'Content-Type': blob.type,
90-
ETag: objectEtag,
91-
}),
107+
headers,
92108
status: 304,
93109
})
94110
}
95111

96112
return new Response(blob, {
97-
headers: new Headers({
98-
'Content-Length': String(blob.size),
99-
'Content-Type': blob.type,
100-
ETag: objectEtag!,
101-
}),
113+
headers,
102114
status: 200,
103115
})
104116
} catch (err) {

0 commit comments

Comments
 (0)