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
2 changes: 1 addition & 1 deletion apps/content/docs/adapters/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ const link = new RPCLink({
}

const { headers } = await import('next/headers')
return Object.fromEntries(await headers())
return await headers()
},
})
```
Expand Down
2 changes: 1 addition & 1 deletion apps/content/docs/adapters/nuxt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof router> = createORPCClient(link)
Expand Down
2 changes: 1 addition & 1 deletion apps/content/docs/adapters/solid-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? {},
})
```

Expand Down
20 changes: 20 additions & 0 deletions packages/client/src/adapters/standard/rpc-link-codec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
14 changes: 7 additions & 7 deletions packages/client/src/adapters/standard/rpc-link-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends ClientContext> {
/**
Expand Down Expand Up @@ -39,7 +39,7 @@ export interface StandardRPCLinkCodecOptions<T extends ClientContext> {
/**
* Inject headers to the request.
*/
headers?: Value<Promisable<StandardHeaders>, [options: ClientOptions<T>, path: readonly string[], input: unknown]>
headers?: Value<Promisable<StandardHeaders | Headers>, [options: ClientOptions<T>, path: readonly string[], input: unknown]>
}

export class StandardRPCLinkCodec<T extends ClientContext> implements StandardLinkCodec<T> {
Expand All @@ -61,16 +61,16 @@ export class StandardRPCLinkCodec<T extends ClientContext> implements StandardLi
}

async encode(path: readonly string[], input: unknown, options: ClientOptions<T>): Promise<StandardRequest> {
let headers = toStandardHeaders(await value(this.headers, options, path, input))
if (options.lastEventId !== undefined) {
headers = mergeStandardHeaders(headers, { 'last-event-id': options.lastEventId })
}
Comment thread
dinwwwh marked this conversation as resolved.

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 (
Expand Down
12 changes: 11 additions & 1 deletion packages/client/src/adapters/standard/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { getMalformedResponseErrorCode, toHttpPath } from './utils'
import { getMalformedResponseErrorCode, toHttpPath, toStandardHeaders } from './utils'

it('convertPathToHttpPath', () => {
expect(toHttpPath(['ping'])).toEqual('/ping')
expect(toHttpPath(['nested', 'ping'])).toEqual('/nested/ping')
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')
Expand Down
14 changes: 14 additions & 0 deletions packages/client/src/adapters/standard/utils.ts
Original file line number Diff line number Diff line change
@@ -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'
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -24,7 +24,7 @@ export interface StandardOpenapiLinkCodecOptions<T extends ClientContext> {
/**
* Inject headers to the request.
*/
headers?: Value<Promisable<StandardHeaders>, [
headers?: Value<Promisable<StandardHeaders | Headers>, [
options: ClientOptions<T>,
path: readonly string[],
input: unknown,
Expand All @@ -45,13 +45,12 @@ export class StandardOpenapiLinkCodec<T extends ClientContext> implements Standa
}

async encode(path: readonly string[], input: unknown, options: ClientOptions<T>): Promise<StandardRequest> {
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))
Comment thread
dinwwwh marked this conversation as resolved.
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)) {
Expand Down
4 changes: 2 additions & 2 deletions packages/standard-server-fetch/src/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -15,7 +15,7 @@ export function toStandardHeaders(headers: Headers, standardHeaders: StandardHea
else {
standardHeaders[key] = value
}
}
})

return standardHeaders
}
Expand Down
2 changes: 1 addition & 1 deletion playgrounds/solid-start/src/lib/orpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof router> = globalThis.$client ?? createORPCClient(link)
Expand Down
Loading