Skip to content

Remote form file is silently replaced with [object Object] when appended to FormData with a filename #17313

Description

@t-eckert

Describe the bug

In a remote form handler, an uploaded file passes instanceof File. Appending it to a FormData with the three-argument overload and forwarding it with fetch sends a well-formed multipart part with the right filename and content type, but with a body made up of the 15-byte string "[object Object]". Nothing throws or warns the user of this.

export const upload = form('unchecked', async ({ file }) => {
	const body = new FormData();
	body.append('file', file);            // intact
	body.append('file', file, file.name); // body becomes "[object Object]"
	await fetch(UPSTREAM, { method: 'POST', body });
});

FormData.set(name, file, filename) and new Blob([file]) lose the contents the same way. A genuine File through the same overload is unaffected.

Only enhanced submissions are affected. deserialize_binary_form rebuilds each upload as a LazyFile wrapped in a Proxy whose getPrototypeOf returns File.prototype. The same form submitted without JavaScript goes through request.formData(), gets a real File, and works, which is part of why this is easy to miss.

The underlying cause is an interaction between the "prototype lie" to get past undici's Blob type check by fooling its instanceOf test and the filename overload which then copies it with new File([value], filename). Node's Blob constructor does a real brand check and treats the part as a string. Without the proxy, append throws TypeError: parameter 2 is not of type 'Blob'. The proxy hides error.

I'd like to submit a PR. What I'd propose is making the failure loud, at least for now. As far as I can tell, undici and Node are behaving reasonably given a value that claims to be a File and isn't, so I don't think this can be fixed downstream. The real fixes I could find either buffer uploads, giving up the laziness LazyFile exists for, or change the API, and that seems like your call rather than mine. The guard can land without deciding it.

Here's my little patch, crafted with love: 46c2292

Reproduction

https://github.com/t-eckert/sveltekit-lazyfile-formdata

pnpm install
pnpm dev

Open http://127.0.0.1:5199, choose any small text file, submit. The handler appends the file three ways, forwards each to /sink (a plain +server.ts doing request.formData()), and reports what arrived. With a 10-byte file:

route arrived as
append(name, file) 10 bytes, intact
append(name, file, file.name) 15 bytes, [object Object]
control: real File, append(name, real, name) 10 bytes, intact

node fixes.mjs in the same repo reproduces the mechanism without SvelteKit or a server.

Logs

Nothing is logged with the failure currently. The file that gets upload just becomes `"[object Object]"`.

System Info

System:
    OS: macOS 26.6.2
    CPU: (12) arm64 Apple M2 Max
    Memory: 764.33 MB / 32.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.23.2 - /etc/profiles/per-user/thomaseckert/bin/node
    Yarn: 1.22.22 - /etc/profiles/per-user/thomaseckert/bin/yarn
    npm: 10.9.8 - /etc/profiles/per-user/thomaseckert/bin/npm
    pnpm: 11.25.0 - /etc/profiles/per-user/thomaseckert/bin/pnpm
    bun: 1.4.2 - /etc/profiles/per-user/thomaseckert/bin/bun
    Deno: 2.9.6 - /etc/profiles/per-user/thomaseckert/bin/deno
  Browsers:
    Chrome: 154.0.8037.93
    Safari: 26.6.2
  npmPackages:
    @sveltejs/adapter-node: ^6.0.0 => 6.0.0
    @sveltejs/kit: 3.0.0 => 3.0.0
    @sveltejs/vite-plugin-svelte: ^7.2.0 => 7.3.0
    svelte: ^5.57.1 => 5.57.1
    vite: ^8.1.5 => 8.3.0

Severity

serious, but I can work around it

Additional Information

Where it surfaced

I was working on a file upload form for a writing portfolio. The service appeared to be working, but all of the files just became "[object Object]".

Proposed fix

Give LazyFile a Symbol.toPrimitive that throws a TypeError naming the cause. All three silent paths fail by string coercion, so all three become loud, while the two-argument append stays lazy and validators still see a File. The commit above adds it with a test. On 3.0.0 the test fails without the change, and with it the kit unit suite passes.

This closes the failure mode, not the underlying mismatch: LazyFile still claims to be a File without being a Blob. Two alternatives were measured and are written up in the README:

Other Runtimes

The two-argument path is only sound on Node. On Bun it is also silently corrupted, and on Deno a LazyFile cannot enter a FormData at all. Table in the README.

Prior Art

#15018 is why the proxy exists (z.file() rejected a bare LazyFile). #16116 and #17147 are adjacent LazyFile issues, and neither covers this.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions