Skip to content

feat(rpc)!: carry batched cookies and shared headers on the batch response - #2264

Merged
dinwwwh merged 3 commits into
mainfrom
claude/sleepy-albattani-tzcpib
Oct 19, 2026
Merged

dinwwwh merged 3 commits into
mainfrom
claude/sleepy-albattani-tzcpib

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 19, 2026

Copy link
Copy Markdown
Member

Summary

Removes the mode option from the batch plugins on both client and server, simplifying the batch API. The server now always starts the batch response once every sub-request has responded (allowing it to carry shared headers and cookies), and streams the response if any sub-response is a stream. This eliminates the need for users to choose between "buffered" and "streaming" modes.

Key Changes

  • Server batch plugin: Removed mode parameter from batch requests; the orpc-batch header now always has value '1' instead of 'buffered' or 'streaming'
  • Response header lifting: Implemented automatic extraction and merging of headers from sub-responses:
    • Cookies from all sub-responses are collected onto the batch response
    • Headers that all sub-responses share with the same value are lifted to the batch response
    • Body-specific headers (content-*, standard-server) are kept on sub-responses
    • Added mapSubresponse option to customize this behavior
  • Streaming behavior: Batch responses now stream when any sub-response is a stream, rather than being mode-dependent
  • Client batch plugin: Removed mode option and BatchLinkPluginMode type export
  • Tests: Updated all batch tests to remove mode parameters and verify the new header-lifting behavior
  • Documentation: Updated batch plugin docs to reflect the simplified API and new header handling

Implementation Details

  • Added SubresponseHead interface to track headers and stream status before full responses are ready
  • Implemented liftSubresponseHeaders() function to intelligently merge sub-response headers onto the batch response
  • Added internalError() helper to safely handle errors in the routing interceptor
  • The batch response waits for all sub-responses to be ready before starting, enabling header collection
  • Keep-alive frames continue to work for streaming responses

https://claude.ai/code/session_01UtE8iZthQGzPQQoMXCSgzc

Browsers only read the headers of the batch response, so cookies set in a
batched call were never stored, and they ended up in the batch body where
JavaScript could read them, even HttpOnly ones.

BatchHandlerPlugin now starts the batch response once every call has
responded, and streams it only when a call returns an event iterator or a
ReadableStream:

- set-cookie never stays on a call. The batch response carries every
  call's cookies, each distinct value once.
- A header every call sets to the same value moves onto the batch
  response, and the link merges it back into each call. Body headers
  (content-*) stay on their call, and the headers option wins.
- A new mapSubresponse option decides what stays on each call. Its
  default removes set-cookie and the headers the batch response already
  has with the same value.

BatchLinkPlugin loses its mode option and BatchLinkPluginMode type, and
sends `orpc-batch: 1`. Links from before this change still work with the
new server.

BREAKING CHANGE: BatchLinkPlugin no longer accepts `mode`, and
`BatchLinkPluginMode` is removed. A batch no longer sends each response
as soon as it is ready.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UtE8iZthQGzPQQoMXCSgzc
…ption opt out

Each call now hands the batch only its headers and whether it streams,
so a large result is freed once it is sent instead of living until the
longest stream in the batch ends. The headers option wins over the
headers the calls share by spread order, so setting one to undefined
keeps it off the batch response. internalError moves to module scope,
and tests and comments left over from streaming mode are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UtE8iZthQGzPQQoMXCSgzc
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
orpc 98a0792 Commit Preview URL

Branch Preview URL
Oct 19 2026, 03:53 AM

@dinwwwh dinwwwh changed the title refactor(rpc): remove batch mode distinction, always stream when needed feat(rpc)!: carry batched cookies and shared headers on the batch response Oct 19, 2026
@codecov

codecov Bot commented Oct 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@dinwwwh
dinwwwh requested a balanced review from Copilot October 19, 2026 03:34
@pkg-pr-new

pkg-pr-new Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@2264

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@2264

@orpc/bun

npm i https://pkg.pr.new/@orpc/bun@2264

@orpc/client

npm i https://pkg.pr.new/@orpc/client@2264

@orpc/cloudflare

npm i https://pkg.pr.new/@orpc/cloudflare@2264

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@2264

@orpc/experimental-effect

npm i https://pkg.pr.new/@orpc/experimental-effect@2264

@orpc/evlog

npm i https://pkg.pr.new/@orpc/evlog@2264

@orpc/hibernation

npm i https://pkg.pr.new/@orpc/hibernation@2264

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@2264

@orpc/experimental-lock

npm i https://pkg.pr.new/@orpc/experimental-lock@2264

@orpc/experimental-msw

npm i https://pkg.pr.new/@orpc/experimental-msw@2264

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@2264

@orpc/next

npm i https://pkg.pr.new/@orpc/next@2264

@orpc/node

npm i https://pkg.pr.new/@orpc/node@2264

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@2264

@orpc/opentelemetry

npm i https://pkg.pr.new/@orpc/opentelemetry@2264

@orpc/pinia-colada

npm i https://pkg.pr.new/@orpc/pinia-colada@2264

@orpc/pino

npm i https://pkg.pr.new/@orpc/pino@2264

@orpc/publisher

npm i https://pkg.pr.new/@orpc/publisher@2264

@orpc/ratelimit

npm i https://pkg.pr.new/@orpc/ratelimit@2264

@orpc/server

npm i https://pkg.pr.new/@orpc/server@2264

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@2264

@orpc/swr

npm i https://pkg.pr.new/@orpc/swr@2264

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@2264

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@2264

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@2264

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@2264

commit: 98a0792

@codspeed

codspeed Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

⚠️ 6 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

✅ 28 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing claude/sleepy-albattani-tzcpib (98a0792) with main (f8504ba)

Open in CodSpeed

Footnotes

  1. 2 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. ↩

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

ℹ️ Minor suggestions only — nothing blocking.

Reviewed changes

  • Batch API simplification — BatchLinkPlugin no longer accepts mode, the BatchLinkPluginMode type is gone, and the orpc-batch request header is always '1' (the server treats any non-empty value as a batch).
  • Server-side header lifting — the batch response now waits for every subresponse's head and lifts the cookies all subresponses set plus the headers they all share, with a new mapSubresponse option to customize what stays on each subresponse.
  • Always stream when needed — the server streams the batch response iff any subresponse body is a ReadableStream or AsyncIteratorObject, replacing the mode-driven split; keep-alive now applies only to batches that stream.
  • Docs and tests — batch.mdx reworked (Response Modes → Streaming Responses + Response Headers), migration and better-auth docs updated, and the unit + e2e suites reworked around the new semantics.

ℹ️ The batch now blocks on the slowest sub-response, and abort cannot release one that ignores its signal

const { batchHeaders, streams } = await lifting (packages/server/src/plugins/batch.ts:421) gates the entire batch response on every subresponse reaching markReady (batch.ts:382). A procedure whose interceptorOptions.next never settles — e.g. one that ignores its signal — leaves lifting pending forever, so the routing interceptor never returns a response and the per-subrequest abort (peer close) cannot break the wait. Previously the interceptor raced (Promise.race([streaming.promise, subrequests.then(...)])) and returned as soon as a stream appeared, so the other subresponses still streamed.

Technical details
# Batches can hang on a never-settling subresponse

## Affected sites
- `packages/server/src/plugins/batch.ts:316-324` — `ready`/`lifting` wait for all heads.
- `packages/server/src/plugins/batch.ts:375-386` — `markReady` runs only after `next` settles.
- `packages/server/src/plugins/batch.ts:401` — the `.finally(resolve(undefined))` safety net is on `peer.message`, which itself awaits the handler, so it does not cover this case.
- `packages/server/src/plugins/batch.ts:421` — `await lifting` never returns while one `ready[i]` is pending.

## Required outcome
Decide whether a subrequest that never settles (including one that ignores its abort signal)
should be allowed to hold the whole batch open indefinitely. If not, the batch needs a
fallback that returns a response (or closes) without waiting on every head.

## Open questions for the human
- Is "wait for every procedure" a hard requirement of carrying shared headers/cookies, or
  acceptable to relax (e.g. start once headers are known from the responded subset)?
- Should an aborted batch request release the interceptor even if a handler ignores `signal`?

</details>

<!--
Pullfrog review metadata. These findings were written against ec32616;
if commits have landed on claude/sleepy-albattani-tzcpib since, treat every specific bug, file, or
line callout as POTENTIALLY STALE and re-diff before acting on it.

- Mode: Review (initial)
- Files reviewed: 18
- Commits reviewed: 2
- Base: main (f8504ba)
- Head: claude/sleepy-albattani-tzcpib (ec32616)
- Reviewed commits:
  - ec326162 — refactor(server): free batched bodies once sent and let the headers option opt out
  - 245b26a2 — feat!: carry batched cookies and shared headers on the batch response
- Prior pullfrog review: none
-->

<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->
<sup><a href="https://pullfrog.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pullfrog.com/logos/frog-white-full-18px.png"><img src="https://pullfrog.com/logos/frog-green-full-18px.png" width="9px" height="9px" style="vertical-align: middle; " alt="Pullfrog"></picture></a>&nbsp;&nbsp;| [Fix all ➔](https://pullfrog.com/trigger/middleapi/orpc/2264?action=fix&review_id=5566654127) | [Fix 👍s ➔](https://pullfrog.com/trigger/middleapi/orpc/2264?action=fix-approved&review_id=5566654127) | [View workflow run](https://github.com/middleapi/orpc/actions/runs/39195135495/job/117876178112) | Using `deepseek-v4.1-flash` (free via [Pullfrog for OSS](https://pullfrog.com/for-oss)) | [𝕏](https://x.com/pullfrogai)</sup>

Comment thread packages/server/src/plugins/batch.ts
Comment thread packages/server/src/plugins/batch.ts Outdated

Copilot AI 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.

🟡 Changes recommended

Cache variation can be lost during header lifting, and mapped streaming bodies can prevent batch responses from starting.

2 open issues
What changed in this PR

Simplifies oRPC batching by removing response modes and carrying cookies and shared headers on the batch response.

Changes:

  • Uses orpc-batch: 1 and automatically selects streaming.
  • Adds header lifting and a mapSubresponse hook.
  • Updates tests, integration helpers, and migration documentation.
File Description
tests/​plugins/​all-plugins.test.ts Tests batched cookies and shared headers.
tests/​batch/​batch-plugin.test.ts Updates mixed-response coverage.
tests/​batch/​__shared__/​client-server.ts Removes mode options.
tests/​batch/​__shared__/​client-server.node-http.ts Updates Node HTTP setup.
tests/​batch/​__shared__/​client-server.hono-fetch.ts Updates Hono setup.
tests/​batch/​__shared__/​client-server.compression-node-http.ts Updates compressed Node setup.
tests/​batch/​__shared__/​client-server.compression-hono-fetch.ts Updates compressed Hono setup.
packages/​server/​src/​plugins/​get-method-csrf-protection.test.ts Updates the batch header value.
packages/​server/​src/​plugins/​batch.ts Implements header lifting and automatic streaming.
packages/​server/​src/​plugins/​batch.test.ts Tests headers, mapping, streaming, and cancellation.
packages/​node/​src/​batch-response-compression-handler-plugin.ts Clarifies a compression comment.
packages/​node/​src/​batch-response-compression-handler-plugin.test.ts Adapts compression tests to automatic streaming.
packages/​client/​src/​plugins/​batch.ts Removes mode configuration.
packages/​client/​src/​plugins/​batch.test.ts Updates client batching tests.
apps/​content/​docs/​plugins/​batch.mdx Documents streaming and response headers.
apps/​content/​docs/​plugins/​batch-response-compression.mdx Revises the streaming explanation.
apps/​content/​docs/​migrations/​from-v1.mdx Explains mode removal.
apps/​content/​docs/​integrations/​better-auth.mdx Removes the batching cookie limitation.

Review effort: Balanced

Comment thread packages/server/src/plugins/batch.ts
Comment on lines +382 to 385
markReady({
headers: response.headers,
stream: response.body instanceof ReadableStream || isAsyncIteratorObject(response.body),
})
…sponse

A cache-control every call shares moves onto the batch response, but a
Vary only some calls set stayed inside the body, so a shared cache could
reuse the batch across languages or, with Vary: Cookie, across sessions.
The batch response now varies on every field any call or the headers
option varies on, and on everything when one varies on `*`. The headers
option docs now say set-cookie and vary merge instead of being replaced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UtE8iZthQGzPQQoMXCSgzc

@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 latest commit only extends header lifting to Vary and was reviewed in full.

Reviewed changes

  • Vary union in header lifting — liftSubresponseHeaders() now collects Vary fields from the plugin headers option and every subresponse, splits and trims them, de-dupes case-insensitively (keeping first-seen casing), joins with , , and collapses to * when any subresponse varies on everything.
  • Docs and JSDoc — batch.mdx and the headers option now state that the batch response carries the Vary fields of its subresponses, and that set-cookie and vary merge rather than being suppressible with undefined.
  • Tests — added coverage for the field union across subresponses, the headers option ordering, and the * case.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@dinwwwh
dinwwwh merged commit 0c7f63b into main Oct 19, 2026
12 checks passed
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.

3 participants