Skip to content

fix: use TextDecoder in FetchResponse.bodyText to prevent OOM on large responses - #5174

Open
yutonakamura-dev wants to merge 1 commit into
ethers-io:mainfrom
yutonakamura-dev:fix/fetch-body-oom
Open

fix: use TextDecoder in FetchResponse.bodyText to prevent OOM on large responses#5174
yutonakamura-dev wants to merge 1 commit into
ethers-io:mainfrom
yutonakamura-dev:fix/fetch-body-oom

Conversation

@yutonakamura-dev

Copy link
Copy Markdown

Summary

Fixes #5168

FetchResponse.bodyText decodes every response body through toUtf8String, which allocates one array element per byte and one string per code point. On large JSON-RPC responses (e.g. 18.7MB trace_block results), three concurrent calls exhaust a 1GB heap and crash the process.

Changes

Replaced toUtf8String(this.#body) with new TextDecoder("utf-8", { fatal: true }).decode(this.#body) in two places in src.ts/utils/fetch.ts:

  1. bodyText getter (line 813) — the main decode path for every HTTP response
  2. Error handler (line 946) — decodes body for error reporting

Removed the now-unused toUtf8String import from fetch.ts. The toUtf8String function itself and its error callback API remain unchanged for other callers.

Why TextDecoder

  • TextDecoder is a native API available in all supported environments (Node.js 12+, all modern browsers)
  • fatal: true throws TypeError on invalid UTF-8, preserving the existing error behavior
  • Zero intermediate allocations — decodes directly to string without building arrays

Reproduction & Results

Tested with 3 concurrent 18.7MB response bodies under a 256MB heap cap:

Before (toUtf8String) After (TextDecoder)
Result FATAL OOM crash Success
Time (single 18.7MB) 928ms 10ms
Heap (single 18.7MB) 794MB 47.6MB
3× concurrent, 256MB cap Crash 31ms, 62MB

Testing

  • All existing test-utils-utf8 tests pass (30 tests)
  • All existing test-utils-misc, test-utils-maths, test-utils-units tests pass (92 tests)
  • Verified edge cases: empty body, Unicode (emoji, CJK), 4-byte UTF-8, invalid UTF-8, null bytes, single byte
  • Verified TextDecoder output matches toUtf8String output for all valid UTF-8 inputs

…e responses

Fixes ethers-io#5168

`FetchResponse.bodyText` decoded every response body through `toUtf8String`,
which internally allocates one array element per byte and one string per code
point via `getUtf8CodePoints` + `_toUtf8String`. On large JSON-RPC responses
(e.g. 18.7MB `trace_block` results), three concurrent calls exhaust a 1GB heap
and crash the process.

Replaced `toUtf8String(this.#body)` with
`new TextDecoder("utf-8", { fatal: true }).decode(this.#body)` in both
`bodyText` getter and the error handler. `fatal: true` preserves the existing
behavior of throwing on invalid UTF-8 data.

The existing `toUtf8String` function and its error callback API remain
unchanged — this fix only affects the HTTP response decoding path.

Before: 18.7MB × 3 concurrent → OOM crash at 256MB heap (928ms, 794MB per decode)
After:  18.7MB × 3 concurrent → 31ms total, 62MB heap, no crash
@ricmoo

ricmoo commented Aug 11, 2026

Copy link
Copy Markdown
Member

Oh wow! I was unfamiliar with the fatal option. Thanks! I was planning a scanner that could perform the check (without constructing the arrays and strings) to run on the scoring before passing it to the decoder, but this seems better. I’ll try it out!

Thanks!

@yutonakamura-dev

Copy link
Copy Markdown
Author

It is still humble and just an idea. I am glad to hear that it was of some help.
Still a rough fix honestly — just the idea of leveraging the built-in fatal option instead of manual array.

Happy to adjust anything if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Large JSON-RPC responses OOM: toUtf8String allocates per byte

2 participants