Skip to content

Conversation

@ottomated
Copy link
Contributor

@ottomated ottomated commented Oct 21, 2025

Closes #14773

Implements a custom binary format used for uploading forms. Also:

  • removes sveltekit:foo form data keys, because now a meta object is transferred along with the form data. sveltekit:foo keys are only set when the form is enhanced anyway, so we already don't have them in the no-js case
  • All forms use this new format. This avoids complexity but maybe should be benchmarked.
  • Upload progress is pretty trivial to implement, but I think that should be a separate PR

Binary format specs:

  • 1 byte: Format version
  • 4 bytes: Length of the header (u32)
  • 2 bytes: Length of the file offset table (u16)
  • header: devalue.stringify([data, meta])
  • file offset table: JSON.stringify([offset1, offset2, ...]) (empty if no files) (offsets start from the end of the table)
  • file1, file2, ...

Example:

00 - version 0
99 00 00 00 - header is 99 bytes long
05 00 - file offset table is 5 bytes long
"[[1,15],{"file1":2,"file2":9},["File",3],[4,5,6,7,8],"bar.txt","text/plain",8,1761175817027,0,["File",10],[11,5,12,13,14],"foo.txt",4,1761175812093,1,{}]" - header (contains 2 files, bar.txt with index 0, foo.txt with index 1)
"[4,0]" - file offset table (means bar.txt starts 4 bytes after this, foo.txt starts 0 bytes after)
"foo" - contents of foo.txt
"bar bar" - contents of bar.txt

Files are sorted smallest to largest, so large files don't have to be streamed in their entirety before we can start reading a small file.

Still needs review:

  • Best practices on sending an XHR
  • Some hacky code around creating fake File objects that can be streamed in on the server side
  • Need to skip a test on node 18 because it doesn't have File support, but I don't know if we can mock that with undici or something
  • Probably want more tests? The feature's pretty complex
  • Is application/x-sveltekit-formdata a good content-type

Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests

  • Run the tests with pnpm test and lint the project with pnpm lint and pnpm check

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features should be minor and those that fix bugs should be patch. Please prefix changeset messages with feat:, fix:, or chore:.

Edits

  • Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.

@changeset-bot
Copy link

changeset-bot bot commented Oct 21, 2025

🦋 Changeset detected

Latest commit: 7cb1fcd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

{
getPrototypeOf() {
// Trick validators into thinking this is a normal File
return File.prototype;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Rich-Harris not sure the best way to do this part - we can't use actual File objects because that would require the data to already be buffered, and setting __proto__ = File.prototype causes private class member issues.

if (this.#stream) throw new TypeError('_setup_internal called twice');
let cursor = 0;
let chunk_index = 0;
this.#stream = new ReadableStream({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very complicated and kinda repeats code from get_buffer above, but I tested the performance and the overhead of making a ReadableStream if we just need a little buffer is pretty huge (especially for the common case).

@ottomated
Copy link
Contributor Author

@Rich-Harris you said

it's especially bad if you call myform.validate(), since the file data will get uploaded even though it's not necessary for validation.

on your issue - but is that true? Couldn't people do

v.pipe(
  v.file(),
  v.check(async file => {
    const text = await file.text();
    return text.startsWith("MAGIC");
  }, 'File header invalid')
)

@ottomated ottomated marked this pull request as ready for review October 22, 2025 23:58
@Rich-Harris
Copy link
Member

just want to say that i'm not ignoring this PR, i'm very excited about it, just absolutely crunched at the moment so haven't had a chance to take a look yet. very soon hopefully! thank you for opening it

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streaming file uploads

2 participants