Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Response body ends prematurely #803

Closed
ronag opened this issue May 11, 2021 · 34 comments
Closed

Response body ends prematurely #803

ronag opened this issue May 11, 2021 · 34 comments
Labels
bug Something isn't working Status: help-wanted This issue/pr is open for contributions
Milestone

Comments

@ronag
Copy link
Member

ronag commented May 11, 2021

Haven't had time to check but I suspect we might have a problem with body sizes larger than 31 bit.

undici will complete without error before reading entire response body.

@ronag ronag added the bug Something isn't working label May 11, 2021
@ronag
Copy link
Member Author

ronag commented May 11, 2021

@dnlup would you mind taking a look and maybe making a test?

@ronag ronag added this to the 4.0 milestone May 11, 2021
@mcollina
Copy link
Member

How does Node.js handle this case?

@ronag
Copy link
Member Author

ronag commented May 11, 2021

It just works. This might be a wasm thing.

@dnlup
Copy link
Contributor

dnlup commented May 11, 2021

Are we talking about something greater than Number.MAX_SAFE_INTEGER?

@ronag
Copy link
Member Author

ronag commented May 11, 2021

Are we talking about something greater than Number.MAX_SAFE_INTEGER?

No, I have a case where I'm unable to receive anything larger than ~2^31. I'm not sure if it's undici at the moment but I think it's good idea to have a test where we transfer more than 2^32 bytes.

@ronag
Copy link
Member Author

ronag commented May 11, 2021

Could also be a problem with Node 16 http1/http2 server. I'm seeing the issue in a reverse proxy in production.

@dnlup
Copy link
Contributor

dnlup commented May 11, 2021

Inspecting llhttp I see content-length is an unsigned int of 64 bits. That should exclude some issues in the parser native side.

@dnlup
Copy link
Contributor

dnlup commented May 11, 2021

I think the problem is with Node. It happens only if you pass the entire buffer to res.write. Undici errors with message The other side closed.

I used Node 16 to test.

@ronag ronag closed this as completed May 11, 2021
@ronag ronag reopened this May 11, 2021
@ronag
Copy link
Member Author

ronag commented May 11, 2021

Something is weird. undici always prematurely ends response at 1605405800 bytes without error. While curl properly reads all 5 GB.

@ronag
Copy link
Member Author

ronag commented May 11, 2021

@mcollina @dnlup

This fails: #805

@ronag
Copy link
Member Author

ronag commented May 11, 2021

@mcollina we should probably fix this before release.

@ronag
Copy link
Member Author

ronag commented May 11, 2021

@indutny How come llhttp ends message without error before outputting content-length number of bytes?

@ronag
Copy link
Member Author

ronag commented May 11, 2021

going back to undici@3 resolves the issue.

@ronag ronag changed the title broken for content-length > 31 bits? Response body ends prematurely May 11, 2021
@ronag ronag added the Status: help-wanted This issue/pr is open for contributions label May 11, 2021
@ronag
Copy link
Member Author

ronag commented May 11, 2021

Currently I'm at a loss on cause and fix for this.

@indutny
Copy link
Member

indutny commented May 12, 2021

Isn't wasm memory space limited to 2gb? I think you need to split up the data before passing it into llhttp.

Correction: 4gb

@ronag
Copy link
Member Author

ronag commented May 12, 2021

The data is split? See the test.

@indutny
Copy link
Member

indutny commented May 12, 2021

Looking. Completely unrelated, you should probably use parseInt(..., 10) instead of just parseInt() (Yes, JS is weird: parseInt('0x1f') === 0x1f)

@indutny
Copy link
Member

indutny commented May 12, 2021

llhttp itself appears to work fine: https://gist.github.com/indutny/818e4428eeadb17bb541a668e653e2a9 . Whenever I execute it - I get back exactly the same amount of bytes that passed to execute.

I strongly suspect that it is either an issue with wasm build or the wrapping around it.

@ronag
Copy link
Member Author

ronag commented May 12, 2021

@indutny Thanks for confirming. Much appreciated.

@ronag
Copy link
Member Author

ronag commented May 12, 2021

@indutny I ported your test into our WebASsembly and was able to reproduce the issue: https://gist.github.com/ronag/3e316e78931f8b49f1138460a77a258a

@indutny
Copy link
Member

indutny commented May 12, 2021

Added a bunch of logging and looks like uint64_t subtraction doesn't work:

parser.content_length=0x100028c68 on_body_len=65536 read_total=1605238784
parser.content_length=0x100018c68 on_body_len=65536 read_total=1605304320
parser.content_length=0x100008c68 on_body_len=65536 read_total=1605369856
parser.content_length=0x0 on_body_len=35944 read_total=1605405800

As you might have guessed 35944 is 0x8c68 in hex. So it subtracts 0x8c68 from 0x100008c68 and gets 0x0.

@indutny
Copy link
Member

indutny commented May 12, 2021

FWIW, here's the code that I've added to wasm_on_body to extract this:

      const _cl = new Uint32Array(llhttp.exports.memory.buffer, p + 8 * 4, 2);
      const _remaining = _cl[1] * 0x100000000 + _cl[0];
      console.log('parser.content_length=0x%s on_body_len=%d read_total=%d', _remaining.toString(16), len, _total += len);

@indutny
Copy link
Member

indutny commented May 12, 2021

What's the best way to view the contents of the wasm file?

@indutny
Copy link
Member

indutny commented May 12, 2021

Sounds like wasm2wat

@ronag
Copy link
Member Author

ronag commented May 12, 2021

What's the best way to view the contents of the wasm file?

Maybe https://webassembly.studio?

@indutny
Copy link
Member

indutny commented May 12, 2021

I know what's going on. Could you find s_n_llhttp__internal__n_consume_content_length replace size_ts with uint64_ts right after it and try recompiling and running the test?

@ronag
Copy link
Member Author

ronag commented May 12, 2021

That fixed it!

@indutny
Copy link
Member

indutny commented May 12, 2021

Alright, working on the real fix. Thanks for trying it.

indutny added a commit to nodejs/llparse that referenced this issue May 12, 2021
On 32bit platforms `size_t` is essentially `uint32_t` (or at times
even meager `uint16_t`). Loading `uint64_t` field value into `size_t` on
these platforms would truncate the high bits and leave only the low 32
(16) bits in place. This leads to various interesting errors in
downstream modules. See:

- nodejs/llhttp#110
- nodejs/undici#803
@indutny
Copy link
Member

indutny commented May 12, 2021

Fix: nodejs/llparse#44 .

@ronag could you try llhttp.c from the attachment and see if it still works?

llhttp.c.zip

@indutny
Copy link
Member

indutny commented May 12, 2021

FWIW, this is the file that I intend to release once llparse fix lands. Feel free to ship the wasm version built from this file (after reviewing the diff) until we'll get it pushed upstream!

indutny added a commit to nodejs/llparse that referenced this issue May 12, 2021
On 32bit platforms `size_t` is essentially `uint32_t` (or at times
even meager `uint16_t`). Loading `uint64_t` field value into `size_t` on
these platforms would truncate the high bits and leave only the low 32
(16) bits in place. This leads to various interesting errors in
downstream modules. See:

- nodejs/llhttp#110
- nodejs/undici#803

This patch makes all field loads go into their respective types.
Truncation doesn't happen in this case because C coercion rules will
cast both types to the largest necessary datatype to hold either of
them.
@ronag
Copy link
Member Author

ronag commented May 12, 2021

Fix: nodejs/llparse#44 .

@ronag could you try llhttp.c from the attachment and see if it still works?

llhttp.c.zip

All good!

@dnlup
Copy link
Contributor

dnlup commented May 12, 2021

Thank you for the help @indutny !

@indutny
Copy link
Member

indutny commented May 12, 2021 via email

@ronag ronag closed this as completed May 12, 2021
indutny added a commit to nodejs/llparse that referenced this issue May 12, 2021
On 32bit platforms `size_t` is essentially `uint32_t` (or at times
even meager `uint16_t`). Loading `uint64_t` field value into `size_t` on
these platforms would truncate the high bits and leave only the low 32
(16) bits in place. This leads to various interesting errors in
downstream modules. See:

- nodejs/llhttp#110
- nodejs/undici#803

This patch makes all field loads go into their respective types.
Truncation doesn't happen in this case because C coercion rules will
cast both types to the largest necessary datatype to hold either of
them.

PR-URL: #44
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Daniele Belardi <[email protected]>
Reviewed-By: Robert Nagy <[email protected]>
Reviewed-By: Rich Trott <[email protected]>
Reviewed-By: Anna Henningsen <[email protected]>
@indutny
Copy link
Member

indutny commented May 12, 2021

Created a new release: https://github.com/nodejs/llhttp/releases/tag/release%2Fv6.0.2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working Status: help-wanted This issue/pr is open for contributions
Projects
None yet
Development

No branches or pull requests

4 participants