Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/content/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export default withMermaid(defineConfig({
collapsed: true,
items: [
{ text: 'CORS', link: '/docs/plugins/cors' },
{ text: 'Request Headers', link: '/docs/plugins/request-headers' },
{ text: 'Response Headers', link: '/docs/plugins/response-headers' },
{ text: 'Hibernation', link: '/docs/plugins/hibernation' },
{ text: 'Dedupe Requests', link: '/docs/plugins/dedupe-requests' },
Expand Down
56 changes: 56 additions & 0 deletions apps/content/docs/plugins/request-headers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
title: Request Headers Plugin
description: Request Headers Plugin for oRPC
---

# Request Headers Plugin

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.

::: info
**What's the difference vs passing request headers directly into the context?**
There's no functional difference, but this plugin provides a consistent interface for accessing headers across different handlers.
:::

## Context Setup

```ts twoslash
import { os } from '@orpc/server'
// ---cut---
import { RequestHeadersPluginContext } from '@orpc/server/plugins'

interface ORPCContext extends RequestHeadersPluginContext {}

const base = os.$context<ORPCContext>()

const example = base
.use(({ context, next }) => {
const authHeader = context.reqHeaders?.get('authorization')
return next()
})
.handler(({ context }) => {
const userAgent = context.reqHeaders?.get('user-agent')
return { userAgent }
})
```

::: info
**Why can `reqHeaders` be `undefined`?**
This allows procedures to run safely even when `RequestHeadersPlugin` is not used, such as in direct calls.
:::

## Handler Setup

```ts
import { RequestHeadersPlugin } from '@orpc/server/plugins'

const handler = new RPCHandler(router, {
plugins: [
new RequestHeadersPlugin()
],
})
```

::: info
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or another custom handler.
:::
1 change: 1 addition & 0 deletions packages/server/src/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './batch'
export * from './cors'
export * from './request-headers'
export * from './response-headers'
export * from './simple-csrf-protection'
export * from './strict-get-method'
114 changes: 114 additions & 0 deletions packages/server/src/plugins/request-headers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { RequestHeadersPluginContext } from './request-headers'
import { OpenAPIHandler } from '../../../openapi/src/adapters/fetch/openapi-handler'
import { os } from '../builder'
import { RequestHeadersPlugin } from './request-headers'

describe('requestHeadersPlugin', () => {
it('works', async () => {
let capturedHeaders: Headers | undefined

const router = os
.$context<RequestHeadersPluginContext>()
.use(({ context, next }) => {
// Capture the headers from context for verification
capturedHeaders = context.reqHeaders
return next()
})
.route({
method: 'GET',
path: '/ping',
})
.handler(() => 'pong')

const handler = new OpenAPIHandler(router, {
plugins: [
new RequestHeadersPlugin(),
],
})

const request = new Request('https://example.com/ping', {
headers: {
'x-custom-1': 'value1',
'x-custom-2': 'value2',
'content-type': 'application/json',
'authorization': 'Bearer token123',
},
})

const { response } = await handler.handle(request)

if (!response) {
throw new Error('response is undefined')
}

expect(capturedHeaders).toBeInstanceOf(Headers)
expect(capturedHeaders?.get('x-custom-1')).toBe('value1')
expect(capturedHeaders?.get('x-custom-2')).toBe('value2')
expect(capturedHeaders?.get('content-type')).toBe('application/json')
expect(capturedHeaders?.get('authorization')).toBe('Bearer token123')
})

it('should clone the context to avoid reference issues', async () => {
const router = os
.$context<RequestHeadersPluginContext>()
.route({
method: 'GET',
path: '/ping',
})
.handler(() => 'ping')

const interceptor = vi.fn(({ next }) => next())

const handler = new OpenAPIHandler(router, {
plugins: [
new RequestHeadersPlugin(),
],
interceptors: [
interceptor,
],
})

const context = { a: 'value' }
await handler.handle(new Request('https://example.com/ping'), { context })

expect(interceptor).toHaveBeenCalledOnce()
expect(interceptor).toHaveBeenCalledWith(expect.objectContaining({
context: { ...context, reqHeaders: expect.any(Headers) },
}))

expect(interceptor.mock.calls[0]![0].context).not.toBe(context)
})

it('should use the provided reqHeaders when already defined', async () => {
const router = os
.$context<RequestHeadersPluginContext>()
.route({
method: 'GET',
path: '/ping',
})
.handler(() => 'ping')

const interceptor = vi.fn(({ next }) => next())

const handler = new OpenAPIHandler(router, {
plugins: [
new RequestHeadersPlugin(),
],
interceptors: [
interceptor,
],
})

const existingHeaders = new Headers({ 'existing-header': 'existing-value' })
const context = { a: 'value', reqHeaders: existingHeaders }
await handler.handle(new Request('https://example.com/ping'), { context })

expect(interceptor).toHaveBeenCalledOnce()
expect(interceptor).toHaveBeenCalledWith(expect.objectContaining({
context,
}))

// The existing reqHeaders should be preserved
expect(interceptor.mock.calls[0]![0].context.reqHeaders).toBe(existingHeaders)
})
})
30 changes: 30 additions & 0 deletions packages/server/src/plugins/request-headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { StandardHandlerOptions, StandardHandlerPlugin } from '../adapters/standard'
import { toFetchHeaders } from '@orpc/standard-server-fetch'

export interface RequestHeadersPluginContext {
reqHeaders?: Headers
}

/**
* The Request Headers Plugin injects a `reqHeaders` instance into the context,
* allowing access to request headers in oRPC.
*
* @see {@link https://orpc.unnoq.com/docs/plugins/request-headers Request Headers Plugin Docs}
*/
export class RequestHeadersPlugin<T extends RequestHeadersPluginContext> implements StandardHandlerPlugin<T> {
init(options: StandardHandlerOptions<T>): void {
options.rootInterceptors ??= []

options.rootInterceptors.push((interceptorOptions) => {
const reqHeaders = interceptorOptions.context.reqHeaders ?? toFetchHeaders(interceptorOptions.request.headers)

return interceptorOptions.next({
...interceptorOptions,
context: {
...interceptorOptions.context,
reqHeaders,
},
})
})
}
}