Repository navigation
Headers are unnecessary encoded to ByteString if there is no Transfer-Encoding: chunked header #42579
Description
Activity
- addedhttpIssues and PRs related to the http subsystem.Issues and PRs related to the http subsystem.
on Apr 2, 2022 @nodejs/http
Hello.
I have triaged this issue.You are correct in saying the that
Transfer-Encodingheader make node behave differently: when TE is present, the headers are sent with the first chunk and the chunk encoding is binary.When TE is not present, the headers are sent with the data but there is no encoding specified and Node defaults to UTF-8.
Your file name, which you binary encoded to latin1, is then re-interpreted as utf-8 and that's why the double encoding happened.However, the problem is not in how Node processes the headers but in the value it self. No spec mandates (at least for what I could find) that non UTF-8 should be converted as latin1/binary and then the client should/might reinterpret them. My suggestion is to encode them using RFC8187 (which you already have in your code when
encoded=truebut somehow you don't do by default).I have filed a PR that explicit this in the docs.
Thanks for your report!For RFC8187:
Producers MUST use the "UTF-8" ([RFC3629]) character encoding.
@ShogunPanda we should use
encodeURIComponent&decodeURIComponentto deal with these Special characters, right?I see that PHP-based servers work this way — they encode headers (it fact, it's only needed for non-
ASCIIcharacters) asUTF-8bytes withinLatin1.It's not something new.
It works fine with a 2013 browser (The older browsers that I found.)Why I can't do the same with node.js?
To produce the same header, as it do PHP forum servers, like I do in my Java example above.
encoded=trueis just a workaround.However, it should work even without this.
And it will work if Node.js will not encode the headers itself if TE is missed.
It already expectsLatin1as a input string. A valid header value. But it additionally encodes it.When TE is not present, the headers are sent with the data but there is no encoding specified and Node defaults to UTF-8.
No spec mandates (at least for what I could find) that non UTF-8 should be converted as latin1/binary and then the client should/might reinterpret them.
So, it feels like a bug.
It encodes headers to UTF-8, while it's not needed.Clients expect headers as is — as
Latin1/binary(which may contain eitherUTF-8bytes, or some 8-bit encoding), but Node.js (w/o TE header) encodes them inUTF-8.Is it not a bug?
For RFC8187:
Producers MUST use the "UTF-8" ([RFC3629]) character encoding.
@ShogunPanda we should use
encodeURIComponent&decodeURIComponentto deal with these Special characters, right?Yes,
encodeURIComponenton server->client, and in theorydecodeURIComponentwhen receiving a client request.So, it feels like a bug. It encodes headers to UTF-8, while it's not needed.
Clients expect headers as is — as
Latin1/binary(which may contain eitherUTF-8bytes, or some 8-bit encoding), but Node.js (w/o TE header) encodes them inUTF-8.Here's the thing. The client expect ASCII only. Not binary. The big difference is that technically the RFC7230 only allows US-ASCII (so from 0 to 127) while we're using latin1 (0 to 255). The client might not understand the extension at all.
Putting latin1 encoded UTF-8 is arbitrary and needs agreement with the client.Is it not a bug?
I see that PHP-based servers work this way — they encode headers (it fact, it's only needed for non-ASCIIcharacters) asUTF-8bytes withinLatin1.It's not something new. It works fine with a 2013 browser (The older browsers that I found.)
In my opinion supporting that is a deviation from the spec.
Why I can't do the same with node.js? To produce the same header, as it do PHP forum servers, like I do in my Java example above.
However, it should work even without this. And it will work if Node.js will not encode the headers itself if TE is missed. It already expects
Latin1as a input string. A valid header value. But it additionally encodes it.That's another thing.
In node we have a optimization that sends headers along with the first part of the body (or the whole body). Only the headers are in latin1, body is not.
If we try to remove the optimization (and I tried when triaging this bug) we solve the issue but benchmarks show a nearly 100% decrease in speed, which we cannot afford for a edge case like this.Once again, I'd like to remark that RFC8187 is the newer standard available (2018), built on top of older RFCs. Consider this to be correct case and any other client support as hackish, since it's not regulated anywhere.
Reacted by 小菜 and Benjamin Gruenbaum- added a commit that references this issue
on Apr 25, 2022 - added a commit that references this issue
on May 31, 2022 - added a commit that references this issue
on Jun 27, 2022 - added a commit that references this issue
on Jul 11, 2022 - added a commit that references this issue
on Jul 31, 2022 - added a commit that references this issue
on Oct 10, 2022 - added a commit that references this issue
on Mar 21, 2024
Version
v17.5.0
Platform
Windows 10
Subsystem
http
What steps will reproduce the bug?
Run this
httpserver:Rock & roll 音楽 («🎵🎶»).txtname.Rock & roll 音楽 («🎵🎶»).txtname (in this case "Transfer-Encoding" header will be removed)How often does it reproduce? Is there a required condition?
Always.
What is the expected behavior?
Both files are downloaded with
Rock & roll 音楽 («🎵🎶»).txtname.What do you see instead?
The first file has the correct name —
Rock & roll 音楽 («🎵🎶»).txt.The second one has wrong name —
Rock & roll é_³æ¥½ («ð__µð__¶Â»).txtAdditional information
TL;DR
If there is
"Transfer-Encoding: chunked"header (exactlychunked)setHeaderworks properly, it sets the input header (ByteString) as is.(Note:
"Transfer-Encoding: chunked"is set by default.)In any other case it additionally (unnecessary) encodes the header to
ByteString.So, the header is encoded twice, that is wrong.
Additional info
The most of HTTP headers are contains only ASCII characters. But when you need to put in a header (For example,
"Content-Disposition", or any custom header) a string that contains non-ASCII* character(s), you can't just put it in as issetHeader.For example:
A HTTP header is a Binary String (
ByteString) —UTF-8bytes withinStringobject.*There is no problem with the headers which contain only ASCII characters, since ASCII charset is subset of
UTF-8andLatin 1encodings, sotoByteString(ASCIIString) === ASCIIString.To get a
ByteStringfromUSVStringyou just need to takeUTF-8bytes from an input string then represent them inLatin 1(ISO-8859-1) encoding.For example, in Node.js:
*To be honest, the entire quote of [
ByteString](https://webidl.spec.whatwg.org/#idl-ByteString:As I can see, a browser also can detect if the string is "just"
8859-1, notUTF-8bytes encoded in8859-1.So, both
"Content-Disposition"headers arevalid"valid"**:The result in
both"both"** cases is a file with"¡«£»÷ÿ.png"** name, even while"¡«£»÷ÿ.png" !== toByteString("¡«£»÷ÿ.png").UPDATE:
**Using non-UTF-8 bytes ("some other 8-bit-per-code-unit encoding") in
ByteStringis browser/OS language dependent!For example, in Firefox with non-EN language using of
"¡«£»÷ÿ.png"as is (withouttoByteString()) results to������.pngfilename, instead of¡«£»÷ÿ.pngIn Chrome it will be
Ў«Ј»чя.pngfor Cyrillic.So, I think it (using of
8859-1in "usual way") should be highly unrecommended.Headers should always be a
ByteStringwith only UTF-8 bytes represented as8859-1(Latin 1).Problem
The problem is that I can't correctly set a header that is a
ByteString(UTF-8bytes inLatin 1) if the original string contains non-ASCII characters.Like the other servers do it.
The problem appears only when the
Transfer-Encoding: chunkedheader (which is present by default) is removed (or changed).In this case
setHeaderencodes the header to Binary String.That is unnecessary, since it's already a
ByteString.It's not possible to put in
setHeaderaUSVString, since in this case it will throwTypeError [ERR_INVALID_CHAR]: Invalid character in header contenterror.So, the header is encoded to
"binary"twice, and browsers download the file with the wrong filenames:Rock & roll é_³æ¥½ («ð__µð__¶Â»).txtinstead ofRock & roll 音楽 («🎵🎶»).txt.You can open the demo server with disabled
Transfer-Encoding: chunkedheader (http://localhost:8000/?te=0 ) and check it:The header is encoded twice!
Examples
A lot of forums encodes headers such way for the attached files (XenForo, vBulletin, for example).
The real life examples:
Oh, wait, it requires an account, if you don't have/(want to create an account), just use my demo server.
Anyway, just look at the screenshots below.
In the browser console you can verify that header are
ByteString:As a bonus, here is an example of Java server made with
ServerSocketwhich also works properly:Main.java