From cef2439601c2ab9d314f6d67ba22082aed69adc5 Mon Sep 17 00:00:00 2001 From: Vijay Misal Date: Fri, 7 Aug 2026 22:42:19 +0530 Subject: [PATCH] fix: preserve req.file across nested Local API calls from hooks createLocal and updateLocal mutate the req object passed via options.req in place (createLocalReq returns the same reference), then unconditionally overwrite req.file with `file ?? (await getFileByPath(filePath))`. When neither `file` nor `filePath` is passed to the nested Local API call (the documented pattern of forwarding the original req from a hook), this resolves to undefined and clobbers a file reference already set on req by the in-flight upload operation. Only assign req.file when a file or filePath is explicitly provided to the operation, so an existing req.file is preserved for hooks and adapters further down the chain. Fixes #15975 --- .../collections/operations/local/create.ts | 8 ++++- .../collections/operations/local/update.ts | 9 +++++- test/uploads/int.spec.ts | 31 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/payload/src/collections/operations/local/create.ts b/packages/payload/src/collections/operations/local/create.ts index 1862bdfe132..69823a2c007 100644 --- a/packages/payload/src/collections/operations/local/create.ts +++ b/packages/payload/src/collections/operations/local/create.ts @@ -217,7 +217,13 @@ export async function createLocal< const req = await createLocalReq(options as CreateLocalReqOptions, payload) - req.file = file ?? (await getFileByPath(filePath!)) + // Only assign `req.file` when a file or filePath was explicitly passed to this operation. + // Otherwise, this would overwrite (and drop) a file reference that was already set on `req` + // by an earlier upload operation, e.g. when a hook passes the original `req` through to a + // nested Local API call. See https://github.com/payloadcms/payload/issues/15975 + if (file || filePath) { + req.file = file ?? (await getFileByPath(filePath!)) + } return createOperation({ collection, diff --git a/packages/payload/src/collections/operations/local/update.ts b/packages/payload/src/collections/operations/local/update.ts index abfc1db555e..2f200b5d147 100644 --- a/packages/payload/src/collections/operations/local/update.ts +++ b/packages/payload/src/collections/operations/local/update.ts @@ -250,7 +250,14 @@ async function updateLocal< } const req = await createLocalReq(options as CreateLocalReqOptions, payload) - req.file = file ?? (await getFileByPath(filePath!)) + + // Only assign `req.file` when a file or filePath was explicitly passed to this operation. + // Otherwise, this would overwrite (and drop) a file reference that was already set on `req` + // by an earlier upload operation, e.g. when a hook passes the original `req` through to a + // nested Local API call. See https://github.com/payloadcms/payload/issues/15975 + if (file || filePath) { + req.file = file ?? (await getFileByPath(filePath!)) + } const args = { id, diff --git a/test/uploads/int.spec.ts b/test/uploads/int.spec.ts index da540db8a02..86b710a2cb8 100644 --- a/test/uploads/int.spec.ts +++ b/test/uploads/int.spec.ts @@ -1808,6 +1808,37 @@ describe('Collections - Uploads', () => { }) }) + describe('req.file preservation across nested Local API calls', () => { + it('should preserve req.file when a hook calls the Local API with the original req', async () => { + const filePath = path.resolve(dirname, './image.png') + const file = await getFileByPath(filePath) + file.name = 'preserve-req-file-test.png' + + // Simulate `req` as it would exist on an upload request, i.e. already + // carrying a `file` reference before any nested Local API call is made. + const req = { file } as PayloadRequest + + // Simulate a hook (e.g. an upload collection's afterChange) that calls + // the Local API on another, non-upload collection, passing the original + // `req` through per the docs' recommendation to preserve request context. + const relationDoc = await payload.create({ + collection: relationSlug, + data: {}, + req, + }) + + expect(relationDoc.id).toBeDefined() + + // The original file reference on `req` must still be accessible after + // the nested Local API call returns, so that a subsequent hook (or an + // upload storage adapter) can still read it. + expect(req.file).toBeDefined() + expect(req.file?.name).toBe('preserve-req-file-test.png') + + await payload.delete({ collection: relationSlug, id: relationDoc.id }) + }) + }) + describe('serverURL handling', () => { it('should store relative URLs in database even when serverURL is set', async () => { // Temporarily set serverURL for this test