Skip to content

fix: prevent client-upload crash when image bytes are unavailable#16971

Draft
denolfe wants to merge 1 commit into
mainfrom
fix/uploads-handle-empty-buffer
Draft

fix: prevent client-upload crash when image bytes are unavailable#16971
denolfe wants to merge 1 commit into
mainfrom
fix/uploads-handle-empty-buffer

Conversation

@denolfe

@denolfe denolfe commented Jun 11, 2026

Copy link
Copy Markdown
Member

Overview

Fixes a 400 crash when uploading an image through a client-upload storage adapter whose staticHandler returns a 302 redirect, and lets collections that don't resize record image dimensions without pulling the file bytes back into the server.

During a client upload the file body never transits the server, so core fetches it back from the adapter to probe dimensions. A redirect (empty body) produced a 0-byte buffer that made image-size throw RangeError: Offset is outside the bounds of the DataView.

Key Changes

Guard dimension probing against missing or unreadable bytes

getImageSize now returns undefined for empty/missing buffers and catches image-size failures, so a redirect or corrupt body degrades to undefined dimensions instead of failing the upload. generateFileData tolerates the undefined result.

Send client-measured dimensions with the upload

The browser measures the image's natural width and height and includes them in the client-upload payload. generateFileData falls back to these when byte probing yields nothing, so dimensions are still recorded for redirect-only adapters.

Skip the byte fetch-back when the server needs no bytes

uploadRequiresFileData decides whether server-side work (sharp adjustments, animated types, imageSizes, crop edits) needs the file. When client dimensions are present and no such work applies, addDataAndFileToRequest skips the staticHandler fetch-back, so bytes never enter the server for collections that don't resize.

Design Decisions

Hardening getImageSize is the minimal, broadly protective change: no adapter can crash the upload pipeline by returning an empty body. It stands on its own regardless of the dimension-passing work.

The decision of whether bytes are needed lives in core rather than the adapter, because only core knows the collection's resize configuration. focalPoint alone does not require bytes, since createImageSizes returns early without imageSizes. The skip path is conservative: when bytes might be needed, it falls back to the existing fetch-back, so correctness never depends on guessing.

ProbedImageSize keeps its required numbers; only getImageSize's return type widens to | undefined. The resize machinery always runs with real bytes, so it stays untouched.

Overall Flow

sequenceDiagram
    participant Browser
    participant Core as addDataAndFileToRequest
    participant Adapter as staticHandler
    participant Gen as generateFileData

    Browser->>Browser: measure naturalWidth/Height
    Browser->>Core: multipart with client-upload JSON (incl. width/height)
    alt dimensions present and no server-side work needed
        Core->>Gen: req.file with empty buffer + dimensions
        Note over Core,Adapter: fetch-back skipped, bytes stay in storage
    else bytes needed (imageSizes / crop / adjustments / animated)
        Core->>Adapter: fetch file back
        Adapter-->>Core: bytes (or redirect, yielding an empty buffer)
        Core->>Gen: req.file with bytes, dimensions carried through
    end
    Gen->>Gen: probe bytes, fall back to client dimensions if unreadable
Loading

A client-upload storage adapter whose staticHandler returns a 302 redirect
(empty body) crashed the upload with a 400: core fetched the body back into a
0-byte buffer, then getImageSize called image-size on it, throwing
"RangeError: Offset is outside the bounds of the DataView".

Fix A (crash guard): getImageSize now returns ProbedImageSize | undefined,
guarding empty/missing buffers and catching image-size errors so a corrupt or
empty body degrades to undefined dimensions instead of failing the upload.

Fix B (skip the fetch-back): the client measures naturalWidth/naturalHeight and
sends them in the client-upload JSON. uploadRequiresFileData determines whether
the server actually needs the bytes (sharp adjustments, animated types,
imageSizes, or crop edits — focalPoint alone does not). When dimensions are
supplied and no server-side work is needed, addDataAndFileToRequest skips the
staticHandler fetch-back entirely, so bytes never enter the server for
no-resize collections. generateFileData falls back to client dimensions when
probing yields nothing.
@github-actions

Copy link
Copy Markdown
Contributor

📦 esbuild Bundle Analysis for payload

This analysis was generated by esbuild-bundle-analyzer. 🤖

Meta File Out File Size (raw) Note
packages/next/meta_index.json esbuild/index.js 201.84 KB ✅ No change
packages/payload/meta_index.json esbuild/index.js 1.41 MB ⚠️ +832 B (+0.1%)
packages/payload/meta_shared.json esbuild/exports/shared.js 192.39 KB ✅ No change
packages/richtext-lexical/meta_client.json esbuild/exports/client_optimized/index.js 304.96 KB ✅ No change
packages/ui/meta_client.json esbuild/exports/client_optimized/index.js 1.37 MB ⚠️ +11.59 KB (+0.9%)
packages/ui/meta_shared.json esbuild/exports/shared_optimized/index.js 18.66 KB ✅ No change
Largest paths These visualization shows top 20 largest paths in the bundle.

Meta file: packages/next/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████████████████▋ }}}$ 98.9%, 197.86 KB
dist/adapters/router.js ${{\color{Goldenrod}{ }}}$ 0.3%, 663 B
dist/adapters/server.js ${{\color{Goldenrod}{ }}}$ 0.3%, 533 B
dist/adapters/layout.js ${{\color{Goldenrod}{ }}}$ 0.3%, 526 B
dist/adapters/views.js ${{\color{Goldenrod}{ }}}$ 0.2%, 409 B
dist/esbuildEntry.js ${{\color{Goldenrod}{ }}}$ 0.0%, 0 B

Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ █████████████████ }}}$ 68.3%, 960.54 KB
dist/fields/hooks ${{\color{Goldenrod}{ ▊ }}}$ 3.1%, 43.88 KB
dist/collections/operations ${{\color{Goldenrod}{ ▋ }}}$ 2.9%, 40.40 KB
dist/versions/migrations ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 18.50 KB
dist/auth/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 15.63 KB
dist/utilities/configToJSONSchema.js ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 14.93 KB
dist/globals/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 13.40 KB
dist/fields/config ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 13.14 KB
dist/queues/operations ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 12.63 KB
dist/fields/validations.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 10.57 KB
dist/collections/config ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 9.53 KB
dist/bin/generateImportMap ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 9.52 KB
dist/config/orderable ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.07 KB
dist/uploads/fetchAPI-multipart ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.80 KB
dist/index.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.77 KB
dist/hierarchy/utils ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 7.64 KB
dist/database/migrations ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 7.55 KB
dist/collections/endpoints ${{\color{Goldenrod}{ }}}$ 0.4%, 6.23 KB
dist/queues/config ${{\color{Goldenrod}{ }}}$ 0.4%, 5.59 KB
dist/auth/strategies ${{\color{Goldenrod}{ }}}$ 0.4%, 5.43 KB
(other) ${{\color{Goldenrod}{ ███████▉ }}}$ 31.7%, 445.92 KB

Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ███████████████████▉ }}}$ 79.6%, 150.12 KB
dist/fields/validations.js ${{\color{Goldenrod}{ █▍ }}}$ 5.6%, 10.57 KB
dist/config/orderable ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 3.13 KB
dist/fields/baseFields ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 2.79 KB
dist/utilities/deepCopyObject.js ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 2.54 KB
dist/auth/cookies.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 1.55 KB
dist/utilities/flattenTopLevelFields.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 1.42 KB
dist/fields/config ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 1.29 KB
dist/utilities/getVersionsConfig.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 1.04 KB
dist/utilities/flattenAllFields.js ${{\color{Goldenrod}{ }}}$ 0.4%, 794 B
dist/utilities/unflatten.js ${{\color{Goldenrod}{ }}}$ 0.4%, 779 B
dist/utilities/sanitizeUserDataForEmail.js ${{\color{Goldenrod}{ }}}$ 0.4%, 713 B
dist/utilities/getFieldPermissions.js ${{\color{Goldenrod}{ }}}$ 0.3%, 651 B
dist/collections/config ${{\color{Goldenrod}{ }}}$ 0.3%, 570 B
dist/bin/generateImportMap ${{\color{Goldenrod}{ }}}$ 0.3%, 561 B
dist/auth/sessions.js ${{\color{Goldenrod}{ }}}$ 0.3%, 525 B
dist/fields/getFieldPaths.js ${{\color{Goldenrod}{ }}}$ 0.3%, 485 B
dist/utilities/appendDateTimezoneSelectFields.js ${{\color{Goldenrod}{ }}}$ 0.2%, 432 B
dist/utilities/getSafeRedirect.js ${{\color{Goldenrod}{ }}}$ 0.2%, 423 B
dist/utilities/deepMerge.js ${{\color{Goldenrod}{ }}}$ 0.2%, 413 B
(other) ${{\color{Goldenrod}{ █████ }}}$ 20.4%, 38.49 KB

Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
dist/features/blocks ${{\color{Goldenrod}{ ███ }}}$ 12.3%, 37.13 KB
dist/lexical/ui ${{\color{Goldenrod}{ ██▊ }}}$ 11.3%, 34.16 KB
dist/lexical/plugins ${{\color{Goldenrod}{ ██▋ }}}$ 10.9%, 32.88 KB
dist/features/experimental_table ${{\color{Goldenrod}{ ██▎ }}}$ 9.0%, 27.16 KB
dist/packages/@lexical ${{\color{Goldenrod}{ █▌ }}}$ 6.3%, 18.99 KB
dist/features/link ${{\color{Goldenrod}{ █▌ }}}$ 6.2%, 18.83 KB
dist/features/toolbars ${{\color{Goldenrod}{ █▍ }}}$ 5.5%, 16.58 KB
dist/features/upload ${{\color{Goldenrod}{ █▏ }}}$ 4.7%, 14.09 KB
dist/features/textState ${{\color{Goldenrod}{ ▉ }}}$ 3.7%, 11.08 KB
dist/features/relationship ${{\color{Goldenrod}{ ▊ }}}$ 3.2%, 9.61 KB
dist/lexical/utils ${{\color{Goldenrod}{ ▋ }}}$ 2.9%, 8.79 KB
dist/features/converters ${{\color{Goldenrod}{ ▋ }}}$ 2.8%, 8.36 KB
dist/utilities/fieldsDrawer ${{\color{Goldenrod}{ ▋ }}}$ 2.7%, 8.12 KB
dist/features/debug ${{\color{Goldenrod}{ ▋ }}}$ 2.5%, 7.40 KB
dist/lexical/config ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 5.08 KB
dist/features/lists ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 5.00 KB
dist/features/format ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.46 KB
dist/lexical/LexicalEditor.js ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.23 KB
dist/features/horizontalRule ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.18 KB
dist/field/Field.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 2.83 KB
(other) ${{\color{Goldenrod}{ █████████████████████▉ }}}$ 87.7%, 264.60 KB

Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ██████████▋ }}}$ 42.8%, 580.42 KB
dist/elements/Hierarchy ${{\color{Goldenrod}{ ▊ }}}$ 3.2%, 43.09 KB
dist/elements/BulkUpload ${{\color{Goldenrod}{ ▌ }}}$ 2.1%, 28.18 KB
dist/views/Version ${{\color{Goldenrod}{ ▌ }}}$ 2.0%, 27.42 KB
dist/views/HierarchyList ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 20.43 KB
dist/elements/Table ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 19.27 KB
dist/views/Edit ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 17.78 KB
dist/views/Dashboard ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 17.73 KB
dist/elements/WhereBuilder ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 17.39 KB
dist/forms/Form ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 15.94 KB
dist/fields/Relationship ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 15.48 KB
dist/fields/Blocks ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 15.06 KB
dist/fields/Upload ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 14.40 KB
dist/elements/QueryPresets ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 10.22 KB
dist/elements/PublishButton ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.67 KB
dist/elements/ReactSelect ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.52 KB
dist/elements/HTMLDiff ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.38 KB
dist/elements/LivePreview ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.26 KB
dist/views/List ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.13 KB
dist/elements/UserMenu ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.98 KB
(other) ${{\color{Goldenrod}{ ██████████████▎ }}}$ 57.2%, 776.26 KB

Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js

Path Size
dist/graphics/Logo ${{\color{Goldenrod}{ ███████▋ }}}$ 30.9%, 5.57 KB
../../node_modules ${{\color{Goldenrod}{ ███▋ }}}$ 14.7%, 2.65 KB
dist/graphics/Icon ${{\color{Goldenrod}{ ██▏ }}}$ 8.5%, 1.52 KB
dist/utilities/formatDocTitle ${{\color{Goldenrod}{ █▊ }}}$ 7.3%, 1.32 KB
dist/providers/TableColumns ${{\color{Goldenrod}{ █▏ }}}$ 4.8%, 866 B
dist/utilities/getGlobalData.js ${{\color{Goldenrod}{ █ }}}$ 4.2%, 762 B
dist/utilities/api.js ${{\color{Goldenrod}{ █ }}}$ 4.2%, 756 B
dist/utilities/groupNavItems.js ${{\color{Goldenrod}{ █ }}}$ 4.1%, 745 B
dist/elements/Translation ${{\color{Goldenrod}{ ▋ }}}$ 2.7%, 493 B
dist/utilities/handleTakeOver.js ${{\color{Goldenrod}{ ▌ }}}$ 2.4%, 440 B
dist/utilities/traverseForLocalizedFields.js ${{\color{Goldenrod}{ ▌ }}}$ 2.3%, 419 B
dist/elements/withMergedProps ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 339 B
dist/utilities/getNavGroups.js ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 338 B
dist/utilities/getVisibleEntities.js ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 329 B
dist/elements/WithServerSideProps ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 232 B
dist/utilities/handleGoBack.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 180 B
dist/fields/mergeFieldStyles.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 157 B
dist/utilities/handleBackToDashboard.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 152 B
dist/forms/Form ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 148 B
dist/utilities/abortAndIgnore.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 146 B
(other) ${{\color{Goldenrod}{ █████████████████▎ }}}$ 69.1%, 12.47 KB
Details

Next to the size is how much the size has increased or decreased compared with the base branch of this PR.

  • ‼️: Size increased by 20% or more. Special attention should be given to this.
  • ⚠️: Size increased in acceptable range (lower than 20%).
  • ✅: No change or even downsized.
  • 🗑️: The out file is deleted: not found in base branch.
  • 🆕: The out file is newly found: will be added to base branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant