Skip to content

Commit c994d65

Browse files
unnoqCopilot
andauthored
fix(standard-server): filter out undefined headers for node:http adapters compatibility (#1269)
Node.js http module throws an error when headers contain undefined values. Additionally, Fastify treats undefined headers as empty strings, which differs from oRPC's expected behavior of omitting them from the response. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved HTTP response header handling to filter out undefined values, ensuring consistent and correct headers (including intentional omission of content-type) across Fastify and Node.js responses. * **Tests** * Added unit tests validating header transformation and that undefined headers are omitted, plus coverage for multiple response scenarios. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 80df4d2 commit c994d65

File tree

7 files changed

+96
-5
lines changed

7 files changed

+96
-5
lines changed

packages/standard-server-fastify/src/response.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import request from 'supertest'
66
import { sendStandardResponse } from './response'
77

88
const toNodeHttpBodySpy = vi.spyOn(StandardServerNode, 'toNodeHttpBody')
9+
const toNodeHttpHeadersSpy = vi.spyOn(StandardServerNode, 'toNodeHttpHeaders')
910

1011
beforeEach(() => {
1112
vi.clearAllMocks()
@@ -39,11 +40,16 @@ describe('sendStandardResponse', () => {
3940
'x-custom-header': 'custom-value',
4041
}, options)
4142

43+
expect(toNodeHttpHeadersSpy).toBeCalledTimes(1)
44+
expect(toNodeHttpHeadersSpy).toBeCalledWith({
45+
'x-custom-header': 'custom-value',
46+
})
47+
4248
expect(sendSpy).toBeCalledTimes(1)
4349
expect(sendSpy).toBeCalledWith(undefined)
4450

4551
expect(res.status).toBe(207)
46-
expect(res.headers['content-type']).toEqual(undefined)
52+
expect(res.headers).not.toHaveProperty('content-type')
4753
expect(res.headers['x-custom-header']).toEqual('custom-value')
4854

4955
expect(res.text).toEqual('')
@@ -77,6 +83,12 @@ describe('sendStandardResponse', () => {
7783
'x-custom-header': 'custom-value',
7884
}, options)
7985

86+
expect(toNodeHttpHeadersSpy).toBeCalledTimes(1)
87+
expect(toNodeHttpHeadersSpy).toBeCalledWith({
88+
'content-type': 'application/json',
89+
'x-custom-header': 'custom-value',
90+
})
91+
8092
expect(sendSpy).toBeCalledTimes(1)
8193
expect(sendSpy).toBeCalledWith(toNodeHttpBodySpy.mock.results[0]!.value)
8294

@@ -120,6 +132,14 @@ describe('sendStandardResponse', () => {
120132
'x-custom-header': 'custom-value',
121133
}, options)
122134

135+
expect(toNodeHttpHeadersSpy).toBeCalledTimes(1)
136+
expect(toNodeHttpHeadersSpy).toBeCalledWith({
137+
'content-disposition': 'inline; filename="blob"; filename*=utf-8\'\'blob',
138+
'content-length': '3',
139+
'content-type': 'text/plain',
140+
'x-custom-header': 'custom-value',
141+
})
142+
123143
expect(sendSpy).toBeCalledTimes(1)
124144
expect(sendSpy).toBeCalledWith(toNodeHttpBodySpy.mock.results[0]!.value)
125145

@@ -170,6 +190,12 @@ describe('sendStandardResponse', () => {
170190
'x-custom-header': 'custom-value',
171191
}, options)
172192

193+
expect(toNodeHttpHeadersSpy).toBeCalledTimes(1)
194+
expect(toNodeHttpHeadersSpy).toBeCalledWith({
195+
'content-type': 'text/event-stream',
196+
'x-custom-header': 'custom-value',
197+
})
198+
173199
expect(sendSpy).toBeCalledTimes(1)
174200
expect(sendSpy).toBeCalledWith(toNodeHttpBodySpy.mock.results[0]!.value)
175201

packages/standard-server-fastify/src/response.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { StandardHeaders, StandardResponse } from '@orpc/standard-server'
22
import type { ToNodeHttpBodyOptions } from '@orpc/standard-server-node'
33
import type { FastifyReply } from 'fastify'
4-
import { toNodeHttpBody } from '@orpc/standard-server-node'
4+
import { toNodeHttpBody, toNodeHttpHeaders } from '@orpc/standard-server-node'
55

66
export interface SendStandardResponseOptions extends ToNodeHttpBodyOptions { }
77

@@ -19,7 +19,9 @@ export function sendStandardResponse(
1919
const resBody = toNodeHttpBody(standardResponse.body, resHeaders, options)
2020

2121
reply.status(standardResponse.status)
22-
reply.headers(resHeaders)
22+
// Fastify treats undefined headers as empty string, so remember to use toNodeHttpHeaders
23+
// to filter out undefined headers
24+
reply.headers(toNodeHttpHeaders(resHeaders))
2325
reply.send(resBody)
2426
})
2527
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { toNodeHttpHeaders } from './headers'
2+
3+
describe('toNodeHttpHeaders', () => {
4+
it('filters out undefined values', () => {
5+
const headers = toNodeHttpHeaders({
6+
'x-custom': 'value',
7+
'x-undefined': undefined,
8+
'set-cookie': ['cookie1=value1', 'cookie2=value2'],
9+
})
10+
11+
expect(headers).toEqual({
12+
'x-custom': 'value',
13+
'set-cookie': ['cookie1=value1', 'cookie2=value2'],
14+
})
15+
expect(headers).not.toHaveProperty('x-undefined')
16+
})
17+
})
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { StandardHeaders } from '@orpc/standard-server'
2+
import type { OutgoingHttpHeaders } from 'node:http'
3+
4+
export function toNodeHttpHeaders(headers: StandardHeaders): OutgoingHttpHeaders {
5+
const nodeHttpHeaders: OutgoingHttpHeaders = {}
6+
7+
for (const [key, value] of Object.entries(headers)) {
8+
// Node.js does not allow headers to be undefined
9+
if (value !== undefined) {
10+
nodeHttpHeaders[key] = value
11+
}
12+
}
13+
14+
return nodeHttpHeaders
15+
}

packages/standard-server-node/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './body'
22
export * from './event-iterator'
3+
export * from './headers'
34
export * from './method'
45
export * from './request'
56
export * from './response'

packages/standard-server-node/src/response.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { Buffer } from 'node:buffer'
44
import Stream from 'node:stream'
55
import request from 'supertest'
66
import * as Body from './body'
7+
import * as Headers from './headers'
78
import { sendStandardResponse } from './response'
89

910
const toNodeHttpBodySpy = vi.spyOn(Body, 'toNodeHttpBody')
11+
const toNodeHttpHeadersSpy = vi.spyOn(Headers, 'toNodeHttpHeaders')
1012

1113
beforeEach(() => {
1214
vi.clearAllMocks()
@@ -34,11 +36,16 @@ describe('sendStandardResponse', () => {
3436
'x-custom-header': 'custom-value',
3537
}, options)
3638

39+
expect(toNodeHttpHeadersSpy).toBeCalledTimes(1)
40+
expect(toNodeHttpHeadersSpy).toBeCalledWith({
41+
'x-custom-header': 'custom-value',
42+
})
43+
3744
expect(endSpy).toBeCalledTimes(1)
3845
expect(endSpy).toBeCalledWith()
3946

4047
expect(res.status).toBe(207)
41-
expect(res.headers['content-type']).toEqual(undefined)
48+
expect(res.headers).not.toHaveProperty('content-type')
4249
expect(res.headers['x-custom-header']).toEqual('custom-value')
4350

4451
expect(res.text).toEqual('')
@@ -66,6 +73,12 @@ describe('sendStandardResponse', () => {
6673
'x-custom-header': 'custom-value',
6774
}, options)
6875

76+
expect(toNodeHttpHeadersSpy).toBeCalledTimes(1)
77+
expect(toNodeHttpHeadersSpy).toBeCalledWith({
78+
'content-type': 'application/json',
79+
'x-custom-header': 'custom-value',
80+
})
81+
6982
expect(endSpy).toBeCalledTimes(1)
7083
expect(endSpy).toBeCalledWith(toNodeHttpBodySpy.mock.results[0]!.value)
7184

@@ -103,6 +116,14 @@ describe('sendStandardResponse', () => {
103116
'x-custom-header': 'custom-value',
104117
}, options)
105118

119+
expect(toNodeHttpHeadersSpy).toBeCalledTimes(1)
120+
expect(toNodeHttpHeadersSpy).toBeCalledWith({
121+
'content-disposition': 'inline; filename="blob"; filename*=utf-8\'\'blob',
122+
'content-length': '3',
123+
'content-type': 'text/plain',
124+
'x-custom-header': 'custom-value',
125+
})
126+
106127
expect(endSpy).toBeCalledTimes(1)
107128
expect(endSpy).toBeCalledWith()
108129

@@ -148,6 +169,12 @@ describe('sendStandardResponse', () => {
148169
'x-custom-header': 'custom-value',
149170
}, options)
150171

172+
expect(toNodeHttpHeadersSpy).toBeCalledTimes(1)
173+
expect(toNodeHttpHeadersSpy).toBeCalledWith({
174+
'content-type': 'text/event-stream',
175+
'x-custom-header': 'custom-value',
176+
})
177+
151178
expect(endSpy).toBeCalledTimes(1)
152179
expect(endSpy).toBeCalledWith()
153180

packages/standard-server-node/src/response.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { StandardHeaders, StandardResponse } from '@orpc/standard-server'
22
import type { ToNodeHttpBodyOptions } from './body'
33
import type { NodeHttpResponse } from './types'
44
import { toNodeHttpBody } from './body'
5+
import { toNodeHttpHeaders } from './headers'
56

67
export interface SendStandardResponseOptions extends ToNodeHttpBodyOptions {}
78

@@ -18,7 +19,9 @@ export function sendStandardResponse(
1819

1920
const resBody = toNodeHttpBody(standardResponse.body, resHeaders, options)
2021

21-
res.writeHead(standardResponse.status, resHeaders)
22+
// Node.js throws an error when a header is undefined, so remember to use toNodeHttpHeaders
23+
// to filter out undefined headers
24+
res.writeHead(standardResponse.status, toNodeHttpHeaders(resHeaders))
2225

2326
if (resBody === undefined) {
2427
res.end()

0 commit comments

Comments
 (0)