Skip to content

fix: let fetch calculate request content length#195

Open
shrey150 wants to merge 2 commits into
mainfrom
shrey/core-2350-let-fetch-set-content-length
Open

fix: let fetch calculate request content length#195
shrey150 wants to merge 2 commits into
mainfrom
shrey/core-2350-let-fetch-set-content-length

Conversation

@shrey150

Copy link
Copy Markdown

Why

The SDK currently calculates Content-Length before handing a request to the configured Fetch implementation. That value becomes stale if Fetch middleware transforms the body afterward, and Node's native Fetch then rejects the request with UND_ERR_REQ_CONTENT_LENGTH_MISMATCH.

The ordinary session-create JSON path is byte-correct on its own. The compatibility problem is ownership: Fetch already calculates request framing from the final body, and current Stainless-generated TypeScript clients leave that work to the transport.

What changed

  • stop synthesizing content-length in the SDK request builder
  • preserve an explicitly caller-provided value
  • add request-builder and real HTTP wire coverage for native Fetch and the node-fetch fallback

Root cause

The length calculation itself predates the reported regression. SDK 2.14.1 changed modern Node runtimes from node-fetch to native globalThis.fetch; native Fetch/Undici strictly validates an explicit length against the final bytes. A body-transforming wrapper can therefore expose a stale header that the earlier transport path did not surface. Delegating the header to Fetch makes it calculate the value after those transformations.

E2E Test Matrix

Command / flow Observed output Confidence / sufficiency
node@22 <harness> local minify Middleware changed JSON from 217 to 159 bytes; the SDK supplied no length; native Fetch sent Content-Length: 159; server received 159 bytes; PASS. Reproduces the previously failing stale-header shape and proves the fix on Node 22/Undici 6.
node@24 <harness> local minify Middleware changed JSON from 217 to 159 bytes; the SDK supplied no length; native Fetch sent Content-Length: 159; server received 159 bytes; PASS. Proves the same regression path on Node 24/Undici 7.
node@22 <harness> local node-fetch and node@24 <harness> local node-fetch In both runs, node-fetch generated Content-Length: 217; server received 217 bytes; PASS. Covers the fallback transport used on older runtimes and when callers provide a Node HTTP agent.
node@22 jest tests/content-length.test.ts and node@24 jest tests/content-length.test.ts Three wire-level tests passed on each runtime: native Fetch, the node-fetch fallback, and body-transforming middleware. Keeps the transport behavior and the reproduced regression covered in the repository.
node@22 <local Stagehand 3.7 flow> and node@24 <local Stagehand 3.7 flow> Real Browserbase stagehand.init() created a session and close() released it on both runtimes; PASS. Exercises the exact downstream Stagehand initialization path with the local SDK build and real service.
node@22 <local SDK flow> and node@24 <local SDK flow> Real Browserbase session create and release succeeded on both runtimes; SDK supplied no explicit length; PASS. Exercises the changed SDK directly against the live API; no live resource identifiers are included.
node@20 jest --runInBand with the generated mock API 22 suites, 140 tests, and 2 snapshots passed. Full repository regression suite.
npm run build on Node 20 CommonJS and ESM builds completed with 0 TypeScript errors; both package import smoke checks passed. Proves the exact branch produces publishable artifacts in the repository's supported build environment.
npm run lint ESLint and TypeScript checks passed. Supporting static verification.

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.

1 participant