The WHATWG Blog
Staged proposals at the WHATWG
April 28th, 2025
by
Domenic Denicola
The WHATWG's living standards incorporate
new features
on an ongoing basis. The default process is to propose an idea on the relevant standard's issue tracker, hash out the details and gather implementer interest, and then work together with the editors to land a pull request.
However, we've found that for larger features, or for community members less experienced with standards contributions, sometimes this lightweight process does not give enough guidance. It can be tricky to attract attention to one's proposal, or to get the clear signals from implementers that are necessary to land changes.
Inspired by other working groups, most notably the
TC39
group that defines the JavaScript language, the WHATWG community has established a new, optional
Stages process
for additions to WHATWG standards. Features can proceed from stage 0, where they just consist of a problem description, through to stage 4, when the feature has fully landed in the relevant living standard. Each stage requires increasing levels of community consensus, including from implementers and from the standard's editors. This gives the proposer, as well as the community, a better idea of a feature's progress towards being considered an accepted part of the web platform.
We've now had the Stages process for over a year. As of today, one proposal,
the
node.moveBefore()
atomic move operation
, has advanced to
stage 4
; one, for
the customizable



hidden=a%0D%0Ab%0D%0Ac%0D%0Ad
But although it might seem simple on the surface, newline normalization in form submissions is a topic that runs deeper than I thought, with bugs in the spec and differences across browsers. This post goes through what the spec used to do, what browsers (used to) implement, and how we went about fixing it.
First, some background on form submission
The data to submit from a form is modeled as an
entry list
– entries being pairs of names (strings) and values (either strings or
File
objects). This is a list rather than a map because a form can have multiple values for each name – which is is how

and



Here is the resulting
multipart/form-data
payload in Chrome and Safari (newlines are always CRLF):
------WebKitFormBoundaryjlUA0jn3NUYxIh2A
Content-Disposition: form-data; name="hidden a%0D%0Ab"

------WebKitFormBoundaryjlUA0jn3NUYxIh2A
Content-Disposition: form-data; name="file a%0D%0Ab"; filename="a%0Db"
Content-Type: application/octet-stream

------WebKitFormBoundaryjlUA0jn3NUYxIh2A--
And this is in Firefox 88 (the current stable version as of this writing):
-----------------------------26303884030012461673680556885
Content-Disposition: form-data; name="hidden a b"

-----------------------------26303884030012461673680556885
Content-Disposition: form-data; name="file a b"; filename="a b"
Content-Type: application/octet-stream

-----------------------------26303884030012461673680556885--
As you can see, Firefox substitutes a space for any newlines (CR, LF or CRLF) in the
multipart/form-data
encoding of entry names and filenames, rather than percent-encoding them as do Chrome and Safari. This behavior was made illegal in the spec in pull request
#6282
, but it couldn't be fixed in Firefox until the spec decided on a normalization behavior. In the case of values, Firefox normalizes to CRLF as the other browsers do.
As for Chrome and Safari, here we see that newlines in entry names and string values are normalized to CRLF, but filenames are not normalized. From the entry list construction algorithm as described above, this makes sense because entry values are only normalized to CRLF
when they are strings
– files are unchanged, and so are their filenames.
Except that, if you change the form's enctype in the above example to
application/x-www-form-urlencoded
, you get this in every browser:
hidden+a%0D%0Ab=a%0D%0Ab&file+a%0D%0Ab=a%0D%0Ab
Since
multipart/form-data
is the only enctype that allows file uploads, other enctypes use their filenames instead. But here it seems like every browser is CRLF-normalizing the filenames, even though in the spec that substitution happens long after constructing the entry list.
Normalizations with
FormData
and
fetch
The
FormData
class started out as a way to send
multipart/form-data
form payloads through the
XMLHttpRequest
and
fetch
APIs without having to generate that payload in JavaScript. As such,
FormData
instances are basically a JS-accessible wrapper over an entry list.
So let's try the same with
FormData
const formData = new FormData();
formData.append("hidden a\rb", "a\rb");
formData.append("file a\rb", new File([], "a\rb"));

// FormData objects in fetch will always be serialized as multipart/form-data.
await fetch("./post", { method: "POST", body: formData });
Safari sends the same form payload as above, with names and values normalized to CRLF, and so does Firefox 88 with values normalized to CRLF (and names and values having their newlines escaped as spaces). But Chrome keeps names, filenames and values unnormalized (here the
character stands for CR):
------WebKitFormBoundarySMGkMfD8mVOnmGDP
Content-Disposition: form-data; name="hidden a%0Db"

------WebKitFormBoundarySMGkMfD8mVOnmGDP
Content-Disposition: form-data; name="file a%0Db"; filename="a%0Db"
Content-Type: application/octet-stream

------WebKitFormBoundarySMGkMfD8mVOnmGDP--
Since
FormData
is just a wrapper over an entry list, and
fetch
simply calls the
multipart/form-data
encoding algorithm, no normalizations should take place. So it looks like Chrome was following the spec here, while Firefox and Safari were apparently doing some newline normalization (for Firefox, on string values only) at the time of serializing as
multipart/form-data
With
FormData
you can also investigate what the "construct the entry list" algorithm does, since if you pass a


element to the
FormData
constructor, it will call that algorithm outside of a form submission context, and let you inspect the resulting entry list.



So it seems like Firefox and Safari are not normalizing as they construct the entry list, and instead normalize names and values at the time that they encode the form into an enctype. In particular, since the
application/x-www-form-urlencoded
and
text/plain
enctypes don't allow file uploads, file entry values are substituted with their filenames
before
the normalization. Entry lists that aren't created from the "construct an entry list" algorithm get normalized all the same.
Chrome instead follows the specification (as it used to be) in normalizing in "construct an entry list" and not normalizing later, even for entry lists created through other means. But that doesn't explain why filenames in the
application/x-www-form-urlencoded
and
text/plain
enctypes are normalized. Does Chrome also have an additional normalization layer?
Investigating late normalization with the
formdata
event
It would be great to investigate in more detail what Chrome and other browsers do
after
constructing the entry list. Since the entry list construction already normalizes entries, any further normalizations that might happen further down the line are obscured in the common case.
In the case of
multipart/form-data
, we can test this because using a
FormData
object with
fetch
doesn't invoke "construct an entry list", and so can see what happens to unnormalized entries. For other enctypes there is no way to create an entry list that doesn't go through "construct an entry list", but as it turns out, the "construct an entry list" algorithm itself offers two ways to end up with unnormalized entries:
form-associated custom elements
(only implemented in Chrome so far) and the
formdata
event
(implemented in Chrome and Firefox). Here we'll only be covering the latter, since their results are equivalent.
One thing I skipped when I covered the "construct an entry list" algorithm above is that, at the end of the algorithm, after all entries corresponding to controls have been added to the entry list, a
formdata
event is fired on the relevant


element. This event has a
formData
attribute which allows you not only to inspect the entry list at that point, but to modify it.
id="form"
action="./post"
method="post"
enctype="application/x-www-form-urlencoded"


For both Chrome and Firefox (not Safari because it doesn't support the
formdata
event), trying this with the
application/x-www-form-urlencoded
enctype gets you a normalized result:
string+a%0D%0Ab=a%0D%0Ab&file+a%0D%0Ab=a%0D%0Ab
Firefox shows the same normalizations for the
text/plain
enctype; Chrome instead normalizes only filenames, not names and values. And with
multipart/form-data
we get the same result as with
fetch
and
FormData
above: Chrome doesn't normalize anything, Firefox normalizes string values (with names and filenames being replaced with spaces).
So in short:
For
application/x-www-form-urlencoded
, all browsers perform an additional newline normalization at the moment of serializing the form payload, whether or not they normalize when constructing the entry list. Note that newline normalizations are idempotent, so normalizing an already normalized string doesn't change it.
For
text/plain
, Firefox and Safari seem to act just like for
application/x-www-form-urlencoded
. Chrome instead only normalizes filenames, probably at the same time as files are being substituted with their filenames.
For
multipart/form-data
, Chrome doesn't normalize anything. Safari instead normalizes names and string values, but not filenames. Firefox does the same as Safari for values, but replaces any newlines in names and filenames with a space.
Remember that these differences across browsers don't really affect the encoding of normal forms, they only matter if you're using
FormData
with
fetch
, the
formdata
event, or form-associated custom elements.
Fixing the spec
So which behavior do we choose? Firefox replacing newlines in
multipart/form-data
names and values with a space is illegal as per
PR #6282
, but anything else is fair game.
For
text/plain
, we have Firefox and Safari behaving in the same way, and Chrome disagreeing. Since
text/plain
cannot represent inputs unambiguously, is little used in any case, and you would need either form-associated custom elements or to use the
formdata
event to see a difference, it seems extremely unlikely that there is web content that depends on either case. So it makes more sense to treat
text/plain
just like
application/x-www-form-urlencoded
and normalize names, filenames and values.
For
multipart/form-data
, there is the added compatibility risk that you can observe this case by using
FormData
and
fetch
, so it's more likely to cause webcompat issues, no matter whether we went with Safari's or Chrome's behavior. In the end we choose to go with Safari's, in order to be consistent at normalizing all enctypes – although the
multipart/form-data
has to be different in not normalizing filenames, of course.
So in pull request
#6287
we fixed this by:
Adding
a new algorithm
that runs before the
application/x-www-form-urlencoded
and
text/plain
serializers. This algorithm first extracts a filename from the entry value, if it's a file, and then CRLF-normalizes both the name and value.
We changed the
multipart/form-data
encoding algorithm
to have a first step that CRLF-normalizes names and string values, leaving file values intact.
Do we need the early normalization though?
At the time that we decided the above changes to the spec, I still thought that the spec's (and Chrome's) behavior of normalizing in "construct the entry list" was correct. But later on I realized that, once we have the late normalizations mentioned above, the early normalization in "construct the entry list" doesn't matter for form submission, since the late normalizations do everything the early one can do and more. The only way you could observe whether that early normalization is present or not is through the
FormData
constructor. So it would make sense to remove that early normalization and standardize on Firefox and Safari's behavior here, as we did in pull request
#6624
One kink remained, though: