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