Skip to content

feat(http/unstable): FormData Decoder/Encoder Streams#6928

Merged
kt3k merged 18 commits intodenoland:mainfrom
BlackAsLight:formdata
Jan 30, 2026
Merged

feat(http/unstable): FormData Decoder/Encoder Streams#6928
kt3k merged 18 commits intodenoland:mainfrom
BlackAsLight:formdata

Conversation

@BlackAsLight
Copy link
Contributor

@BlackAsLight BlackAsLight commented Jan 2, 2026

This pull request introduces a new package (@std/formdata), offering a way to process FormData content in a streaming manner. It is based off the RFC 7578 spec.

When a website submits a standard HTML form, it is sent using the multipart/form-data content type. Currently, servers must wait until the entire payload has been received before any processing can begin, which often forces them to impose strict size limits and rely on client-side JavaScript to handle larger uploads. This pull request addresses that limitation by enabling servers to process large uploads incrementally, without requiring JavaScript on the client, and even allowing it to be completely disabled.

Example

import { assertEquals } from "jsr:@std/assert";
import { FormDataDecoderStream } from "jsr:@std/formdata";

const request = new Request("https://example.com", {
  method: "POST",
  body: function() {
    const formData = new FormData();
    formData.append("a", "b");
    return formData;
  }()
});

for await (const entry of FormDataDecoderStream.from(request)) {
  assertEquals(entry.name, "a");
  assertEquals(await new Response(entry.value).text(), "b");
}

@tomas-zijdemans
Copy link
Contributor

Sorry, I'm quite inexperienced in deno std.

Looking at YAML parsing in the std it uses SyntaxError for malformed input. So instead of:

if (buffer == undefined) throw new Error("Unexpected EOF");

you could do:

if (buffer == undefined) throw new SyntaxError("Unexpected EOF");

...and so on.

Then, when the caller makes a mistake you could opt for TypeError. So instead of:

if (contentType == undefined) {
    throw new Error("Content-Type header is missing");
}

you could do:

if (contentType == undefined) {
   throw new TypeError("Content-Type header is missing");
}

...and so on

@BlackAsLight
Copy link
Contributor Author

This is ready to be reviewed btw. The only failing test is the pull request title

@kt3k
Copy link
Member

kt3k commented Jan 28, 2026

Thanks for the PR and sorry for the delay in review.

I'm not sure if this should be an independent package. Would it make sense to include it under std/http?

@BlackAsLight
Copy link
Contributor Author

I'm not sure if this should be an independent package. Would it make sense to include it under std/http?

Maybe. I can move it there if everyone thinks that's better

@tomas-zijdemans
Copy link
Contributor

I vote for placing it in http 👍

@github-actions github-actions bot added the http label Jan 29, 2026
@BlackAsLight BlackAsLight changed the title feat(formdata/unstable): new module feat(http/unstable): FormData Decoder/Encoder Streams Jan 29, 2026
Copy link
Member

@kt3k kt3k left a comment

Choose a reason for hiding this comment

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

LGTM!

@kt3k kt3k merged commit 73441a0 into denoland:main Jan 30, 2026
19 checks passed
@BlackAsLight BlackAsLight deleted the formdata branch January 30, 2026 07:40
@KnorpelSenf
Copy link
Contributor

@BlackAsLight is there a reason why you chose to only support streams but not iterators?

@BlackAsLight
Copy link
Contributor Author

@BlackAsLight is there a reason why you chose to only support streams but not iterators?

The preferred use case is via the .from method which returns a ReadableStream instead of an instance of itself. ReadableStreams are already iterators.

The multipart/form-data media type isn't the same as other file formats as you need information within the headers to correctly decode it. These streaming classes aren't TransformStreams like Tar and Cbor offers but instead meant to be the start or end of a streaming process.

/**
* The value of the form data entry.
*/
value: ReadableStream<Uint8Array>;
Copy link
Contributor

Choose a reason for hiding this comment

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

I meant that I would want to pass an (async) iterator here without first having to convert it to a stream

Copy link
Contributor Author

@BlackAsLight BlackAsLight Jan 31, 2026

Choose a reason for hiding this comment

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

Did you mean this line: https://github.com/denoland/std/pull/6928/changes/BASE..637c16fcb6c6b3058e2f323d474c351248959fe8#diff-9a727642d87d2fe68db223e18e8d935be3dd5a12031a1de01657571e9467a412R21

If so, simply for simplicity. One can do ReadableStream.from to convert an iterator or async iterator to a stream. Even if I changed the type, it would just move it to me wrapping it in a stream instead of you.

Asking for a stream specifically instead of an iterator also means I can take advantage of byob if it is available. Although if I'm being honest, the lack of support for a byob stream to continue being byob through a TransformStream is disappointing.

tomas-zijdemans pushed a commit to tomas-zijdemans/std_cb that referenced this pull request Feb 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants