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

Web3.js cannot handle chunked transfer encodings #1418

Closed
NicolasPennie opened this issue Jul 21, 2023 · 26 comments · Fixed by #1447
Closed

Web3.js cannot handle chunked transfer encodings #1418

NicolasPennie opened this issue Jul 21, 2023 · 26 comments · Fixed by #1447
Labels
bug Something isn't working released

Comments

@NicolasPennie
Copy link

NicolasPennie commented Jul 21, 2023

Overview

Web3js throws an error when it processes a response from an RPC server with chunked transfer encoding: Invalid response body while trying to fetch [...]: Premature close. This happens because web3js is bundling a bad version of node-fetch. The bad version of node-fetch is 2.6.12 and later versions are also impacted. Version 2.6.7 does not have this problem.
Here is the bug in node-fetch: node-fetch/node-fetch#1219

It's critical to support chunked transfer encodings since its part of the HTTP/1.1 spec. This bug is very limiting for RPC providers such as ourselves (Helius) trying to make performance optimizations.

Web3js users impacted by this bug can workaround by installing version 2.6.7 and overriding the fetch setting of the Connection object:

    const fetch = require('node-fetch');
    const connection = new Connection(connectionString, {
        commitment: 'confirmed',
        fetch: (url, init) => fetch(url, { ...init, compress: true }),
    });

Steps to reproduce

  1. Create an RPC proxy that returns data with Transfer-Encoding: chunked. Alternatively you can use Helius RPCs, but we will soon be changing it due to this issue. You're welcome to create a key for free at https://dev.helius.xyz.
  2. Set web3js to 1.78.0 and run npm install.
  3. Create a standard connection object (new Connection('https://rpc.helius.xyz?api-key-here'))
  4. Make any RPC request.

Description of bug

All details provided in the overview.

@NicolasPennie NicolasPennie added the bug Something isn't working label Jul 21, 2023
@steveluscher
Copy link
Collaborator

Oof. This is awful.

  1. I think we're basically just going to drop support for any version of Node prior to 18 LTS, which would fix this problem. fetch is supported natively there.
  2. In general, we need to junk this transport and provide an HTTP/2 compatible transport to folks running web3.js in Node.

@NicolasPennie
Copy link
Author

NicolasPennie commented Jul 21, 2023 via email

@steveluscher
Copy link
Collaborator

I'm trying to remember why we upgraded, and I have a bad feeling it was because of a security advisory.

@steveluscher
Copy link
Collaborator

@steveluscher
Copy link
Collaborator

Related: #1255 (comment)

@NicolasPennie
Copy link
Author

I'm trying to remember why we upgraded, and I have a bad feeling it was because of a security advisory.

That's unfortunate. So what's the path forward? Will web3js be enforcing a Node 18+?

@steveluscher
Copy link
Collaborator

Isn't the solution for RPCs to do this?

From the release notes for node-fetch@2.6.8:

For responses using chunked Transfer-Encoding without a Content-Length, this adds a check at the socket's close event to verify that the last bytes received were the final chunk bytes, i.e. 0\r\n. If not, it creates a Premature close error and sends it to response.body.destroy()

cc/ @linuskendall

@NicolasPennie
Copy link
Author

Isn't the solution for RPCs to do this?

From the release notes for node-fetch@2.6.8:

For responses using chunked Transfer-Encoding without a Content-Length, this adds a check at the socket's close event to verify that the last bytes received were the final chunk bytes, i.e. 0\r\n. If not, it creates a Premature close error and sends it to response.body.destroy()

cc/ @linuskendall

We're looking into changes on our end to work around it, yes. But RPC providers shouldn't have to. Chunked transfer encodings are part of the HTTP 1.1 spec, which has been the standard for well over a decade now. This limits RPC providers from exploring certain latency optimizations.

@steveluscher
Copy link
Collaborator

But RPC providers shouldn't have to…

@linuskendall: Oh? Doesn't the spec require that the final chunk be zero length, followed by an empty line?

The chunked transfer coding is complete when a chunk with a chunk-size of zero is received, possibly followed by a trailer section, and finally terminated by an empty line.

https://datatracker.ietf.org/doc/html/rfc9112#chunked.encoding

@NicolasPennie
Copy link
Author

NicolasPennie commented Jul 25, 2023

@linuskendall: Oh? Doesn't the spec require that the final chunk be zero length, followed by an empty line?

Ah ok, I misread this. Let me see if we can make a change to support this on our end.

Why wouldn't this cause problems for other clients though?

@steveluscher
Copy link
Collaborator

Why wouldn't this cause problems for other clients though?

  1. They haven't updated to the spec-compliant version of node-fetch
  2. They don't use node-fetch

@NicolasPennie
Copy link
Author

NicolasPennie commented Jul 26, 2023

  1. They haven't updated to the spec-compliant version of node-fetch 2. They don't use node-fetch

What I mean is that Postman, rust clients, golang clients, native fetch, axios, and other clients all handle our responses without any issue.

It would surprise me that only node-fetch is spec-compliant.

@steveluscher
Copy link
Collaborator

When you get to be my age, it becomes very difficult to feign surprise at such things.

Anyway, the next version of web3.js absolutely will not use node-fetch, none the least of which because:

  1. It's a nightmare
  2. We're not going to support anything older than Node 18LTS
  3. Node 18+ supports fetch natively
  4. The next version of web3.js is going to support HTTP/2 from Node or die trying (and we'll probably backport it)

@linuskendall
Copy link

So, two things we are seeing:

  1. We are seeing the same problem as Helius

  2. The response headers I have been following are standards compliant..

Now standards compliant means very little on the web ... The workaround seems to be to disable chunked encoding for http/1.1? That doesnt feel great. After reviewing this I dont think node-fetch is doing the right thing either.

@steveluscher
Copy link
Collaborator

Response headers, @linuskendall? The problem isn’t with the headers, it’s with the response body. The body has to end with a zero-length chunk, followed by an empty line, before the socket close, or the close is considered premature.

@arhee
Copy link

arhee commented Jul 27, 2023

We're seeing the same issue. I was wondering if there was a timeline for a fix, so that we can implement a workaround if need be

steveluscher added a commit that referenced this issue Jul 27, 2023
# Summary

Folks are having trouble using the `node-fetch` polyfill with certain RPC providers that:

* use chunked-transfer encoding
* don't send a zero-sized chunk before closing the socket

`node-fetch` doesn't like this, and fatals.

One easy thing we can do while we figure this all out is to use the _native_ `fetch()` API included in Node 18+ by default and 17.5+ behind an experimental flag.

Fixes #1418 in Node 18+.

# Test Plan

CI run.
@steveluscher
Copy link
Collaborator

Who here is running their app in Node 18+? What are your thoughts on upgrading to #1447?

@linuskendall
Copy link

linuskendall commented Jul 28, 2023

Response headers, @linuskendall? The problem isn’t with the headers, it’s with the response body. The body has to end with a zero-length chunk, followed by an empty line, before the socket close, or the close is considered premature.

Yeah I know, I guess the point was "responses". Let me double check the responses again, but last I checked we were sending standards compliant chunks.

@linuskendall
Copy link

linuskendall commented Jul 28, 2023

So @steveluscher you previously reported this:

IF Content-Length is blank
  THEN IF chunked response ends with exactly 0\r\n
    ALL GOOD
  ELSE
    FATAL with premature close

This this is the current behaviour of node-fetch. The spec says:

The chunked transfer coding is complete when a chunk with a chunk-size of zero is received, possibly followed by a trailer section, and finally terminated by an empty line.

So there are two options. Either chunk-size 0 OR trailer section (your "0" I guess right?)) followed by \r\n

Now in our responses I see:

Screenshot 2023-07-28 at 19 10 26

In hex:
Screenshot 2023-07-28 at 19 20 48

What i am seeing is
30 0a 0d 0a 0d

Which is basically the very last chunk is a 0 length chunk (chunk-size 0) followed by \r\n. Exactly as per spec. It looks like the 0\r\n is the penulimate chunk so the total end response is : 0\r\n<empty chunk>\r\n. It looks like node-fetch is tripping up on this type of response however I believe it is standards compliant.

@linuskendall
Copy link

Why wouldn't this cause problems for other clients though?

  1. They haven't updated to the spec-compliant version of node-fetch
  2. They don't use node-fetch

I don't think node-fetch is spec compliant and I think they've got it wrong :)

@steveluscher
Copy link
Collaborator

Oooh. What's that tool, @linuskendall?

@linuskendall
Copy link

Oooh. What's that tool, @linuskendall?

It's capture traffic via tcpdump and illustrated in wireshark :) Wireshark is great for this type of thing.

@steveluscher
Copy link
Collaborator

@linuskendall, can you toss me a URL that produces one of these chunked responses so that I can poke and prod at it with node-fetch?

steveluscher added a commit that referenced this issue Jul 28, 2023
# Summary

Folks are having trouble using the `node-fetch` polyfill with certain RPC providers that:

* use chunked-transfer encoding
* don't send a zero-sized chunk before closing the socket

`node-fetch` doesn't like this, and fatals.

One easy thing we can do while we figure this all out is to use the _native_ `fetch()` API included in Node 18+ by default and 17.5+ behind an experimental flag.

Fixes #1418 in Node 18+.

# Test Plan

CI run.
@github-actions
Copy link
Contributor

🎉 This issue has been resolved in version 1.78.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@steveluscher
Copy link
Collaborator

@peroxy, @linuskendall, and I found the root of this problem: node-fetch/node-fetch#1767

In short, if you use node-fetch between v2.6.8 and v2.6.12, an HTTP agent in keepalive mode, and an RPC that returns chunked responses, you will hit this bug.

Solutions include:

@github-actions
Copy link
Contributor

Because there has been no activity on this issue for 7 days since it was closed, it has been automatically locked. Please open a new issue if it requires a follow up.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 11, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Something isn't working released
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants