Skip to content

feat(storage-*): include modified headers into the response headers of files when using adapters #12096

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions docs/upload/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ _An asterisk denotes that an option is required._
| **`withMetadata`** | If specified, appends metadata to the output image file. Accepts a boolean or a function that receives `metadata` and `req`, returning a boolean. |
| **`hideFileInputOnCreate`** | Set to `true` to prevent the admin UI from showing file inputs during document creation, useful for programmatic file generation. |
| **`hideRemoveFile`** | Set to `true` to prevent the admin UI having a way to remove an existing file while editing. |
| **`modifyResponseHeaders`** | Accepts an object with existing `headers` and allows you to manipulate the response headers for media files. [More](#modifying-response-headers) |

### Payload-wide Upload Options

Expand Down Expand Up @@ -409,7 +410,7 @@ To fetch files from **restricted URLs** that would otherwise be blocked by CORS,

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

```
```ts
import type { CollectionConfig } from 'payload'

export const Media: CollectionConfig = {
Expand All @@ -422,7 +423,7 @@ export const Media: CollectionConfig = {
pathname: '',
port: '',
protocol: 'https',
search: ''
search: '',
},
{
hostname: 'example.com',
Expand Down Expand Up @@ -457,3 +458,44 @@ _An asterisk denotes that an option is required._
## Access Control

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.

## Modifying response headers

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.

### Modifying existing headers

With this method you can directly interface with the `Headers` object and modify the existing headers to append or remove headers.

```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
},
},
}
```

### Return new headers

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.

```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
},
},
}
```
5 changes: 4 additions & 1 deletion packages/payload/src/uploads/endpoints/getFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ export const getFileHandler: PayloadHandler = async (req) => {

if (collection.config.upload.handlers?.length) {
let customResponse = null
const headers = new Headers()

for (const handler of collection.config.upload.handlers) {
customResponse = await handler(req, {
doc: accessResult,
headers,
params: {
collection: collection.config.slug,
filename,
Expand Down Expand Up @@ -96,7 +99,7 @@ export const getFileHandler: PayloadHandler = async (req) => {
headers.set('Content-Type', fileTypeResult.mime)
headers.set('Content-Length', stats.size + '')
headers = collection.config.upload?.modifyResponseHeaders
? collection.config.upload.modifyResponseHeaders({ headers })
? collection.config.upload.modifyResponseHeaders({ headers }) || headers
: headers

return new Response(data, {
Expand Down
3 changes: 2 additions & 1 deletion packages/payload/src/uploads/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export type UploadConfig = {
req: PayloadRequest,
args: {
doc: TypeWithID
headers?: Headers
params: { clientUploadContext?: unknown; collection: string; filename: string }
},
) => Promise<Response> | Promise<void> | Response | void)[]
Expand All @@ -200,7 +201,7 @@ export type UploadConfig = {
* Ability to modify the response headers fetching a file.
* @default undefined
*/
modifyResponseHeaders?: ({ headers }: { headers: Headers }) => Headers
modifyResponseHeaders?: ({ headers }: { headers: Headers }) => Headers | void
/**
* Controls the behavior of pasting/uploading files from URLs.
* If set to `false`, fetching from remote URLs is disabled.
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-cloud-storage/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export type StaticHandler = (
req: PayloadRequest,
args: {
doc?: TypeWithID
headers?: Headers
params: { clientUploadContext?: unknown; collection: string; filename: string }
},
) => Promise<Response> | Response
Expand Down
32 changes: 27 additions & 5 deletions packages/storage-azure/src/staticHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface Args {
}

export const getHandler = ({ collection, getStorageClient }: Args): StaticHandler => {
return async (req, { params: { clientUploadContext, filename } }) => {
return async (req, { headers: incomingHeaders, params: { clientUploadContext, filename } }) => {
try {
const prefix = await getFilePrefix({ clientUploadContext, collection, filename, req })
const blockBlobClient = getStorageClient().getBlockBlobClient(
Expand All @@ -29,14 +29,36 @@ export const getHandler = ({ collection, getStorageClient }: Args): StaticHandle

const response = blob._response

const rawHeaders = { ...response.headers.rawHeaders() }

let initHeaders: HeadersInit = {
...rawHeaders,
}

// Typescript is difficult here with merging these types from Azure
if (incomingHeaders) {
initHeaders = {
...initHeaders,
...incomingHeaders,
}
}

let headers = new Headers(initHeaders)

const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
const objectEtag = response.headers.get('etag')

if (
collection.upload &&
typeof collection.upload === 'object' &&
typeof collection.upload.modifyResponseHeaders === 'function'
) {
headers = collection.upload.modifyResponseHeaders({ headers }) || headers
}

if (etagFromHeaders && etagFromHeaders === objectEtag) {
return new Response(null, {
headers: new Headers({
...response.headers.rawHeaders(),
}),
headers,
status: 304,
})
}
Expand All @@ -62,7 +84,7 @@ export const getHandler = ({ collection, getStorageClient }: Args): StaticHandle
})

return new Response(readableStream, {
headers: response.headers.rawHeaders(),
headers,
status: response.status,
})
} catch (err: unknown) {
Expand Down
28 changes: 17 additions & 11 deletions packages/storage-gcs/src/staticHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface Args {
}

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

let headers = new Headers(incomingHeaders)

headers.append('Content-Length', String(metadata.size))
headers.append('Content-Type', String(metadata.contentType))
headers.append('ETag', String(metadata.etag))

if (
collection.upload &&
typeof collection.upload === 'object' &&
typeof collection.upload.modifyResponseHeaders === 'function'
) {
headers = collection.upload.modifyResponseHeaders({ headers }) || headers
}

if (etagFromHeaders && etagFromHeaders === objectEtag) {
return new Response(null, {
headers: new Headers({
'Content-Length': String(metadata.size),
'Content-Type': String(metadata.contentType),
ETag: String(metadata.etag),
}),
headers,
status: 304,
})
}
Expand All @@ -50,11 +60,7 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat
})

return new Response(readableStream, {
headers: new Headers({
'Content-Length': String(metadata.size),
'Content-Type': String(metadata.contentType),
ETag: String(metadata.etag),
}),
headers,
status: 200,
})
} catch (err: unknown) {
Expand Down
31 changes: 18 additions & 13 deletions packages/storage-s3/src/staticHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const streamToBuffer = async (readableStream: any) => {
}

export const getHandler = ({ bucket, collection, getStorageClient }: Args): StaticHandler => {
return async (req, { params: { clientUploadContext, filename } }) => {
return async (req, { headers: incomingHeaders, params: { clientUploadContext, filename } }) => {
let object: AWS.GetObjectOutput | undefined = undefined
try {
const prefix = await getFilePrefix({ clientUploadContext, collection, filename, req })
Expand All @@ -57,17 +57,27 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat
return new Response(null, { status: 404, statusText: 'Not Found' })
}

let headers = new Headers(incomingHeaders)

headers.append('Content-Length', String(object.ContentLength))
headers.append('Content-Type', String(object.ContentType))
headers.append('Accept-Ranges', String(object.AcceptRanges))
headers.append('ETag', String(object.ETag))

const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
const objectEtag = object.ETag

if (
collection.upload &&
typeof collection.upload === 'object' &&
typeof collection.upload.modifyResponseHeaders === 'function'
) {
headers = collection.upload.modifyResponseHeaders({ headers }) || headers
}

if (etagFromHeaders && etagFromHeaders === objectEtag) {
return new Response(null, {
headers: new Headers({
'Accept-Ranges': String(object.AcceptRanges),
'Content-Length': String(object.ContentLength),
'Content-Type': String(object.ContentType),
ETag: String(object.ETag),
}),
headers,
status: 304,
})
}
Expand All @@ -88,12 +98,7 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat
const bodyBuffer = await streamToBuffer(object.Body)

return new Response(bodyBuffer, {
headers: new Headers({
'Accept-Ranges': String(object.AcceptRanges),
'Content-Length': String(object.ContentLength),
'Content-Type': String(object.ContentType),
ETag: String(object.ETag),
}),
headers,
status: 200,
})
} catch (err) {
Expand Down
33 changes: 21 additions & 12 deletions packages/storage-uploadthing/src/staticHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ type Args = {
}

export const getHandler = ({ utApi }: Args): StaticHandler => {
return async (req, { doc, params: { clientUploadContext, collection, filename } }) => {
return async (
req,
{ doc, headers: incomingHeaders, params: { clientUploadContext, collection, filename } },
) => {
try {
let key: string
const collectionConfig = req.payload.collections[collection]?.config

if (
clientUploadContext &&
Expand All @@ -21,7 +25,6 @@ export const getHandler = ({ utApi }: Args): StaticHandler => {
) {
key = clientUploadContext.key
} else {
const collectionConfig = req.payload.collections[collection]?.config
let retrievedDoc = doc

if (!retrievedDoc) {
Expand Down Expand Up @@ -82,23 +85,29 @@ export const getHandler = ({ utApi }: Args): StaticHandler => {
const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
const objectEtag = response.headers.get('etag')

let headers = new Headers(incomingHeaders)

headers.append('Content-Length', String(blob.size))
headers.append('Content-Type', blob.type)
headers.append('ETag', objectEtag)

if (
collectionConfig.upload &&
typeof collectionConfig.upload === 'object' &&
typeof collectionConfig.upload.modifyResponseHeaders === 'function'
) {
headers = collectionConfig.upload.modifyResponseHeaders({ headers }) || headers
}

if (etagFromHeaders && etagFromHeaders === objectEtag) {
return new Response(null, {
headers: new Headers({
'Content-Length': String(blob.size),
'Content-Type': blob.type,
ETag: objectEtag,
}),
headers,
status: 304,
})
}

return new Response(blob, {
headers: new Headers({
'Content-Length': String(blob.size),
'Content-Type': blob.type,
ETag: objectEtag,
}),
headers,
status: 200,
})
} catch (err) {
Expand Down
Loading