diff --git a/apps/content/docs/adapters/next.md b/apps/content/docs/adapters/next.md index 5c9a801d1..0cc422b16 100644 --- a/apps/content/docs/adapters/next.md +++ b/apps/content/docs/adapters/next.md @@ -102,7 +102,7 @@ const link = new RPCLink({ } const { headers } = await import('next/headers') - return Object.fromEntries(await headers()) + return await headers() }, }) ``` diff --git a/apps/content/docs/adapters/nuxt.md b/apps/content/docs/adapters/nuxt.md index 313fa47d7..a3b3d3f0b 100644 --- a/apps/content/docs/adapters/nuxt.md +++ b/apps/content/docs/adapters/nuxt.md @@ -57,7 +57,7 @@ export default defineNuxtPlugin(() => { const link = new RPCLink({ url: `${typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000'}/rpc`, - headers: () => Object.fromEntries(event?.headers ?? []), + headers: event?.headers, }) const client: RouterClient = createORPCClient(link) diff --git a/apps/content/docs/adapters/solid-start.md b/apps/content/docs/adapters/solid-start.md index fe2618961..eb91652ee 100644 --- a/apps/content/docs/adapters/solid-start.md +++ b/apps/content/docs/adapters/solid-start.md @@ -61,7 +61,7 @@ import { getRequestEvent } from 'solid-js/web' const link = new RPCLink({ url: `${typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000'}/rpc`, - headers: () => Object.fromEntries(getRequestEvent()?.request.headers ?? []), + headers: () => getRequestEvent()?.request.headers ?? {}, }) ``` diff --git a/packages/client/src/adapters/standard/rpc-link-codec.test.ts b/packages/client/src/adapters/standard/rpc-link-codec.test.ts index 752d9aa66..9f10cb7bb 100644 --- a/packages/client/src/adapters/standard/rpc-link-codec.test.ts +++ b/packages/client/src/adapters/standard/rpc-link-codec.test.ts @@ -104,6 +104,26 @@ describe('standardRPCLinkCodec', () => { expect(request.headers).toBe(mergeStandardHeadersSpy.mock.results[0]!.value) }) + it('support fetch headers', async () => { + const headers = new Headers() + headers.append('cookie', 'a=1') + headers.append('cookie', 'b=2') + headers.append('set-cookie', 'a1=1') + headers.append('set-cookie', 'b1=2') + + const codec = new StandardRPCLinkCodec(serializer, { + url: 'http://localhost:3000', + headers, + }) + + const request = await codec.encode(['test'], 'input', { context: {} }) + + expect(request.headers).toEqual({ + 'cookie': 'a=1; b=2', + 'set-cookie': ['a1=1', 'b1=2'], + }) + }) + describe('base url', () => { it('works with /prefix', async () => { const codec = new StandardRPCLinkCodec(serializer, { diff --git a/packages/client/src/adapters/standard/rpc-link-codec.ts b/packages/client/src/adapters/standard/rpc-link-codec.ts index a4b93bc7b..ef796420f 100644 --- a/packages/client/src/adapters/standard/rpc-link-codec.ts +++ b/packages/client/src/adapters/standard/rpc-link-codec.ts @@ -6,7 +6,7 @@ import type { StandardLinkCodec } from './types' import { isAsyncIteratorObject, stringifyJSON, value } from '@orpc/shared' import { mergeStandardHeaders } from '@orpc/standard-server' import { createORPCErrorFromJson, isORPCErrorJson, isORPCErrorStatus, ORPCError } from '../../error' -import { getMalformedResponseErrorCode, toHttpPath } from './utils' +import { getMalformedResponseErrorCode, toHttpPath, toStandardHeaders } from './utils' export interface StandardRPCLinkCodecOptions { /** @@ -39,7 +39,7 @@ export interface StandardRPCLinkCodecOptions { /** * Inject headers to the request. */ - headers?: Value, [options: ClientOptions, path: readonly string[], input: unknown]> + headers?: Value, [options: ClientOptions, path: readonly string[], input: unknown]> } export class StandardRPCLinkCodec implements StandardLinkCodec { @@ -61,16 +61,16 @@ export class StandardRPCLinkCodec implements StandardLi } async encode(path: readonly string[], input: unknown, options: ClientOptions): Promise { + let headers = toStandardHeaders(await value(this.headers, options, path, input)) + if (options.lastEventId !== undefined) { + headers = mergeStandardHeaders(headers, { 'last-event-id': options.lastEventId }) + } + const expectedMethod = await value(this.expectedMethod, options, path, input) - let headers = await value(this.headers, options, path, input) const baseUrl = await value(this.baseUrl, options, path, input) const url = new URL(baseUrl) url.pathname = `${url.pathname.replace(/\/$/, '')}${toHttpPath(path)}` - if (options.lastEventId !== undefined) { - headers = mergeStandardHeaders(headers, { 'last-event-id': options.lastEventId }) - } - const serialized = this.serializer.serialize(input) if ( diff --git a/packages/client/src/adapters/standard/utils.test.ts b/packages/client/src/adapters/standard/utils.test.ts index 65dfb34a2..0de7d87bd 100644 --- a/packages/client/src/adapters/standard/utils.test.ts +++ b/packages/client/src/adapters/standard/utils.test.ts @@ -1,4 +1,4 @@ -import { getMalformedResponseErrorCode, toHttpPath } from './utils' +import { getMalformedResponseErrorCode, toHttpPath, toStandardHeaders } from './utils' it('convertPathToHttpPath', () => { expect(toHttpPath(['ping'])).toEqual('/ping') @@ -6,6 +6,16 @@ it('convertPathToHttpPath', () => { expect(toHttpPath(['nested/', 'ping'])).toEqual('/nested%2F/ping') }) +it('toStandardHeaders', () => { + expect(toStandardHeaders({})).toEqual({}) + expect(toStandardHeaders({ 'content-type': 'application/json' })).toEqual({ 'content-type': 'application/json' }) + + expect(toStandardHeaders(new Headers())).toEqual({}) + const headers = new Headers({ 'content-type': 'application/json' }) + expect(toStandardHeaders(headers)).toEqual({ 'content-type': 'application/json' }) + expect(toStandardHeaders({ forEach: headers.forEach.bind(headers) } as any)).toEqual({ 'content-type': 'application/json' }) +}) + it('getMalformedResponseErrorCode', () => { expect(getMalformedResponseErrorCode(400)).toEqual('BAD_REQUEST') expect(getMalformedResponseErrorCode(401)).toEqual('UNAUTHORIZED') diff --git a/packages/client/src/adapters/standard/utils.ts b/packages/client/src/adapters/standard/utils.ts index 4f133af4b..c3b7869bd 100644 --- a/packages/client/src/adapters/standard/utils.ts +++ b/packages/client/src/adapters/standard/utils.ts @@ -1,10 +1,24 @@ +import type { StandardHeaders } from '@orpc/standard-server' import type { HTTPPath } from '../../types' +import { toStandardHeaders as fetchHeadersToStandardHeaders } from '@orpc/standard-server-fetch' import { COMMON_ORPC_ERROR_DEFS } from '../../error' export function toHttpPath(path: readonly string[]): HTTPPath { return `/${path.map(encodeURIComponent).join('/')}` } +export function toStandardHeaders(headers: Headers | StandardHeaders): StandardHeaders { + /** + * Determines if the provided `headers` is a headers-like object. + * Avoids `instanceof` checks as this is intended for standard APIs where the Headers constructor may not be available. + */ + if (typeof headers.forEach === 'function') { + return fetchHeadersToStandardHeaders(headers as Headers) + } + + return headers as StandardHeaders +} + export function getMalformedResponseErrorCode(status: number): string { return Object.entries(COMMON_ORPC_ERROR_DEFS).find(([, def]) => def.status === status)?.[0] ?? 'MALFORMED_ORPC_ERROR_RESPONSE' } diff --git a/packages/openapi-client/src/adapters/standard/openapi-link-codec.test.ts b/packages/openapi-client/src/adapters/standard/openapi-link-codec.test.ts index 4a426bd11..601cc5405 100644 --- a/packages/openapi-client/src/adapters/standard/openapi-link-codec.test.ts +++ b/packages/openapi-client/src/adapters/standard/openapi-link-codec.test.ts @@ -50,6 +50,26 @@ describe('standardOpenapiLinkCodecOptions', () => { expect(request.headers['x-custom']).toEqual('value') }) + it('support fetch headers', async () => { + const headers = new Headers() + headers.append('cookie', 'a=1') + headers.append('cookie', 'b=2') + headers.append('set-cookie', 'a1=1') + headers.append('set-cookie', 'b1=2') + + const codec = new StandardOpenapiLinkCodec({ ping: oc }, serializer, { + url: 'http://localhost:3000', + headers, + }) + + const request = await codec.encode(['ping'], 'input', { context: {} }) + + expect(request.headers).toEqual({ + 'cookie': 'a=1; b=2', + 'set-cookie': ['a1=1', 'b1=2'], + }) + }) + describe('inputStructure=compact', () => { describe('with dynamic params', () => { const codec = new StandardOpenapiLinkCodec({ ping: oc.route({ path: '/ping/{date}' }) }, serializer, { diff --git a/packages/openapi-client/src/adapters/standard/openapi-link-codec.ts b/packages/openapi-client/src/adapters/standard/openapi-link-codec.ts index 68b5c997c..363898a95 100644 --- a/packages/openapi-client/src/adapters/standard/openapi-link-codec.ts +++ b/packages/openapi-client/src/adapters/standard/openapi-link-codec.ts @@ -5,7 +5,7 @@ import type { Promisable, Value } from '@orpc/shared' import type { StandardHeaders, StandardLazyResponse, StandardRequest, StandardResponse } from '@orpc/standard-server' import type { StandardOpenAPISerializer } from './openapi-serializer' import { createORPCErrorFromJson, isORPCErrorJson, isORPCErrorStatus } from '@orpc/client' -import { getMalformedResponseErrorCode, toHttpPath } from '@orpc/client/standard' +import { getMalformedResponseErrorCode, toHttpPath, toStandardHeaders } from '@orpc/client/standard' import { fallbackContractConfig, isContractProcedure, ORPCError } from '@orpc/contract' import { get, isObject, value } from '@orpc/shared' import { mergeStandardHeaders } from '@orpc/standard-server' @@ -24,7 +24,7 @@ export interface StandardOpenapiLinkCodecOptions { /** * Inject headers to the request. */ - headers?: Value, [ + headers?: Value, [ options: ClientOptions, path: readonly string[], input: unknown, @@ -45,13 +45,12 @@ export class StandardOpenapiLinkCodec implements Standa } async encode(path: readonly string[], input: unknown, options: ClientOptions): Promise { - const baseUrl = await value(this.baseUrl, options, path, input) - let headers = await value(this.headers, options, path, input) - + let headers = toStandardHeaders(await value(this.headers, options, path, input)) if (options.lastEventId !== undefined) { headers = mergeStandardHeaders(headers, { 'last-event-id': options.lastEventId }) } + const baseUrl = await value(this.baseUrl, options, path, input) const procedure = get(this.contract, path) if (!isContractProcedure(procedure)) { diff --git a/packages/standard-server-fetch/src/headers.ts b/packages/standard-server-fetch/src/headers.ts index 18283b1f6..23cd3efcc 100644 --- a/packages/standard-server-fetch/src/headers.ts +++ b/packages/standard-server-fetch/src/headers.ts @@ -5,7 +5,7 @@ import type { StandardHeaders } from '@orpc/standard-server' * @param standardHeaders - The base headers can be changed by the function and effects on the original headers. */ export function toStandardHeaders(headers: Headers, standardHeaders: StandardHeaders = {}): StandardHeaders { - for (const [key, value] of headers) { + headers.forEach((value, key) => { if (Array.isArray(standardHeaders[key])) { standardHeaders[key].push(value) } @@ -15,7 +15,7 @@ export function toStandardHeaders(headers: Headers, standardHeaders: StandardHea else { standardHeaders[key] = value } - } + }) return standardHeaders } diff --git a/playgrounds/solid-start/src/lib/orpc.ts b/playgrounds/solid-start/src/lib/orpc.ts index fb4a3bed6..32be646d0 100644 --- a/playgrounds/solid-start/src/lib/orpc.ts +++ b/playgrounds/solid-start/src/lib/orpc.ts @@ -15,7 +15,7 @@ declare global { const link = new RPCLink({ url: `${typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000'}/rpc`, - headers: () => Object.fromEntries(getRequestEvent()?.request.headers ?? []), + headers: () => getRequestEvent()?.request.headers ?? {}, }) export const client: RouterClient = globalThis.$client ?? createORPCClient(link)