Skip to content

fix(gaxios): enforce maxContentLength on headerless responses in Bun - #9773

Open
danieljbruce wants to merge 3 commits into
mainfrom
fix/gaxios-bun-max-content-length
Open

danieljbruce wants to merge 3 commits into
mainfrom
fix/gaxios-bun-max-content-length

Conversation

@danieljbruce

@danieljbruce danieljbruce commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Description

Enforces maxContentLength on headerless buffered responses under Bun.

Impact

Prevents oversized buffered responses from bypassing maxContentLength limits on Bun.

Changes

  • Enforce maxSize (rawInit.size / rawInit.maxContentLength) inside readBuffer() in Gaxios.#createBunFetch() so buffered (json, text, arraybuffer, blob) responses reject with content size at <url> over limit: <maxSize> when exceeded even without a Content-Length header.

Stack

  1. fix(gaxios): enforce maxContentLength on headerless responses in Bun #9773 — fix(gaxios): enforce maxContentLength on headerless responses in Bun (fix/gaxios-bun-max-content-length → main) (this PR)
  2. fix(gaxios): enforce maxContentLength on stream responses in Bun #9777 — fix(gaxios): enforce maxContentLength on stream responses in Bun (fix/gaxios-bun-max-content-length-stream → fix/gaxios-bun-max-content-length)
  3. fix(gaxios): honor maxContentLength and maxRedirects instance defaults #9778 — fix(gaxios): honor maxContentLength and maxRedirects instance defaults (fix/gaxios-instance-defaults → fix/gaxios-bun-max-content-length-stream)
  4. fix(gaxios): forward https.Agent TLS options and agent factories in Bun #9774 — fix(gaxios): forward https.Agent TLS options and agent factories in Bun (fix/gaxios-bun-agent-tls → fix/gaxios-instance-defaults)

Testing

  • Added unit tests in core/packages/gaxios/test/test.getch.ts covering maxContentLength rejection on headerless responses across json, text, arraybuffer, and blob response types.
  • Steps to Reproduce (in terminal): Run cd core/packages/gaxios && pnpm run compile && JS_RUNTIME=bun MOCHA_PARALLEL=false bun --bun ../../../bin/run-test.cjs --no-c8 build/esm/test/test.getch.js -g "maxContentLength".

Alternatives

  • Checking maxContentLength only when a Content-Length header is present in getResponseData() was rejected because chunked/headerless responses silently bypass response size limits under Bun.
  • Not merging this PR was rejected because node-fetch enforces size on headerless responses on Node.js.

@danieljbruce

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a stream-limiting mechanism in Gaxios to enforce maxContentLength on responses that lack a Content-Length header. It introduces a Transform stream limiter that tracks received bytes and destroys the stream if the limit is exceeded. Additionally, it fixes a bug where instance options for maxContentLength and maxRedirects were not correctly mapped. The review feedback highlights that the newly added tests might not actually exercise the stream-limiting logic for headerless responses because nock automatically includes a Content-Length header by default. It is recommended to explicitly set the transfer-encoding header to chunked in the test mocks to ensure the stream limiter is properly tested.

'arraybuffer',
'blob',
] as const) {
const scope = nock(url).get('/').reply(200, body);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

By default, nock automatically calculates and includes a Content-Length header when replying with a body. This means the test might be passing due to the pre-existing Content-Length validation rather than actually exercising the new stream-limiting logic for headerless responses.

To ensure the response is truly headerless and the stream limiter is exercised, explicitly set the transfer-encoding header to chunked in the nock reply.

      const scope = nock(url)
        .get('/')
        .reply(200, body, {'transfer-encoding': 'chunked'});

Comment thread core/packages/gaxios/test/test.getch.ts Outdated
scope.done();
}

const defaultScope = nock(url).get('/').reply(200, body);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the test cases above, nock will automatically include a Content-Length header here. To ensure this test case actually exercises the stream limiter for instance defaults on headerless responses, set the transfer-encoding header to chunked.

    const defaultScope = nock(url)
      .get('/')
      .reply(200, body, {'transfer-encoding': 'chunked'});

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements enforcement of maxContentLength on responses without a Content-Length header by wrapping the response stream in a Transform stream that tracks chunk sizes and destroys the stream if the limit is exceeded. It also corrects the mapping of maxContentLength and maxRedirects from options to opts. The feedback suggests optimizing the readBuffer function by using a zero-copy Buffer view over Uint8Array chunks to avoid unnecessary memory allocations and copying overhead.

Comment thread core/packages/gaxios/src/gaxios.ts Outdated
Comment on lines 861 to 864
for await (const chunk of getNodeStream()) {
chunks.push(
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In Bun, the fetch response stream yields Uint8Array chunks rather than Node.js Buffer instances. Using Buffer.from(chunk as Uint8Array) copies the underlying memory of each chunk into a new Buffer allocation.

To avoid this unnecessary memory allocation and copying overhead, you can use Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength) to create a zero-copy Buffer view over the same underlying ArrayBuffer.

          for await (const chunk of getNodeStream()) {
            chunks.push(
              Buffer.isBuffer(chunk)
                ? chunk
                : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength),
            );
          }

Comment thread core/packages/gaxios/src/gaxios.ts Outdated
} from './common.js';
import {getRetryConfig} from './retry.js';
import {PassThrough, Readable, pipeline} from 'stream';
import {PassThrough, Readable, Transform, pipeline} from 'stream';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Import Transform from stream so #createBunFetch() can wrap the response stream in a byte-counting transform when maxContentLength (size) is configured.

Comment thread core/packages/gaxios/src/gaxios.ts Outdated
Comment on lines +481 to +482
if (typeof opts.maxContentLength === 'number') {
opts.size = opts.maxContentLength;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Read opts.maxContentLength from the merged opts object instead of raw options so new Gaxios({maxContentLength}) instance defaults populate opts.size.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this. For reference, opts just adds the defaults to options so I can't see any problem with this unless we would want to deliberately have opts.size be undefined for cases that it wasn't passed into the function.

Comment thread core/packages/gaxios/src/gaxios.ts Outdated
Comment on lines +485 to +486
if (typeof opts.maxRedirects === 'number') {
opts.follow = opts.maxRedirects;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Read opts.maxRedirects from the merged opts object instead of raw options so new Gaxios({maxRedirects}) instance defaults populate opts.follow.

Comment on lines +731 to +732
size?: number;
maxContentLength?: number;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Declare size and maxContentLength on rawInit in #createBunFetch() so the Bun fetch wrapper can read the configured byte limit.

}
throw err;
}
const maxSize = rawInit?.size ?? rawInit?.maxContentLength ?? 0;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Compute maxSize from rawInit.size or rawInit.maxContentLength (defaulting to 0 when unset) to determine whether response stream byte-limiting is needed.

Comment thread core/packages/gaxios/src/gaxios.ts Outdated
Comment on lines +825 to +862
const getNodeStream = () => {
// Lazily converts the Web ReadableStream response body into a Node.js
// Readable stream and wraps it in a byte-counting Transform when
// maxSize is set so both res.body and buffered body readers enforce
// maxContentLength.
if (!nodeStream) {
const webReadable = Readable.fromWeb(rawBody);
if (maxSize > 0) {
let totalBytes = 0;
const limiter = new Transform({
transform(chunk: Buffer | Uint8Array, _encoding, callback) {
totalBytes += chunk.byteLength;
if (totalBytes > maxSize) {
webReadable.destroy();
callback(
Object.assign(
new Error(
`content size at ${String((input as {url?: unknown})?.url ?? input)} over limit: ${maxSize}`,
),
{type: 'max-size'},
),
);
return;
}
callback(null, chunk);
},
});
webReadable.on('error', err => limiter.destroy(err));
limiter.once('close', () => {
if (!webReadable.destroyed) webReadable.destroy();
});
nodeStream = webReadable.pipe(limiter);
} else {
nodeStream = webReadable;
}
}
return nodeStream;
};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Define getNodeStream() to lazily convert the Web ReadableStream into a Node.js Readable and pipe it through a byte-counting Transform when maxSize > 0, immediately destroying the underlying stream and emitting content size at <url> over limit: <maxSize> (type: 'max-size') if exceeded.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really necessary to solve the issue in question or can we stack this change on top?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also out of date. We removed getNodeStream.

Comment thread core/packages/gaxios/src/gaxios.ts Outdated
const readBuffer = async () => {
const chunks: Buffer[] = [];
for await (const chunk of nodeStream!) {
for await (const chunk of getNodeStream()) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Consume getNodeStream() inside readBuffer() so buffered response readers (text(), json(), arrayBuffer(), blob()) pass through the maxSize stream limiter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of date

Comment thread core/packages/gaxios/src/gaxios.ts Outdated
get() {
nodeStream ||= Readable.fromWeb(rawBody);
return nodeStream;
return getNodeStream();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Return getNodeStream() from the res.body getter so responseType: 'stream' callers also receive the byte-limited Node.js Readable stream.

});
res.text = async () => {
if (!nodeStream) return origText();
if (!nodeStream && maxSize <= 0) return origText();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Route res.text() through readBuffer() whenever maxSize > 0 so headerless/chunked text responses enforce maxContentLength instead of bypassing the stream limiter via origText().

};
res.json = async () => {
if (!nodeStream) return origJson();
if (!nodeStream && maxSize <= 0) return origJson();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Route res.json() through res.text() / readBuffer() whenever maxSize > 0 so headerless/chunked JSON responses enforce maxContentLength.

};
res.arrayBuffer = async () => {
if (!nodeStream) return origArrayBuffer();
if (!nodeStream && maxSize <= 0) return origArrayBuffer();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Route res.arrayBuffer() through readBuffer() whenever maxSize > 0 so headerless/chunked binary responses enforce maxContentLength.

};
res.blob = async () => {
if (!nodeStream) return origBlob();
if (!nodeStream && maxSize <= 0) return origBlob();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Route res.blob() through readBuffer() whenever maxSize > 0 so headerless/chunked Blob responses enforce maxContentLength.

Comment thread core/packages/gaxios/test/test.getch.ts Outdated
Comment on lines +346 to +370
it('should enforce maxContentLength on responses without a Content-Length header and from instance defaults', async () => {
const body = {hello: 'world'};
for (const responseType of [
'json',
'text',
'arraybuffer',
'blob',
] as const) {
const scope = nock(url).get('/').reply(200, body);
await assert.rejects(
request({url, maxContentLength: 5, responseType}),
(err: Error) => err instanceof GaxiosError && /limit/.test(err.message),
);
scope.done();
}

const defaultScope = nock(url).get('/').reply(200, body);
const client = new Gaxios({maxContentLength: 5});
await assert.rejects(
client.request({url}),
(err: Error) => err instanceof GaxiosError && /limit/.test(err.message),
);
defaultScope.done();
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Add unit test verifying that maxContentLength rejects responses without a Content-Length header across json, text, arraybuffer, and blob response types, as well as when configured via new Gaxios({maxContentLength}) instance defaults.

Comment on lines +686 to +705
await assert.rejects(
request({url, maxContentLength: 2}),
(err: Error) =>
err instanceof GaxiosError && /over limit: 2/.test(err.message),
);

const streamMaxRes = await request<Readable>({
url,
maxContentLength: 2,
responseType: 'stream',
});
await assert.rejects(
(async () => {
for await (const chunk of streamMaxRes.data) {
assert.ok(chunk);
}
})(),
(err: Error) => /over limit: 2/.test(err.message),
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Add unit assertions in the Bun fetch test verifying that maxContentLength rejects oversized responses with over limit: 2 for both buffered JSON and responseType: 'stream'.

const origArrayBuffer = res.arrayBuffer.bind(res);
const origBlob = res.blob.bind(res);
const readBuffer = async () => {
nodeStream ||= Readable.fromWeb(rawBody);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Lazily initialize nodeStream from Readable.fromWeb(rawBody) inside readBuffer() so buffered readers (text(), json(), arrayBuffer(), blob()) can stream and measure the body even when res.body has not been accessed yet.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of date

Comment thread core/packages/gaxios/src/gaxios.ts Outdated
Comment on lines +828 to +842
let totalBytes = 0;
for await (const chunk of nodeStream) {
const buf = Buffer.isBuffer(chunk)
? chunk
: Buffer.from(chunk as Uint8Array);
totalBytes += buf.byteLength;
if (maxSize > 0 && totalBytes > maxSize) {
throw Object.assign(
new Error(
`content size at ${String((input as {url?: unknown})?.url ?? input)} over limit: ${maxSize}`,
),
{type: 'max-size'},
);
}
chunks.push(buf);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Track totalBytes while reading chunks in readBuffer() and throw content size at <url> over limit: <maxSize> (type: 'max-size') as soon as totalBytes > maxSize.

@danieljbruce danieljbruce Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Intent: Enforce maxContentLength (opts.size) on headerless/chunked buffered responses (json, text, arraybuffer, blob) in Bun so that both Node.js (V8) and Bun throw a max-size GaxiosError as soon as totalBytes > maxSize.

2x2 Matrix (totalBytes > maxContentLength without Content-Length header)

Without this change With this change
Node.js (V8) ❌ Throws error (GaxiosError: content size at <url> over limit: <maxSize>) ❌ Throws error (GaxiosError: content size at <url> over limit: <maxSize>)
Bun ✅ No error thrown (resolves 200 OK with the oversized payload) ❌ Throws error (GaxiosError: content size at <url> over limit: <maxSize>)

What happens if we don't throw the error for Bun?

  1. Silent limit bypass: When a response omits the Content-Length header (e.g., Transfer-Encoding: chunked), Gaxios.getResponseData()'s upfront header check does not run. Because Bun's native globalThis.fetch ignores the node-fetch size option, for await (const chunk of nodeStream) in readBuffer() continues reading every chunk to completion and resolves successfully with the oversized json / text / arraybuffer / blob data instead of rejecting.
  2. Unbounded memory buffering (OOM risk): All incoming chunks are accumulated in chunks: Buffer[] and concatenated via Buffer.concat(chunks) with no upper bound, allowing a large or runaway response stream to exhaust memory even when the caller explicitly configured maxContentLength.
  3. Runtime divergence & test failures: Callers expecting maxContentLength to reject oversized responses get a GaxiosError (type: 'max-size') on Node.js (V8) but a resolved response on Bun, causing the headerless maxContentLength tests in core/packages/gaxios/test/test.getch.ts to fail under Bun.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see how #2 and #3 is a problem. Can you explain why "resolves successfully with the oversized json / text / arraybuffer / blob data instead of rejecting" in #1 is a problem?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Good catch — in the list above, #1 was phrased as the mechanism (how the check gets bypassed when Content-Length is omitted), while #2 and #3 were the main consequences.

That said, even when an oversized response fits comfortably in memory (so #2 OOM doesn't happen) and setting aside #3 (Node vs. Bun divergence), resolving 200 OK with the oversized json / text / arraybuffer / blob payload instead of rejecting causes two concrete issues for callers:

  1. Unnecessary synchronous CPU / event-loop blocking (especially for json): In Gaxios.getResponseData(), case 'json' awaits res.text() and then runs JSON.parse(data) synchronously. If a caller sets maxContentLength: 10_000 (10 KB) and a server streams a 50 MB chunked JSON response, resolving readBuffer() means the runtime UTF-8 decodes and synchronously JSON.parse()s all 50 MB on the main thread instead of aborting as soon as the 10 KB threshold is crossed.
  2. Broken size invariant for downstream consumers: Callers use maxContentLength as a boundary guardrail so res.data is guaranteed to be <= maxContentLength before passing it to downstream systems with strict payload caps (e.g., database columns, Redis/cache entries, Pub/Sub or Cloud Tasks messages) or when fetching from untrusted endpoints that use Transfer-Encoding: chunked. Resolving successfully hands oversized data to application code that assumed the size limit was enforced at the HTTP layer.

Comment on lines +346 to +362
it('should enforce maxContentLength on responses without a Content-Length header', async () => {
const body = {hello: 'world'};
for (const responseType of [
'json',
'text',
'arraybuffer',
'blob',
] as const) {
const scope = nock(url).get('/').reply(200, body);
await assert.rejects(
request({url, maxContentLength: 5, responseType}),
(err: Error) => err instanceof GaxiosError && /limit/.test(err.message),
);
scope.done();
}
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Add unit test verifying that maxContentLength rejects responses without a Content-Length header across json, text, arraybuffer, and blob response types.

const origArrayBuffer = res.arrayBuffer.bind(res);
const origBlob = res.blob.bind(res);
const readBuffer = async () => {
nodeStream ||= Readable.fromWeb(rawBody);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a genuine bug. I notice that nodeStream wasn't even assigned before!

@danieljbruce
danieljbruce added this pull request to stack #9782 October 29, 2026 20:51
@danieljbruce

danieljbruce commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

[Generated] Intent: Enforce maxContentLength (opts.size) on headerless/chunked buffered responses (json, text, arraybuffer, blob) in Bun so that both Node.js (V8) and Bun throw a max-size GaxiosError as soon as totalBytes > maxSize.

2x2 Matrix (totalBytes > maxContentLength without Content-Length header)

Without this change With this change
Node.js (V8) ❌ Throws error (GaxiosError: content size at <url> over limit: <maxSize>) ❌ Throws error (GaxiosError: content size at <url> over limit: <maxSize>)
Bun ✅ No error thrown (resolves 200 OK with the oversized payload) ❌ Throws error (GaxiosError: content size at <url> over limit: <maxSize>)

What happens if we don't throw the error for Bun?

  1. Silent limit bypass: When a response omits the Content-Length header (e.g., Transfer-Encoding: chunked), Gaxios.getResponseData()'s upfront header check does not run. Because Bun's native globalThis.fetch ignores the node-fetch size option, for await (const chunk of nodeStream) in readBuffer() continues reading every chunk to completion and resolves successfully with the oversized json / text / arraybuffer / blob data instead of rejecting.
  2. Unbounded memory buffering (OOM risk): All incoming chunks are accumulated in chunks: Buffer[] and concatenated via Buffer.concat(chunks) with no upper bound, allowing a large or runaway response stream to exhaust memory even when the caller explicitly configured maxContentLength.
  3. Runtime divergence & test failures: Callers expecting maxContentLength to reject oversized responses get a GaxiosError (type: 'max-size') on Node.js (V8) but a resolved response on Bun, causing the headerless maxContentLength tests in core/packages/gaxios/test/test.getch.ts to fail under Bun.

@danieljbruce
danieljbruce marked this pull request as ready for review October 30, 2026 14:06
@danieljbruce
danieljbruce requested a review from a team as a code owner October 30, 2026 14:06
@github-actions
github-actions Bot requested a review from shivanee-p October 30, 2026 14:06
const origArrayBuffer = res.arrayBuffer.bind(res);
const origBlob = res.blob.bind(res);
const readBuffer = async () => {
nodeStream ||= Readable.fromWeb(rawBody);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: your comment on why nodeStream wasn't assigned in readBuffer() previously: in main, nodeStream was only initialized inside the res.body getter because res.text(), res.json(), res.arrayBuffer(), and res.blob() all started with if (!nodeStream) return origText(). That meant readBuffer() was previously only reachable if the caller had already accessed res.body (which locked rawBody via Readable.fromWeb(rawBody)).

Now that those guards check if (!nodeStream && maxSize <= 0), readBuffer() runs whenever maxSize > 0 even if res.body was never touched, so lazily initializing nodeStream ||= Readable.fromWeb(rawBody) here is required.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good to know. Thank you!

Comment thread core/packages/gaxios/src/gaxios.ts Outdated
Comment on lines +830 to +842
const buf = Buffer.isBuffer(chunk)
? chunk
: Buffer.from(chunk as Uint8Array);
totalBytes += buf.byteLength;
if (maxSize > 0 && totalBytes > maxSize) {
throw Object.assign(
new Error(
`content size at ${String((input as {url?: unknown})?.url ?? input)} over limit: ${maxSize}`,
),
{type: 'max-size'},
);
}
chunks.push(buf);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (non-blocking, and I see the byte-counting part of this loop gets replaced upstack in #9777 by getNodeStream()'s Transform limiter): Buffer.from(chunk as Uint8Array) allocates and copies the Uint8Array's memory before checking totalBytes > maxSize (and before Buffer.concat copies all chunks into the final buffer).

Checking chunk.byteLength before converting avoids copying an oversized chunk that is about to throw, and in both Node.js and Bun Buffer.concat(chunks) natively accepts ReadonlyArray<Uint8Array> (or you can wrap Uint8Array chunks with a zero-copy view Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength)) to avoid the extra copy per chunk.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Probably worth addressing here since it seems to be a small change and improves the code immediately. We might only merge the rest of the stack if it is highest in priority.

'arraybuffer',
'blob',
] as const) {
const scope = nock(url).get('/').reply(200, body);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note on the gemini-code-assist comment here: nock does not add a Content-Length header unless .replyContentLength() or an explicit 'content-length' header is specified (by default .reply(200, body) only sets {'content-type': 'application/json'}), so this test already exercises the headerless response path as written.

@danieljbruce danieljbruce Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Generated] Thank you for pointing that out! To make it unambiguous in the test itself that the headerless stream byte-counting path is exercised rather than the upfront Content-Length header check ("Response's Content-Length is over the limit."), I tightened the assertion in eb5273f to match /over limit: 5/.test(err.message) and added a brief comment noting nock's default .reply(200, body) behavior.

@danieljbruce
danieljbruce force-pushed the fix/gaxios-bun-max-content-length branch from 022a425 to eb5273f Compare October 30, 2026 21:02

This branch has not been deployed

No deployments
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.

2 participants