Skip to content

fix(node,fetch,core): only send the standard-server header when content headers are ambiguous - #59

Closed
dinwwwh wants to merge 3 commits into
mainfrom
claude/http-adapter-standard-server-5cf6fd
Closed

fix(node,fetch,core): only send the standard-server header when content headers are ambiguous#59
dinwwwh wants to merge 3 commits into
mainfrom
claude/http-adapter-standard-server-5cf6fd

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Aug 12, 2026

Copy link
Copy Markdown
Member

File and blob bodies used to carry a standard-server: file header on every message, even when a receiver could already identify them from content-type/content-length alone. That header is now sent only when the content headers would lead somewhere else, so ordinary file responses go out without it.

New resolveStandardBodyHint(headers) in core resolves the hint a receiver lands on from the standard headers: the standard-server header when it holds a recognized value, otherwise the same content-type/content-length inference the body parsers do. The node and fetch adapters use it in their blob branch and set the header only when the headers don't already resolve to file.

Fixes

  • A file or blob with a length and an ordinary content-type no longer sends standard-server — one fewer non-standard header on every file response.
  • The header is still sent where the content headers are ambiguous: a common content-type (application/json, multipart/form-data, application/x-www-form-urlencoded, text/event-stream), a body with no content-type and no length, and a NaN-size blob (BunS3).
  • Streams keep the hint unconditionally. Their length is unknown when the headers are built, but the transport can still add a content-length — an empty stream goes out as content-length: 0 and would come back as a file.
  • The fastify client-server test no longer drops an empty content-type; it round-trips it through a placeholder so a typeless blob keeps its (valid, empty) content-type across the wire.

Notes for reviewers

  • An empty content-type stays a valid content-type everywhere: it is not normalized to "absent", matching what the parsers already do.
  • resolveStandardBodyHint ignores an unrecognized standard-server value and falls back to the content headers. The parsers instead treat any unknown hint as an opaque stream. That divergence is inert today — the senders only consult the resolver when no hint is set — but it needs a decision before this replaces the parsers' own if-chains.
  • Callers can still pre-set standard-server to override the auto-set hint, and the [] unset-header convention still suppresses it.

Testing

pnpm test (925 tests), typecheck and lint all pass. New core coverage for every branch of resolveStandardBodyHint, plus adapter cases for a common-content-type blob, an empty blob, and an empty blob with no content-type.

…nt headers are ambiguous

File and blob responses carried a `standard-server: file` header on every
request, even though a receiver can already identify them from
content-type/content-length alone.

Adds `resolveStandardBodyHint` to core, which resolves the body hint a
receiver lands on from the standard headers, and uses it in the node and
fetch adapters to skip the header when the headers already resolve to a
file. Streams keep the hint unconditionally: their length is unknown at
that point, but the transport can still add a content-length (an empty
stream), which would read back as a file.
@pkg-pr-new

pkg-pr-new Bot commented Aug 12, 2026

Copy link
Copy Markdown
@standardserver/aws-lambda

npm i https://pkg.pr.new/@standardserver/aws-lambda@59

@standardserver/bun

npm i https://pkg.pr.new/@standardserver/bun@59

@standardserver/core

npm i https://pkg.pr.new/@standardserver/core@59

@standardserver/deno

npm i https://pkg.pr.new/@standardserver/deno@59

@standardserver/fastify

npm i https://pkg.pr.new/@standardserver/fastify@59

@standardserver/fetch

npm i https://pkg.pr.new/@standardserver/fetch@59

@standardserver/node

npm i https://pkg.pr.new/@standardserver/node@59

@standardserver/peer

npm i https://pkg.pr.new/@standardserver/peer@59

@standardserver/shared

npm i https://pkg.pr.new/@standardserver/shared@59

commit: 2ef4ede

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 108 skipped benchmarks1


Comparing claude/http-adapter-standard-server-5cf6fd (2ef4ede) with main (6cc7812)2

Open in CodSpeed

Footnotes

  1. 108 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (fc2df69) during the generation of this report, so 6cc7812 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@pullfrog pullfrog 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.

ℹ️ Logic is sound and well tested — one stale doc line worth a touch-up.

Reviewed changes — PR #59 stops emitting standard-server: file on every file/blob body, sending it only when the content headers would otherwise lead a receiver to parse the body as something other than a file.

  • core resolveStandardBodyHint(headers) — new function mirroring the body parsers' inference: a recognized standard-server value wins, otherwise content-type/content-length determine the hint. Careful unit coverage of every branch (empty content-type, repeated headers, empty-array unset convention, NaN-size).
  • fetch/node blob sendcontent-length is now written only when Number.isFinite(body.size) (NaN-size bun-stream returns the stream without a bogus content-length), and standard-server: file is set only when resolveStandardBodyHint(headers) doesn't already resolve to file.
  • stream send — hint kept unconditionally, since an empty stream can still pick up content-length: 0 from the transport and read back as a file.
  • fastify client-server hook — empty content-type now round-trips via a custom/empty placeholder instead of being deleted, preserving the typeless-empty-blob case.

The conditional is safe: the resolver returns file exactly when a hint-less receiver would already parse the body as a file (content-length present and content-type not a common type), and returns something else exactly when the hint is required to force file parsing (common content-type, or no content-length). The removed-hint edge (empty + typeless blob, which now sends no header) is covered by the existing empty-file case in tests/data-transfer.test.ts, which round-trips new File([], '', { type: '' }) through every transport and asserts type === '' — confirming empty content-type survives the wire. NaN-size blobs correctly keep the hint (pinned in packages/node/src/body.test.ts:601 and the fetch equivalents).

ℹ️ Nitpicks

  • README.md:170 still says a file/blob body "automatically sets the ... standard-server header", but this PR makes it conditional. Worth rewording to note the header is only set when the content headers would otherwise mislead the receiver.

Pullfrog  | Fix it ➔View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

@pullfrog pullfrog 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.

✅ No new issues found. The new commit correctly closes the empty-body gap the initial change left open, with tests that would actually fail without the fix.

Reviewed changes — incremental delta since the prior pullfrog review (head aebc02117ada9c, "fix(fetch,node): keep binary bodies identifiable on bun"):

  • Restored empty-body safety net — both blob senders (toFetchBody, toNodeHttpBody) now emit standard-server: file whenever body.size === 0, on top of the existing "resolver says non-file" rule. This covers the empty+typeless-blob case that the initial change had stopped sending a hint for: bun drops an empty content-type and exposes a zero-length body as no body at all, so without the hint the receiver reads nothing.
  • Fetch receiver fixtoStandardBody now treats content-length: 0 with a null body as an explicit empty body, returning an empty File instead of undefined; and sources File.type from the content-type header rather than blob.type (which bun empties once .body has been touched).
  • Tests — new coverage pins the empty-file receive path (would fail without the contentLength !== '0' fix) and the empty/common-content-type/empty-typeless send paths.

I verified the delta end-to-end: the body.size === 0 short-circuit only adds a redundant hint where resolveStandardBodyHint already returns file (so receiver behavior is unchanged), and genuinely needed for the empty typeless case. The node receiver needs no equivalent change — node http doesn't null zero-length bodies and its file branch already handles content-length: 0. Locally re-ran the fetch/node/core body suites (114 tests), data-transfer.test.ts (176), and fastify response (16) — all pass.

Pullfrog  | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

@dinwwwh
dinwwwh force-pushed the claude/http-adapter-standard-server-5cf6fd branch from 17ada9c to 6572945 Compare August 12, 2026 09:44

@pullfrog pullfrog 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.

Important

The "improve" commit silently reverts the empty-file receive fix that the previous commit introduced (and the last Pullfrog review verified): the fetch receiver's File.type is sourced from re.blob() again instead of the content-type header, and the only test pinning the empty-file receive path was deleted. No rationale appears in the commit or PR body.

Reviewed changes — incremental delta since the prior pullfrog review (head 17ada9c6572945, "improve"):

  • fetch receiver empty-file reverttoStandardBody's file branch sources File.type from re.blob() again rather than the content-type header. The null-body early-return (hint === null && re.body === null && contentLength !== '0') was also removed; that part is fine, since senders always hint empty bodies and the line-22 guard covers typeless null bodies.
  • deleted empty file receive test — the fetch case pinning Response(null, { 'content-length': '0', 'content-type': 'application/pdf' }) → an empty File carrying the header's type was removed and not replaced, leaving the empty-file receive path untested again.
  • test:coverage — now runs pnpm run -r test before the coverage pass.
  • node/fetch send comments — empty-body blob-send branch comments reworded; logic unchanged.

⚠️ Empty-file receive fix reverted, leaving the path untested

packages/fetch/src/body.ts:57 now builds the received file as new File([blob], …, { type: blob.type }), reverting 17ada9c, whose rationale (in the comment this commit removes) was that "bun empties the blob type once .body has been touched". If that bun behavior still holds, an empty File/Blob with a real content-type (e.g. application/pdf) round-tripped through bun loses its type on receive, because re.blob() yields an empty-type blob. The commit also deletes the one test that pinned the desired behavior, so this can regress silently.

Note this delta is invisible in the base→head diff (base already used blob.type, and the test only ever existed between 17ada9c and 6572945), which is likely why it slipped through. Please confirm bun is no longer affected (and add coverage for the empty-file-with-content-type receive path), or restore the header source and the test.

Technical details
# Revert of empty-file receive fix (17ada9c -> 6572945)

## Affected sites
- packages/fetch/src/body.ts:57 — `new File([blob], fileName ?? 'blob', { type: blob.type })`;
  17ada9c used `type: contentType ?? ''` (content-type header) because bun empties
  `blob.type` once the body is read.
- packages/fetch/src/body.test.ts — the `'empty file (a zero content-length is an empty
  body, not a missing one)'` receive test was deleted with no replacement.

## Required outcome
- Empty file/blob with a non-empty content-type must keep its type on receive even when the
  transport yields a null/empty body (the bun case 17ada9c addressed).

## Open questions for the human
- Is the bun `blob.type`-emptying bug still a concern, or was `blob.type` verified correct
  across runtimes? If safe, re-add a test pinning type preservation; if not, restore the
  `content-type`-header source.
- Removing the null-body guard itself is correct/needed (senders always hint `body.size === 0`;
  line 22 covers typeless null bodies) — no action needed there.

Pullfrog  | Fix it ➔View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

@pullfrog pullfrog 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.

Important

The new "improve" commit (2ef4ede) silently removes the node receiver's GET/HEAD body-parsing guard together with its only test. No rationale appears in the commit or PR body. Please confirm this is intentional (and why) or restore it.

Reviewed changes — incremental delta since the prior pullfrog review (head 65729452ef4ede, "improve"):

  • Removed GET/HEAD body guard (node receive)toStandardBody no longer returns undefined early for GET/HEAD requests that arrive without an explicit standard-server/hint but with headers implying a body, and the only test pinning that behavior (ignores body parsing for GET requests even when headers imply a body) was deleted.

⚠️ GET/HEAD body-parsing guard removed without rationale

Commit 2ef4ede deletes EMPTY_BODY_METHOD_SET and the early return undefined in packages/node/src/body.ts that made toStandardBody ignore the body of GET/HEAD requests lacking an explicit hint, plus the sole test that pinned it. The MDN-referenced guard was a node-only safety net (the fetch receiver has no equivalent), so this changes receive-side behavior: a GET/HEAD request whose headers imply a body — e.g. content-type: application/json with a readable stream, or a content-length + content-type — is now parsed (returning a JSON value or a File) instead of returning undefined and leaving the stream untouched. The commit message is just "improve", so it's unclear whether this is an intentional parity change (aligning node with fetch) or an accidental revert; either way the deleted test means the new behavior is no longer pinned. Adding parity with fetch is a defensible cleanup, but it deserves a stated reason and, if kept, a replacement assertion for the empty-case (with content-type but no body, the json branch already returns undefined, so a guard-like outcome often still holds — worth confirming).

Technical details
# GET/HEAD body-parsing guard removed (commit 2ef4ede)

## Affected sites
- packages/node/src/body.ts — `EMPTY_BODY_METHOD_SET = new Set(['GET','HEAD'])` (old L18-21),
  the early `if (hint === undefined && EMPTY_BODY_METHOD_SET.has(toStandardMethod(req.method))) return undefined`
  (old L44-46), and the now-unused `toStandardMethod` import (old L10) were removed.
- packages/node/src/body.test.ts — `'ignores body parsing for GET requests even when headers imply a body'`
  (old L40-48) deleted with no replacement.

## Behavior change
`GET`/`HEAD` node http requests carrying headers that imply a body (content-type present with a
stream, or content-length + content-type) now get that body read and parsed by `toStandardBody`
(parseEmptyableJSON → value, or _streamToFile → File), where before they returned `undefined` and
left the stream untouched. No other test depends on the old guard; 925-test suite still passes.

## Required outcome
- Author explicitly confirms the removal is intentional and why (fetch parity? "GET with a body
  should parse"?), or restores the guard and its test.

## Open questions for the human
- Do any node adapters/downstream rely on GET/HEAD bodies being ignored?
- Is this a leftover from the standard-server header work, or a deliberate parity change?

Note: the prior review's flagged fetch-receive concern (File.type sourced from blob.type in packages/fetch/src/body.ts:57, reverting type preservation for empty file/blob round-trips) remains open — 2ef4ede doesn't touch the fetch adapter. It is tracked in the PR summary snapshot.

Pullfrog  | Fix it ➔View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

@dinwwwh

dinwwwh commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Closing: the premise doesn't hold. Dropping the standard-server header relies on the receiver inferring file from content-length, but a proxy that compresses the response drops or rewrites content-length, so that inference is not reliable enough to depend on.

@dinwwwh dinwwwh closed this Aug 12, 2026
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