Skip to content

Commit 101a7ac

Browse files
authored
fix(server): let batch request headers win over sub-request headers (#1912)
The batch handler's default `mapSubrequest` merged the batch root request headers into each sub-request but let the sub-request win on collision. That order is now flipped, so the batch request headers take priority. The batch root request is the only part the transport actually saw. Its headers are set by the browser (`cookie`, `origin`, `sec-fetch-*`) or by a proxy (`x-forwarded-for`, `authorization`), while a sub-request is client-controlled request payload. A sub-request can no longer overwrite them. ## Compatibility With the default `BatchLinkPlugin`, behavior is unchanged. Its batch headers are the headers common to every sub-request, and its default `mapSubrequest` strips those from each sub-request, so the two sets never collide. Only setups with a custom client `headers` option, or a hand-rolled client, that deliberately set a per-sub-request override of a batch-level header are affected. They can restore the old merge through the `mapSubrequest` option. ## Testing `packages/server`, `packages/client`, and the root `tests/` suites pass, along with `pnpm lint` and `pnpm type:check`. The existing header-merge test now asserts the new precedence, and a spoofing test covers a sub-request trying to override an `authorization` header carried by the batch request.
1 parent 3ff7f48 commit 101a7ac

2 files changed

Lines changed: 21 additions & 4 deletions

File tree

packages/server/src/plugins/batch.test.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ describe('batchHandlerPlugin', () => {
404404
})
405405
}
406406

407-
it('merges batch request headers into sub-requests and lets sub-requests win', async () => {
407+
it('merges batch request headers into sub-requests and lets the batch request win', async () => {
408408
const { handler, seenHeaders } = createHeaderCapturingHandler()
409409

410410
await handler.handle(createBatchRequestWithHeaders(
@@ -415,10 +415,21 @@ describe('batchHandlerPlugin', () => {
415415
expect(seenHeaders[0]).toMatchObject({
416416
'x-from-batch': 'batch',
417417
'x-from-sub-request': 'sub-request',
418-
'x-overridden': 'sub-request',
418+
'x-overridden': 'batch',
419419
})
420420
expect(seenHeaders[0]!['orpc-batch']).toBeUndefined()
421421
})
422+
423+
it('prevents a sub-request from spoofing a header the batch request already carries', async () => {
424+
const { handler, seenHeaders } = createHeaderCapturingHandler()
425+
426+
await handler.handle(createBatchRequestWithHeaders(
427+
{ authorization: 'Bearer real' },
428+
{ authorization: 'Bearer spoofed' },
429+
))
430+
431+
expect(seenHeaders[0]!.authorization).toEqual('Bearer real')
432+
})
422433
})
423434

424435
describe('configuration options', () => {

packages/server/src/plugins/batch.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export interface BatchHandlerPluginOptions<T extends Context> {
2828
/**
2929
* Map each subrequest in the batch before it is processed.
3030
*
31-
* @default merges the batch request headers into the subrequest and remove `orpc-batch` header to prevent nested batching
31+
* @default merges the batch request headers into the subrequest, giving them priority over
32+
* the subrequest headers, and removes the `orpc-batch` header to prevent nested batching
3233
*/
3334
mapSubrequest?: (subrequest: StandardLazyRequest, batchOptions: StandardHandlerRoutingInterceptorOptions<T>) => StandardLazyRequest
3435

@@ -102,8 +103,13 @@ export class BatchHandlerPlugin<T extends Context> implements StandardHandlerPlu
102103
this.mapSubrequest = options.mapSubrequest ?? ((subRequest, { request: batchRequest }) => ({
103104
...subRequest,
104105
headers: {
105-
...batchRequest.headers,
106106
...subRequest.headers,
107+
/**
108+
* The batch request headers win over the subrequest ones. They are the only ones the
109+
* transport actually saw, so headers the browser injects on its own, such as `cookie`
110+
* or `origin`, cannot be overridden by a subrequest, which is just request payload.
111+
*/
112+
...batchRequest.headers,
107113
'orpc-batch': undefined, // useful in case batch plugin is used multiple times
108114
},
109115
}))

0 commit comments

Comments
 (0)