From 34d6ccf4feb9d9f51f4e2bf4e5d0ac5d146909c7 Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 2 Dec 2025 14:33:53 +0700 Subject: [PATCH 1/5] fix(standard-server): filter out undefined headers for node adapters compatibility 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. --- .../src/response.test.ts | 28 +++++++++++++++++- .../standard-server-fastify/src/response.ts | 6 ++-- .../standard-server-node/src/headers.test.ts | 17 +++++++++++ packages/standard-server-node/src/headers.ts | 16 ++++++++++ packages/standard-server-node/src/index.ts | 1 + .../standard-server-node/src/response.test.ts | 29 ++++++++++++++++++- packages/standard-server-node/src/response.ts | 5 +++- 7 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 packages/standard-server-node/src/headers.test.ts create mode 100644 packages/standard-server-node/src/headers.ts diff --git a/packages/standard-server-fastify/src/response.test.ts b/packages/standard-server-fastify/src/response.test.ts index bd1c6ed8a..19fae60e5 100644 --- a/packages/standard-server-fastify/src/response.test.ts +++ b/packages/standard-server-fastify/src/response.test.ts @@ -6,6 +6,7 @@ import request from 'supertest' import { sendStandardResponse } from './response' const toNodeHttpBodySpy = vi.spyOn(StandardServerNode, 'toNodeHttpBody') +const toNodeHttpHeadersSpy = vi.spyOn(StandardServerNode, 'toNodeHttpHeaders') beforeEach(() => { vi.clearAllMocks() @@ -39,11 +40,16 @@ describe('sendStandardResponse', () => { 'x-custom-header': 'custom-value', }, options) + expect(toNodeHttpHeadersSpy).toBeCalledTimes(1) + expect(toNodeHttpHeadersSpy).toBeCalledWith({ + 'x-custom-header': 'custom-value', + }) + expect(sendSpy).toBeCalledTimes(1) expect(sendSpy).toBeCalledWith(undefined) expect(res.status).toBe(207) - expect(res.headers['content-type']).toEqual(undefined) + expect(res.headers).not.toHaveProperty('content-type') expect(res.headers['x-custom-header']).toEqual('custom-value') expect(res.text).toEqual('') @@ -77,6 +83,12 @@ describe('sendStandardResponse', () => { 'x-custom-header': 'custom-value', }, options) + expect(toNodeHttpHeadersSpy).toBeCalledTimes(1) + expect(toNodeHttpHeadersSpy).toBeCalledWith({ + 'content-type': 'application/json', + 'x-custom-header': 'custom-value', + }) + expect(sendSpy).toBeCalledTimes(1) expect(sendSpy).toBeCalledWith(toNodeHttpBodySpy.mock.results[0]!.value) @@ -120,6 +132,14 @@ describe('sendStandardResponse', () => { 'x-custom-header': 'custom-value', }, options) + expect(toNodeHttpHeadersSpy).toBeCalledTimes(1) + expect(toNodeHttpHeadersSpy).toBeCalledWith({ + 'content-disposition': 'inline; filename="blob"; filename*=utf-8\'\'blob', + 'content-length': '3', + 'content-type': 'text/plain', + 'x-custom-header': 'custom-value', + }) + expect(sendSpy).toBeCalledTimes(1) expect(sendSpy).toBeCalledWith(toNodeHttpBodySpy.mock.results[0]!.value) @@ -170,6 +190,12 @@ describe('sendStandardResponse', () => { 'x-custom-header': 'custom-value', }, options) + expect(toNodeHttpHeadersSpy).toBeCalledTimes(1) + expect(toNodeHttpHeadersSpy).toBeCalledWith({ + 'content-type': 'text/event-stream', + 'x-custom-header': 'custom-value', + }) + expect(sendSpy).toBeCalledTimes(1) expect(sendSpy).toBeCalledWith(toNodeHttpBodySpy.mock.results[0]!.value) diff --git a/packages/standard-server-fastify/src/response.ts b/packages/standard-server-fastify/src/response.ts index eaeaea8d1..07fd00fb8 100644 --- a/packages/standard-server-fastify/src/response.ts +++ b/packages/standard-server-fastify/src/response.ts @@ -1,7 +1,7 @@ import type { StandardHeaders, StandardResponse } from '@orpc/standard-server' import type { ToNodeHttpBodyOptions } from '@orpc/standard-server-node' import type { FastifyReply } from 'fastify' -import { toNodeHttpBody } from '@orpc/standard-server-node' +import { toNodeHttpBody, toNodeHttpHeaders } from '@orpc/standard-server-node' export interface SendStandardResponseOptions extends ToNodeHttpBodyOptions { } @@ -19,7 +19,9 @@ export function sendStandardResponse( const resBody = toNodeHttpBody(standardResponse.body, resHeaders, options) reply.status(standardResponse.status) - reply.headers(resHeaders) + // fastify treat undefined headers as empty string, so remember to use toNodeHttpHeaders + // to filter out undefined headers + reply.headers(toNodeHttpHeaders(resHeaders)) reply.send(resBody) }) } diff --git a/packages/standard-server-node/src/headers.test.ts b/packages/standard-server-node/src/headers.test.ts new file mode 100644 index 000000000..968987ae2 --- /dev/null +++ b/packages/standard-server-node/src/headers.test.ts @@ -0,0 +1,17 @@ +import { toNodeHttpHeaders } from './headers' + +describe('toNodeHttpHeaders', () => { + it('filters out undefined values', () => { + const headers = toNodeHttpHeaders({ + 'x-custom': 'value', + 'x-undefined': undefined, + 'set-cookie': ['cookie1=value1', 'cookie2=value2'], + }) + + expect(headers).toEqual({ + 'x-custom': 'value', + 'set-cookie': ['cookie1=value1', 'cookie2=value2'], + }) + expect(headers).not.toHaveProperty('x-undefined') + }) +}) diff --git a/packages/standard-server-node/src/headers.ts b/packages/standard-server-node/src/headers.ts new file mode 100644 index 000000000..d99ec3ae1 --- /dev/null +++ b/packages/standard-server-node/src/headers.ts @@ -0,0 +1,16 @@ +import type { StandardHeaders } from '@orpc/standard-server' +import type { OutgoingHttpHeaders } from 'node:http' + +export function toNodeHttpHeaders(headers: StandardHeaders): OutgoingHttpHeaders { + const nodeHttpHeaders: OutgoingHttpHeaders = {} + + for (const key in headers) { + const value = headers[key] + // nodejs not allow header is undefined + if (value !== undefined) { + nodeHttpHeaders[key] = value + } + } + + return nodeHttpHeaders +} diff --git a/packages/standard-server-node/src/index.ts b/packages/standard-server-node/src/index.ts index 005d1b859..2cab78bcf 100644 --- a/packages/standard-server-node/src/index.ts +++ b/packages/standard-server-node/src/index.ts @@ -1,5 +1,6 @@ export * from './body' export * from './event-iterator' +export * from './headers' export * from './method' export * from './request' export * from './response' diff --git a/packages/standard-server-node/src/response.test.ts b/packages/standard-server-node/src/response.test.ts index 3dab22d47..414b3b2e1 100644 --- a/packages/standard-server-node/src/response.test.ts +++ b/packages/standard-server-node/src/response.test.ts @@ -4,9 +4,11 @@ import { Buffer } from 'node:buffer' import Stream from 'node:stream' import request from 'supertest' import * as Body from './body' +import * as Headers from './headers' import { sendStandardResponse } from './response' const toNodeHttpBodySpy = vi.spyOn(Body, 'toNodeHttpBody') +const toNodeHttpHeadersSpy = vi.spyOn(Headers, 'toNodeHttpHeaders') beforeEach(() => { vi.clearAllMocks() @@ -34,11 +36,16 @@ describe('sendStandardResponse', () => { 'x-custom-header': 'custom-value', }, options) + expect(toNodeHttpHeadersSpy).toBeCalledTimes(1) + expect(toNodeHttpHeadersSpy).toBeCalledWith({ + 'x-custom-header': 'custom-value', + }) + expect(endSpy).toBeCalledTimes(1) expect(endSpy).toBeCalledWith() expect(res.status).toBe(207) - expect(res.headers['content-type']).toEqual(undefined) + expect(res.headers).not.toHaveProperty('content-type') expect(res.headers['x-custom-header']).toEqual('custom-value') expect(res.text).toEqual('') @@ -66,6 +73,12 @@ describe('sendStandardResponse', () => { 'x-custom-header': 'custom-value', }, options) + expect(toNodeHttpHeadersSpy).toBeCalledTimes(1) + expect(toNodeHttpHeadersSpy).toBeCalledWith({ + 'content-type': 'application/json', + 'x-custom-header': 'custom-value', + }) + expect(endSpy).toBeCalledTimes(1) expect(endSpy).toBeCalledWith(toNodeHttpBodySpy.mock.results[0]!.value) @@ -103,6 +116,14 @@ describe('sendStandardResponse', () => { 'x-custom-header': 'custom-value', }, options) + expect(toNodeHttpHeadersSpy).toBeCalledTimes(1) + expect(toNodeHttpHeadersSpy).toBeCalledWith({ + 'content-disposition': 'inline; filename="blob"; filename*=utf-8\'\'blob', + 'content-length': '3', + 'content-type': 'text/plain', + 'x-custom-header': 'custom-value', + }) + expect(endSpy).toBeCalledTimes(1) expect(endSpy).toBeCalledWith() @@ -148,6 +169,12 @@ describe('sendStandardResponse', () => { 'x-custom-header': 'custom-value', }, options) + expect(toNodeHttpHeadersSpy).toBeCalledTimes(1) + expect(toNodeHttpHeadersSpy).toBeCalledWith({ + 'content-type': 'text/event-stream', + 'x-custom-header': 'custom-value', + }) + expect(endSpy).toBeCalledTimes(1) expect(endSpy).toBeCalledWith() diff --git a/packages/standard-server-node/src/response.ts b/packages/standard-server-node/src/response.ts index 4da1e6508..7509c9c96 100644 --- a/packages/standard-server-node/src/response.ts +++ b/packages/standard-server-node/src/response.ts @@ -2,6 +2,7 @@ import type { StandardHeaders, StandardResponse } from '@orpc/standard-server' import type { ToNodeHttpBodyOptions } from './body' import type { NodeHttpResponse } from './types' import { toNodeHttpBody } from './body' +import { toNodeHttpHeaders } from './headers' export interface SendStandardResponseOptions extends ToNodeHttpBodyOptions {} @@ -18,7 +19,9 @@ export function sendStandardResponse( const resBody = toNodeHttpBody(standardResponse.body, resHeaders, options) - res.writeHead(standardResponse.status, resHeaders) + // nodejs throws error when header is undefined, so remember to use toNodeHttpHeaders + // to filter out undefined headers + res.writeHead(standardResponse.status, toNodeHttpHeaders(resHeaders)) if (resBody === undefined) { res.end() From e75819a748601f436645160dc7516739c0197219 Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 2 Dec 2025 14:37:50 +0700 Subject: [PATCH 2/5] Update packages/standard-server-node/src/headers.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/standard-server-node/src/headers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/standard-server-node/src/headers.ts b/packages/standard-server-node/src/headers.ts index d99ec3ae1..fa40be997 100644 --- a/packages/standard-server-node/src/headers.ts +++ b/packages/standard-server-node/src/headers.ts @@ -6,7 +6,7 @@ export function toNodeHttpHeaders(headers: StandardHeaders): OutgoingHttpHeaders for (const key in headers) { const value = headers[key] - // nodejs not allow header is undefined + // nodejs does not allow headers to be undefined if (value !== undefined) { nodeHttpHeaders[key] = value } From 3cbcb550dc49d97c512dfec01ba98d63d1746918 Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 2 Dec 2025 14:38:04 +0700 Subject: [PATCH 3/5] Update packages/standard-server-fastify/src/response.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/standard-server-fastify/src/response.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/standard-server-fastify/src/response.ts b/packages/standard-server-fastify/src/response.ts index 07fd00fb8..d088e2471 100644 --- a/packages/standard-server-fastify/src/response.ts +++ b/packages/standard-server-fastify/src/response.ts @@ -19,7 +19,7 @@ export function sendStandardResponse( const resBody = toNodeHttpBody(standardResponse.body, resHeaders, options) reply.status(standardResponse.status) - // fastify treat undefined headers as empty string, so remember to use toNodeHttpHeaders + // Fastify treats undefined headers as empty string, so remember to use toNodeHttpHeaders // to filter out undefined headers reply.headers(toNodeHttpHeaders(resHeaders)) reply.send(resBody) From e9ac7751601dd125cd9cb0ccbde6b840089acf4d Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 2 Dec 2025 14:38:11 +0700 Subject: [PATCH 4/5] Update packages/standard-server-node/src/response.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/standard-server-node/src/response.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/standard-server-node/src/response.ts b/packages/standard-server-node/src/response.ts index 7509c9c96..0a59f7b7a 100644 --- a/packages/standard-server-node/src/response.ts +++ b/packages/standard-server-node/src/response.ts @@ -19,7 +19,7 @@ export function sendStandardResponse( const resBody = toNodeHttpBody(standardResponse.body, resHeaders, options) - // nodejs throws error when header is undefined, so remember to use toNodeHttpHeaders + // Node.js throws an error when a header is undefined, so remember to use toNodeHttpHeaders // to filter out undefined headers res.writeHead(standardResponse.status, toNodeHttpHeaders(resHeaders)) From 8bf8fa7a3805f85e4a464928b993912e5d1dd7de Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 2 Dec 2025 14:42:37 +0700 Subject: [PATCH 5/5] improve --- packages/standard-server-node/src/headers.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/standard-server-node/src/headers.ts b/packages/standard-server-node/src/headers.ts index fa40be997..c33459001 100644 --- a/packages/standard-server-node/src/headers.ts +++ b/packages/standard-server-node/src/headers.ts @@ -4,9 +4,8 @@ import type { OutgoingHttpHeaders } from 'node:http' export function toNodeHttpHeaders(headers: StandardHeaders): OutgoingHttpHeaders { const nodeHttpHeaders: OutgoingHttpHeaders = {} - for (const key in headers) { - const value = headers[key] - // nodejs does not allow headers to be undefined + for (const [key, value] of Object.entries(headers)) { + // Node.js does not allow headers to be undefined if (value !== undefined) { nodeHttpHeaders[key] = value }