Describe the Bug
When a POST /api/<upload-slug>?select[…]=true request creates a doc on a collection configured with @payloadcms/storage-s3 (or any cloud-storage adapter), and the select query projects mimeType (and/or filename) out of the result doc, the file is silently not uploaded to S3:
- HTTP 201 returned
- doc persisted to Mongo with all sharp-computed metadata (filename, mimeType, filesize, sizes)
- Bucket holds no object at the key
GET <public-url> → 403 / 404
- Server logs: nothing — no error, no warning
The same request without the select query writes all variants to S3 normally.
Root cause
@payloadcms/plugin-cloud-storage/src/utilities/getIncomingFiles.ts reads filename and mimeType from the data argument (the doc passed through hooks):
if (file && data.filename && data.mimeType) {
const mainFile = { buffer: file.data, ..., mimeType: data.mimeType }
files = [mainFile]
// ...build sizes
}
return files
When the request uses ?select[id]=true&select[filename]=true&select[filesize]=true, the doc flowing through hooks is the select-projected version — data.mimeType is undefined. The condition fails, files = [], the afterChange hook in plugin-cloud-storage does nothing, and adapter.handleUpload is never called.
The file itself is on req.file (set by addDataAndFileToRequest / processMultipart) with the correct mime type the whole time. The hook just refuses to use it because the projected doc is missing the field.
Symptom signature
Side-by-side from the reproduction, same source file, same auth, same endpoint:
=== WITH ?select[id,filename,filesize] (broken) → 201 ===
{ "filename": "bug-with-select-….webp", "filesize": 5000, "id": "…" }
bucket: 0 objects for that key
=== WITHOUT select (control) → 201 ===
{ "filename": "ok-without-select-….webp", "filesize": 5000 }
bucket: 5 objects (original + xs/sm/md/og variants)
Link to the code that reproduces this issue
https://github.com/jhb-dev/payload-s3-client-uploads-rest-silent-skip
Reproduction Steps
-
Clone the reproduction repository and run the development server:
docker compose up -d # starts mongo + minio + creates the bucket
pnpm install
pnpm dev
-
In another terminal, run the reproduction script:
node scripts/reproduce.mjs
The script registers an admin user, mints an API key, and uploads the same test.webp twice — first with ?select[id]=true&select[filename]=true&select[filesize]=true, then with no select query.
-
Inspect the bucket:
docker exec repro-minio mc ls local/payload-repro-bucket
-
Expected: both uploads land in the bucket (original + size variants).
-
Actual: only the control upload (without select) is in the bucket. The first upload — returned 201 with a created doc — has no objects. The doc points at a filename whose binary never reached storage. GET against media.url returns 403/404. No server-side error logged.
Suggested Fix
getIncomingFiles should fall back to the in-memory file object when data.filename / data.mimeType are missing — those fields are already populated on req.file regardless of what the response doc projection includes:
// packages/plugin-cloud-storage/src/utilities/getIncomingFiles.ts
const effectiveFilename = data.filename ?? file?.name
const effectiveMimeType = data.mimeType ?? file?.mimetype
if (file && effectiveFilename && effectiveMimeType) {
const mainFile = {
buffer: file.data,
clientUploadContext: file.clientUploadContext,
filename: effectiveFilename,
filesize: file.size,
mimeType: effectiveMimeType,
tempFilePath: file.tempFilePath,
}
files = [mainFile]
// …
}
Verified locally with pnpm patch @payloadcms/plugin-cloud-storage@3.84.1 against the reproduction: the main object now uploads correctly when ?select[…] is used.
Follow-up: the same projection issue affects data.sizes. If sizes isn't in the select, image-size variants are still skipped even after the fix above. A complete fix should fall back to req.payloadUploadSizes for variants when data.sizes is missing.
Which area(s) are affected?
plugin: cloud-storage
Environment Info
Relevant Packages:
payload: 3.84.1
next: 16.2.6
@payloadcms/db-mongodb: 3.84.1
@payloadcms/graphql: 3.84.1
@payloadcms/next/utilities: 3.84.1
@payloadcms/plugin-cloud-storage: 3.84.1
@payloadcms/richtext-lexical: 3.84.1
@payloadcms/storage-s3: 3.84.1
Describe the Bug
When a
POST /api/<upload-slug>?select[…]=truerequest creates a doc on a collection configured with@payloadcms/storage-s3(or anycloud-storageadapter), and theselectquery projectsmimeType(and/orfilename) out of the result doc, the file is silently not uploaded to S3:GET <public-url>→ 403 / 404The same request without the
selectquery writes all variants to S3 normally.Root cause
@payloadcms/plugin-cloud-storage/src/utilities/getIncomingFiles.tsreadsfilenameandmimeTypefrom thedataargument (the doc passed through hooks):When the request uses
?select[id]=true&select[filename]=true&select[filesize]=true, thedocflowing through hooks is the select-projected version —data.mimeTypeisundefined. The condition fails,files = [], theafterChangehook inplugin-cloud-storagedoes nothing, andadapter.handleUploadis never called.The file itself is on
req.file(set byaddDataAndFileToRequest/processMultipart) with the correct mime type the whole time. The hook just refuses to use it because the projected doc is missing the field.Symptom signature
Side-by-side from the reproduction, same source file, same auth, same endpoint:
Link to the code that reproduces this issue
https://github.com/jhb-dev/payload-s3-client-uploads-rest-silent-skip
Reproduction Steps
Clone the reproduction repository and run the development server:
In another terminal, run the reproduction script:
The script registers an admin user, mints an API key, and uploads the same
test.webptwice — first with?select[id]=true&select[filename]=true&select[filesize]=true, then with noselectquery.Inspect the bucket:
Expected: both uploads land in the bucket (original + size variants).
Actual: only the control upload (without
select) is in the bucket. The first upload — returned 201 with a created doc — has no objects. The doc points at a filename whose binary never reached storage.GETagainstmedia.urlreturns 403/404. No server-side error logged.Suggested Fix
getIncomingFilesshould fall back to the in-memoryfileobject whendata.filename/data.mimeTypeare missing — those fields are already populated onreq.fileregardless of what the response doc projection includes:Verified locally with
pnpm patch @payloadcms/plugin-cloud-storage@3.84.1against the reproduction: the main object now uploads correctly when?select[…]is used.Follow-up: the same projection issue affects
data.sizes. Ifsizesisn't in the select, image-size variants are still skipped even after the fix above. A complete fix should fall back toreq.payloadUploadSizesfor variants whendata.sizesis missing.Which area(s) are affected?
plugin: cloud-storage
Environment Info