diff --git a/.changeset/maticjs-disable-keepalive.md b/.changeset/maticjs-disable-keepalive.md new file mode 100644 index 00000000..b4bbcc95 --- /dev/null +++ b/.changeset/maticjs-disable-keepalive.md @@ -0,0 +1,7 @@ +--- +'@maticnetwork/maticjs': patch +--- + +Disable HTTP keep-alive on the network-config and ABI metadata fetches to fix intermittent `FetchError: Premature close` failures. + +Node 19+ enables HTTP keep-alive by default. node-fetch then reuses a connection the upstream CDN has already idle-closed, and the next gzip response fails mid-decompression — surfacing as the misleading `network - is not supported`. It reproduces 100% from datacenter/CI egress and intermittently elsewhere. These fetches run once per client init and are cached, so opening a fresh connection per request only costs a TLS handshake at startup. This complements the transient-retry added in #481, which could not recover when every reused socket was already stale. The browser build is unaffected (`window.fetch` ignores the agent). diff --git a/packages/maticjs/src/utils/http_request.ts b/packages/maticjs/src/utils/http_request.ts index dae867f7..1e94f5e0 100644 --- a/packages/maticjs/src/utils/http_request.ts +++ b/packages/maticjs/src/utils/http_request.ts @@ -1,12 +1,34 @@ import { retryTransient } from './retry'; -const fetch: (input: RequestInfo, init?: RequestInit) => Promise = (() => { +// node-fetch supports an `agent` option that the DOM lib's RequestInit lacks. +type FetchInit = RequestInit & { agent?: unknown }; + +const fetch: (input: RequestInfo, init?: FetchInit) => Promise = (() => { if (process.env.BUILD_ENV === 'node') { return require('node-fetch').default; } return window.fetch; })(); +// Force a non-keep-alive HTTP agent in the Node build. Node 19+ defaults http(s) +// agents to keepAlive:true; node-fetch then reuses a socket the upstream CDN +// (Cloudflare) has already idle-closed, and the next gzip response dies +// mid-decompression as `FetchError: Premature close` — observed 100% from CI, +// intermittently elsewhere. These network-config and ABI fetches run once per +// client init and are cached, so a fresh connection per request costs only a TLS +// handshake at startup. The browser build's `window.fetch` ignores `agent`, and +// node:http/https are required only inside the BUILD_ENV==='node' branch so they +// are never bundled for the browser. +const requestAgent: unknown = (() => { + if (process.env.BUILD_ENV === 'node') { + const httpAgent = new (require('http').Agent)({ keepAlive: false }); + const httpsAgent = new (require('https').Agent)({ keepAlive: false }); + return (parsedUrl: { protocol: string }) => + parsedUrl.protocol === 'http:' ? httpAgent : httpsAgent; + } + return undefined; +})(); + // Fail with the HTTP status / body on a non-2xx or non-JSON response instead of // letting a bare `res.json()` throw a context-free parse error. async function parseJsonResponse(res: Response, method: string, url: string): Promise { @@ -53,7 +75,8 @@ export class HttpRequest { headers: { 'Content-Type': 'application/json', Accept: 'application/json' - } + }, + agent: requestAgent }).then((res) => parseJsonResponse(res, 'GET', fullUrl)) ); } @@ -68,7 +91,8 @@ export class HttpRequest { 'Content-Type': 'application/json', Accept: 'application/json' }, - body: body ? JSON.stringify(body) : null + body: body ? JSON.stringify(body) : null, + agent: requestAgent }).then((res) => parseJsonResponse(res, 'POST', fullUrl)) ); } diff --git a/packages/maticjs/webpack.config.js b/packages/maticjs/webpack.config.js index 7516e0d6..97bca9e3 100644 --- a/packages/maticjs/webpack.config.js +++ b/packages/maticjs/webpack.config.js @@ -54,7 +54,12 @@ const clientConfig = { }, resolve: { modules: [path.resolve(__dirname, 'src'), 'node_modules'], - extensions: ['.json', '.js', '.ts', '.tsx'] + extensions: ['.json', '.js', '.ts', '.tsx'], + // `http`/`https` are referenced only in the Node build (guarded by + // BUILD_ENV==='node') to build a keep-alive-disabled agent. The web/UMD + // bundles never execute that branch, so map the modules to empty stubs + // rather than letting webpack fail trying to resolve Node core modules. + fallback: { http: false, https: false } }, plugins: [ new copyPlugin({