Skip to content

fix(client-fetch): [BUG] cancel inferred stream bodies#3831

Open
Stono wants to merge 2 commits intohey-api:mainfrom
Stono:fix/client-fetch-cancel-inferred-stream-bodies
Open

fix(client-fetch): [BUG] cancel inferred stream bodies#3831
Stono wants to merge 2 commits intohey-api:mainfrom
Stono:fix/client-fetch-cancel-inferred-stream-bodies

Conversation

@Stono
Copy link
Copy Markdown
Contributor

@Stono Stono commented Apr 27, 2026

Hey,
So I wrote a new app recently that pings a heartbeat every 5s, the endpoint is a 204/no-content, and generated the client for that app using the latest hey-api library.

The app was dying mysterious after a while, and then i remembered a similar issue we had ages ago where unconsumed fetch bodies eventually killed the app.

Looking into the code, it looks to be the same problem here, so this PR adds canceling of response bodies where no response content is expected.

Whats Changed
Updated the client-fetch runtime to cancel inferred stream bodies in auto mode:

  • In the zero-length fast path
  • In the general stream parse branch

Behaviour in explicit stream mode remains unchanged:

  • If parseAs is explicitly stream, the response body is still returned and not canceled.

Why

  • Prevents dangling/unconsumed response streams in the auto-inferred stream path.
  • Aligns runtime behaviour better with no-content and void-style endpoint expectations.
  • Preserves existing stream semantics for users who intentionally request stream responses.

@bolt-new-by-stackblitz
Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 27, 2026

@Stono is attempting to deploy a commit to the Hey API Team on Vercel.

A member of the Team first needs to authorize it.

@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Apr 27, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 27, 2026

⚠️ No Changeset found

Latest commit: 515291a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@dosubot dosubot Bot added the bug 🔥 Broken or incorrect behavior. label Apr 27, 2026
@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 27, 2026

TL;DR — When parseAs is 'auto' and getParseAs infers 'stream' (no Content-Type header), the client now cancels the response body instead of returning it. This prevents dangling unconsumed streams that can accumulate and eventually crash long-running applications hitting no-content endpoints.

Key changes

  • Cancel inferred stream bodies in client-fetch auto mode — In both the zero-length fast path and the general stream parse branch, response.body is now canceled and undefined is returned when the stream content type was auto-inferred rather than explicitly requested.
  • Add tests for inferred vs. explicit stream cancellation — Three targeted test cases: cancellation of auto-inferred streams (status 200), cancellation of auto-inferred streams for 204 responses, and verification that explicit parseAs: 'stream' requests still return the body untouched.

Summary | 2 files | 2 commits | base: mainfix/client-fetch-cancel-inferred-stream-bodies


Cancel inferred stream bodies to prevent resource leaks

Before: When a response had no Content-Type header, getParseAs fell through to 'stream' and the client returned response.body as data — even though the caller never asked for a stream. For endpoints like 204 heartbeats, this left unconsumed ReadableStream handles open indefinitely, eventually exhausting resources.
After: The 'stream' case in both the empty-body fast path (status 204 / Content-Length: 0) and the general parse branch now checks opts.parseAs === 'auto'. If the stream inference was automatic, it calls await response.body?.cancel() and returns undefined. Explicit parseAs: 'stream' requests are unaffected.

Why does an unconsumed stream cause problems?

The Fetch API specification requires response bodies to be consumed or explicitly canceled. An unconsumed body keeps the underlying TCP connection open in the connection pool and prevents garbage collection of associated buffers. In a long-running service making frequent requests (e.g. a 5-second heartbeat), these dangling streams accumulate until the process runs out of file descriptors or memory.

bundle/client.ts · client.test.ts

Pullfrog  | View workflow run | via Pullfrog𝕏

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Important

client-next and client-ky have the same case 'stream': pattern with getParseAs returning 'stream' for null Content-Type — they are susceptible to the same dangling-stream issue. Consider applying the same fix there (separate PR is fine).

Good fix overall. The opts.parseAs === 'auto' guard correctly distinguishes between inferred and explicit stream mode, and the response.body?.cancel() call properly releases the underlying connection. Two minor items below.

Task list (4/4 completed)
  • Read the PR diff and understand the changes
  • Read the PR description and linked issue for context
  • Investigate the changed source file in full context
  • Self-critique and submit review

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run𝕏

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

Labels

bug 🔥 Broken or incorrect behavior. size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant