Skip to content

Commit caeb672

Browse files
authored
feat(server): Request Headers Plugin (#804)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced the Request Headers Plugin, allowing handlers to access incoming request headers via a standardized context property. * Added comprehensive documentation for the Request Headers Plugin, including usage examples and integration guidance. * Added sidebar navigation for the new plugin documentation. * **Tests** * Added tests to verify correct behavior of the Request Headers Plugin, including context handling and header preservation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent fe5c63f commit caeb672

5 files changed

Lines changed: 202 additions & 0 deletions

File tree

apps/content/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export default withMermaid(defineConfig({
131131
collapsed: true,
132132
items: [
133133
{ text: 'CORS', link: '/docs/plugins/cors' },
134+
{ text: 'Request Headers', link: '/docs/plugins/request-headers' },
134135
{ text: 'Response Headers', link: '/docs/plugins/response-headers' },
135136
{ text: 'Hibernation', link: '/docs/plugins/hibernation' },
136137
{ text: 'Dedupe Requests', link: '/docs/plugins/dedupe-requests' },
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
title: Request Headers Plugin
3+
description: Request Headers Plugin for oRPC
4+
---
5+
6+
# Request Headers Plugin
7+
8+
The Request Headers Plugin allows you to access request headers in oRPC. It injects a `reqHeaders` instance into the `context`, enabling you to read incoming request headers easily.
9+
10+
::: info
11+
**What's the difference vs passing request headers directly into the context?**
12+
There's no functional difference, but this plugin provides a consistent interface for accessing headers across different handlers.
13+
:::
14+
15+
## Context Setup
16+
17+
```ts twoslash
18+
import { os } from '@orpc/server'
19+
// ---cut---
20+
import { RequestHeadersPluginContext } from '@orpc/server/plugins'
21+
22+
interface ORPCContext extends RequestHeadersPluginContext {}
23+
24+
const base = os.$context<ORPCContext>()
25+
26+
const example = base
27+
.use(({ context, next }) => {
28+
const authHeader = context.reqHeaders?.get('authorization')
29+
return next()
30+
})
31+
.handler(({ context }) => {
32+
const userAgent = context.reqHeaders?.get('user-agent')
33+
return { userAgent }
34+
})
35+
```
36+
37+
::: info
38+
**Why can `reqHeaders` be `undefined`?**
39+
This allows procedures to run safely even when `RequestHeadersPlugin` is not used, such as in direct calls.
40+
:::
41+
42+
## Handler Setup
43+
44+
```ts
45+
import { RequestHeadersPlugin } from '@orpc/server/plugins'
46+
47+
const handler = new RPCHandler(router, {
48+
plugins: [
49+
new RequestHeadersPlugin()
50+
],
51+
})
52+
```
53+
54+
::: info
55+
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or another custom handler.
56+
:::
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './batch'
22
export * from './cors'
3+
export * from './request-headers'
34
export * from './response-headers'
45
export * from './simple-csrf-protection'
56
export * from './strict-get-method'
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import type { RequestHeadersPluginContext } from './request-headers'
2+
import { OpenAPIHandler } from '../../../openapi/src/adapters/fetch/openapi-handler'
3+
import { os } from '../builder'
4+
import { RequestHeadersPlugin } from './request-headers'
5+
6+
describe('requestHeadersPlugin', () => {
7+
it('works', async () => {
8+
let capturedHeaders: Headers | undefined
9+
10+
const router = os
11+
.$context<RequestHeadersPluginContext>()
12+
.use(({ context, next }) => {
13+
// Capture the headers from context for verification
14+
capturedHeaders = context.reqHeaders
15+
return next()
16+
})
17+
.route({
18+
method: 'GET',
19+
path: '/ping',
20+
})
21+
.handler(() => 'pong')
22+
23+
const handler = new OpenAPIHandler(router, {
24+
plugins: [
25+
new RequestHeadersPlugin(),
26+
],
27+
})
28+
29+
const request = new Request('https://example.com/ping', {
30+
headers: {
31+
'x-custom-1': 'value1',
32+
'x-custom-2': 'value2',
33+
'content-type': 'application/json',
34+
'authorization': 'Bearer token123',
35+
},
36+
})
37+
38+
const { response } = await handler.handle(request)
39+
40+
if (!response) {
41+
throw new Error('response is undefined')
42+
}
43+
44+
expect(capturedHeaders).toBeInstanceOf(Headers)
45+
expect(capturedHeaders?.get('x-custom-1')).toBe('value1')
46+
expect(capturedHeaders?.get('x-custom-2')).toBe('value2')
47+
expect(capturedHeaders?.get('content-type')).toBe('application/json')
48+
expect(capturedHeaders?.get('authorization')).toBe('Bearer token123')
49+
})
50+
51+
it('should clone the context to avoid reference issues', async () => {
52+
const router = os
53+
.$context<RequestHeadersPluginContext>()
54+
.route({
55+
method: 'GET',
56+
path: '/ping',
57+
})
58+
.handler(() => 'ping')
59+
60+
const interceptor = vi.fn(({ next }) => next())
61+
62+
const handler = new OpenAPIHandler(router, {
63+
plugins: [
64+
new RequestHeadersPlugin(),
65+
],
66+
interceptors: [
67+
interceptor,
68+
],
69+
})
70+
71+
const context = { a: 'value' }
72+
await handler.handle(new Request('https://example.com/ping'), { context })
73+
74+
expect(interceptor).toHaveBeenCalledOnce()
75+
expect(interceptor).toHaveBeenCalledWith(expect.objectContaining({
76+
context: { ...context, reqHeaders: expect.any(Headers) },
77+
}))
78+
79+
expect(interceptor.mock.calls[0]![0].context).not.toBe(context)
80+
})
81+
82+
it('should use the provided reqHeaders when already defined', async () => {
83+
const router = os
84+
.$context<RequestHeadersPluginContext>()
85+
.route({
86+
method: 'GET',
87+
path: '/ping',
88+
})
89+
.handler(() => 'ping')
90+
91+
const interceptor = vi.fn(({ next }) => next())
92+
93+
const handler = new OpenAPIHandler(router, {
94+
plugins: [
95+
new RequestHeadersPlugin(),
96+
],
97+
interceptors: [
98+
interceptor,
99+
],
100+
})
101+
102+
const existingHeaders = new Headers({ 'existing-header': 'existing-value' })
103+
const context = { a: 'value', reqHeaders: existingHeaders }
104+
await handler.handle(new Request('https://example.com/ping'), { context })
105+
106+
expect(interceptor).toHaveBeenCalledOnce()
107+
expect(interceptor).toHaveBeenCalledWith(expect.objectContaining({
108+
context,
109+
}))
110+
111+
// The existing reqHeaders should be preserved
112+
expect(interceptor.mock.calls[0]![0].context.reqHeaders).toBe(existingHeaders)
113+
})
114+
})
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { StandardHandlerOptions, StandardHandlerPlugin } from '../adapters/standard'
2+
import { toFetchHeaders } from '@orpc/standard-server-fetch'
3+
4+
export interface RequestHeadersPluginContext {
5+
reqHeaders?: Headers
6+
}
7+
8+
/**
9+
* The Request Headers Plugin injects a `reqHeaders` instance into the context,
10+
* allowing access to request headers in oRPC.
11+
*
12+
* @see {@link https://orpc.unnoq.com/docs/plugins/request-headers Request Headers Plugin Docs}
13+
*/
14+
export class RequestHeadersPlugin<T extends RequestHeadersPluginContext> implements StandardHandlerPlugin<T> {
15+
init(options: StandardHandlerOptions<T>): void {
16+
options.rootInterceptors ??= []
17+
18+
options.rootInterceptors.push((interceptorOptions) => {
19+
const reqHeaders = interceptorOptions.context.reqHeaders ?? toFetchHeaders(interceptorOptions.request.headers)
20+
21+
return interceptorOptions.next({
22+
...interceptorOptions,
23+
context: {
24+
...interceptorOptions.context,
25+
reqHeaders,
26+
},
27+
})
28+
})
29+
}
30+
}

0 commit comments

Comments
 (0)