From 57fb586371980e8230c2d5132ad20a87bf23b29e Mon Sep 17 00:00:00 2001 From: Paul Sachs Date: Thu, 4 Sep 2025 13:08:50 -0400 Subject: [PATCH 1/3] Add support for headers Adding headers to the query key and fetching operation Signed-off-by: Paul Sachs --- .../src/call-unary-method.ts | 3 +- .../src/connect-query-key.test.ts | 44 +++++++++++++++++- .../src/connect-query-key.ts | 24 ++++++++++ .../src/create-infinite-query-options.ts | 7 +++ .../src/create-query-options.test.ts | 6 +++ .../src/create-query-options.ts | 10 ++++ .../src/call-unary-method.test.ts | 32 +++++++++++++ .../src/use-infinite-query.test.ts | 46 +++++++++++++++++++ .../connect-query/src/use-infinite-query.ts | 3 ++ packages/connect-query/src/use-query.test.ts | 43 +++++++++++++++++ packages/connect-query/src/use-query.ts | 3 ++ packages/test-utils/src/index.tsx | 12 ++++- 12 files changed, 230 insertions(+), 3 deletions(-) diff --git a/packages/connect-query-core/src/call-unary-method.ts b/packages/connect-query-core/src/call-unary-method.ts index 4624a680..448e3b5b 100644 --- a/packages/connect-query-core/src/call-unary-method.ts +++ b/packages/connect-query-core/src/call-unary-method.ts @@ -34,13 +34,14 @@ export async function callUnaryMethod< input: MessageInitShape | undefined, options?: { signal?: AbortSignal; + headers?: HeadersInit; }, ): Promise> { const result = await transport.unary( schema, options?.signal, undefined, - undefined, + options?.headers, input ?? create(schema.input), undefined, ); diff --git a/packages/connect-query-core/src/connect-query-key.test.ts b/packages/connect-query-core/src/connect-query-key.test.ts index 857e9a06..c460f8de 100644 --- a/packages/connect-query-core/src/connect-query-key.test.ts +++ b/packages/connect-query-core/src/connect-query-key.test.ts @@ -167,7 +167,7 @@ describe("createConnectQueryKey", () => { }); sampleQueryClient.setQueryData( key, - create(SayResponseSchema, { sentence: "a proper value" }), + create(SayResponseSchema, { sentence: "a proper value" }) ); sampleQueryClient.setQueryData(key, (prev) => { @@ -178,6 +178,48 @@ describe("createConnectQueryKey", () => { }); }); + describe("headers", () => { + it("allows headers to be passed as an object", () => { + const key = createConnectQueryKey({ + schema: ElizaService.method.say, + input: create(SayRequestSchema, { sentence: "hi" }), + cardinality: "finite", + headers: { + "x-custom-header": "custom-value", + }, + }); + expect(key[1].headers).toEqual({ + "x-custom-header": "custom-value", + }); + }); + it("allows headers to be passed as a tuple", () => { + const key = createConnectQueryKey({ + schema: ElizaService.method.say, + input: create(SayRequestSchema, { sentence: "hi" }), + cardinality: "finite", + headers: [ + ["x-custom-header", "custom-value"], + ], + }); + expect(key[1].headers).toEqual({ + "x-custom-header": "custom-value", + }); + }); + it("allows headers to be passed as a HeadersInit", () => { + const key = createConnectQueryKey({ + schema: ElizaService.method.say, + input: create(SayRequestSchema, { sentence: "hi" }), + cardinality: "finite", + headers: new Headers({ + "x-custom-header": "custom-value", + }), + }); + expect(key[1].headers).toEqual({ + "x-custom-header": "custom-value", + }); + }); + }); + describe("infinite queries", () => { it("contains type hints to indicate the output type", () => { const sampleQueryClient = new QueryClient(); diff --git a/packages/connect-query-core/src/connect-query-key.ts b/packages/connect-query-core/src/connect-query-key.ts index 9f7623f3..cb9e9e3b 100644 --- a/packages/connect-query-core/src/connect-query-key.ts +++ b/packages/connect-query-core/src/connect-query-key.ts @@ -44,6 +44,10 @@ type SharedConnectQueryOptions = { * or "skipped". */ input?: Record | "skipped"; + /** + * Headers to be sent with the request. + */ + headers?: Record; }; type InfiniteConnectQueryKey = @@ -125,6 +129,10 @@ type KeyParamsForMethod = { * If omit the field with this name from the key for infinite queries. */ pageParamKey?: keyof MessageInitShape; + /** + * Set `headers` in the key. + */ + headers?: HeadersInit; }; type KeyParamsForService = { @@ -220,6 +228,7 @@ export function createConnectQueryKey< transport?: string; cardinality?: "finite" | "infinite"; input?: "skipped" | Record; + headers?: Record; } = params.schema.kind == "rpc" ? { @@ -246,5 +255,20 @@ export function createConnectQueryKey< ); } } + if (params.schema.kind === "rpc" && "headers" in params && params.headers !== undefined) { + props.headers = createHeadersKey(params.headers); + } return ["connect-query", props] as ConnectQueryKey; } + +/** + * Creates a record of headers from a HeadersInit object. + */ +function createHeadersKey(headers: HeadersInit): Record { + const result: Record = {}; + const arrayToIterate = Array.isArray(headers) || headers instanceof Headers ? headers : Object.entries(headers); + for (const [key, value] of arrayToIterate) { + result[key] = value; + } + return result; +} \ No newline at end of file diff --git a/packages/connect-query-core/src/create-infinite-query-options.ts b/packages/connect-query-core/src/create-infinite-query-options.ts index 176ef108..d7ebae43 100644 --- a/packages/connect-query-core/src/create-infinite-query-options.ts +++ b/packages/connect-query-core/src/create-infinite-query-options.ts @@ -49,6 +49,7 @@ export interface ConnectInfiniteQueryOptions< MessageInitShape[ParamKey], MessageShape >; + headers?: HeadersInit; } // eslint-disable-next-line @typescript-eslint/max-params -- we have 4 required arguments @@ -79,6 +80,7 @@ function createUnaryInfiniteQueryFn< }; return callUnaryMethod(transport, schema, inputCombinedWithPageParam, { signal: context.signal, + headers: context.queryKey[1].headers, }); }; } @@ -97,6 +99,7 @@ export function createInfiniteQueryOptions< transport, getNextPageParam, pageParamKey, + headers, }: ConnectInfiniteQueryOptions & { transport: Transport }, ): { getNextPageParam: ConnectInfiniteQueryOptions< @@ -124,6 +127,7 @@ export function createInfiniteQueryOptions< transport, getNextPageParam, pageParamKey, + headers, }: ConnectInfiniteQueryOptions & { transport: Transport }, ): { getNextPageParam: ConnectInfiniteQueryOptions< @@ -149,6 +153,7 @@ export function createInfiniteQueryOptions< transport, getNextPageParam, pageParamKey, + headers, }: ConnectInfiniteQueryOptions & { transport: Transport }, ): { getNextPageParam: ConnectInfiniteQueryOptions< @@ -180,6 +185,7 @@ export function createInfiniteQueryOptions< transport, getNextPageParam, pageParamKey, + headers, }: ConnectInfiniteQueryOptions & { transport: Transport }, ): { getNextPageParam: ConnectInfiniteQueryOptions< @@ -203,6 +209,7 @@ export function createInfiniteQueryOptions< schema, transport, input, + headers }); const structuralSharing = createStructuralSharing(schema.output); const queryFn = diff --git a/packages/connect-query-core/src/create-query-options.test.ts b/packages/connect-query-core/src/create-query-options.test.ts index c259715c..f57cfd29 100644 --- a/packages/connect-query-core/src/create-query-options.test.ts +++ b/packages/connect-query-core/src/create-query-options.test.ts @@ -48,12 +48,18 @@ describe("createQueryOptions", () => { input: { sentence: "hi" }, transport: mockedElizaTransport, cardinality: "finite", + headers: { + "x-custom-header": "custom-value", + } }); const opt = createQueryOptions( sayMethodDescriptor, { sentence: "hi" }, { transport: mockedElizaTransport, + headers: { + "x-custom-header": "custom-value", + }, }, ); expect(opt.queryKey).toStrictEqual(want); diff --git a/packages/connect-query-core/src/create-query-options.ts b/packages/connect-query-core/src/create-query-options.ts index 6b8b0e7e..cd31344b 100644 --- a/packages/connect-query-core/src/create-query-options.ts +++ b/packages/connect-query-core/src/create-query-options.ts @@ -36,6 +36,7 @@ function createUnaryQueryFn( return async (context) => { return callUnaryMethod(transport, schema, input, { signal: context.signal, + headers: context.queryKey[1].headers, }); }; } @@ -51,8 +52,10 @@ export function createQueryOptions< input: MessageInitShape | undefined, { transport, + headers, }: { transport: Transport; + headers?: HeadersInit; }, ): { queryKey: ConnectQueryKey; @@ -67,8 +70,10 @@ export function createQueryOptions< input: SkipToken, { transport, + headers, }: { transport: Transport; + headers?: HeadersInit; }, ): { queryKey: ConnectQueryKey; @@ -83,8 +88,10 @@ export function createQueryOptions< input: SkipToken | MessageInitShape | undefined, { transport, + headers, }: { transport: Transport; + headers?: HeadersInit; }, ): { queryKey: ConnectQueryKey; @@ -99,8 +106,10 @@ export function createQueryOptions< input: SkipToken | MessageInitShape | undefined, { transport, + headers, }: { transport: Transport; + headers?: HeadersInit; }, ): { queryKey: ConnectQueryKey; @@ -112,6 +121,7 @@ export function createQueryOptions< input: input ?? create(schema.input), transport, cardinality: "finite", + headers, }); const structuralSharing = createStructuralSharing(schema.output); const queryFn = diff --git a/packages/connect-query/src/call-unary-method.test.ts b/packages/connect-query/src/call-unary-method.test.ts index 5328eebb..ca49bbdd 100644 --- a/packages/connect-query/src/call-unary-method.test.ts +++ b/packages/connect-query/src/call-unary-method.test.ts @@ -72,4 +72,36 @@ describe("callUnaryMethod", () => { }); expect(result.current.query1.data?.sentence).toEqual("Response 1"); }); + it("can pass headers through", async () => { + let resolve: () => void; + const promise = new Promise((res) => { + resolve = res; + }); + const transport = mockEliza({ + sentence: "Response 1", + }, false, { + router: { + interceptors: [(next) => (req) => { + expect(req.header.get("x-custom-header")).toEqual("custom-value"); + resolve(); + return next(req); + }] + } + }); + const input: SayRequest = create(SayRequestSchema, { + sentence: "query 1", + }); + const res = await callUnaryMethod( + transport, + ElizaService.method.say, + input, + { + headers: { + "x-custom-header": "custom-value", + }, + }, + ); + await promise; + expect(res.sentence).toEqual("Response 1"); + }); }); diff --git a/packages/connect-query/src/use-infinite-query.test.ts b/packages/connect-query/src/use-infinite-query.test.ts index 0174961f..c7827c94 100644 --- a/packages/connect-query/src/use-infinite-query.test.ts +++ b/packages/connect-query/src/use-infinite-query.test.ts @@ -396,4 +396,50 @@ describe("useSuspenseInfiniteQuery", () => { wrapper({}, mockedPaginatedTransport), ); }); + + it("can pass headers through", async () => { + let resolve: () => void; + const promise = new Promise((res) => { + resolve = res; + }); + const transport = mockPaginatedTransport({ + items: ["Intercepted!"], + page: 0n, + }, false, { + router: { + interceptors: [(next) => (req) => { + expect(req.header.get("x-custom-header")).toEqual("custom-value"); + resolve(); + return next(req); + }] + } + }); + const { result } = renderHook( + () => { + return useSuspenseInfiniteQuery( + methodDescriptor, + { + page: 0n, + }, + { + getNextPageParam: (lastPage) => lastPage.page + 1n, + pageParamKey: "page", + transport, + headers: { + "x-custom-header": "custom-value", + }, + }, + ); + }, + wrapper({}), + ); + + await waitFor(() => { + expect(result.current.isSuccess).toBeTruthy(); + }); + + await promise; + + expect(result.current.data.pages[0].items).toEqual(["Intercepted!"]); + }); }); diff --git a/packages/connect-query/src/use-infinite-query.ts b/packages/connect-query/src/use-infinite-query.ts index e36bf82c..b2574ff6 100644 --- a/packages/connect-query/src/use-infinite-query.ts +++ b/packages/connect-query/src/use-infinite-query.ts @@ -85,6 +85,7 @@ export function useInfiniteQuery< transport: transport ?? transportFromCtx, getNextPageParam, pageParamKey, + }); return tsUseInfiniteQuery({ ...baseOptions, @@ -128,6 +129,7 @@ export function useSuspenseInfiniteQuery< transport, pageParamKey, getNextPageParam, + headers, ...queryOptions }: UseSuspenseInfiniteQueryOptions, ): UseSuspenseInfiniteQueryResult>, ConnectError> { @@ -136,6 +138,7 @@ export function useSuspenseInfiniteQuery< transport: transport ?? transportFromCtx, getNextPageParam, pageParamKey, + headers, }); return tsUseSuspenseInfiniteQuery({ ...baseOptions, diff --git a/packages/connect-query/src/use-query.test.ts b/packages/connect-query/src/use-query.test.ts index 3216cf81..8a5481e2 100644 --- a/packages/connect-query/src/use-query.test.ts +++ b/packages/connect-query/src/use-query.test.ts @@ -265,4 +265,47 @@ describe("useSuspenseQuery", () => { expect(result.current.data).toBe(11); }); + + it("can pass headers through", async () => { + let resolve: () => void; + const promise = new Promise((res) => { + resolve = res; + }); + const transport = mockEliza({ + sentence: "Response 1", + }, false, { + router: { + interceptors: [(next) => (req) => { + expect(req.header.get("x-custom-header")).toEqual("custom-value"); + resolve(); + return next(req); + }] + } + }); + const { result } = renderHook( + () => { + return useSuspenseQuery( + sayMethodDescriptor, + { + sentence: "hello", + }, + { + transport, + headers: { + "x-custom-header": "custom-value", + }, + }, + ); + }, + wrapper({}, mockedElizaTransport), + ); + + await waitFor(() => { + expect(result.current.isSuccess).toBeTruthy(); + }); + + await promise; + + expect(result.current.data.sentence).toBe("Response 1"); + }); }); diff --git a/packages/connect-query/src/use-query.ts b/packages/connect-query/src/use-query.ts index 3675af22..4c4a6137 100644 --- a/packages/connect-query/src/use-query.ts +++ b/packages/connect-query/src/use-query.ts @@ -95,6 +95,7 @@ export type UseSuspenseQueryOptions< > & { /** The transport to be used for the fetching. */ transport?: Transport; + headers?: HeadersInit; }; /** @@ -109,12 +110,14 @@ export function useSuspenseQuery< input?: MessageInitShape, { transport, + headers, ...queryOptions }: UseSuspenseQueryOptions = {}, ): UseSuspenseQueryResult { const transportFromCtx = useTransport(); const baseOptions = createQueryOptions(schema, input, { transport: transport ?? transportFromCtx, + headers, }); return tsUseSuspenseQuery({ ...baseOptions, diff --git a/packages/test-utils/src/index.tsx b/packages/test-utils/src/index.tsx index a577f9ef..ae19d980 100644 --- a/packages/test-utils/src/index.tsx +++ b/packages/test-utils/src/index.tsx @@ -14,7 +14,7 @@ import type { MessageInitShape } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf"; -import { createRouterTransport } from "@connectrpc/connect"; +import { createRouterTransport, type ConnectRouterOptions } from "@connectrpc/connect"; import { BigIntService, @@ -42,6 +42,9 @@ export const sleep = async (timeout: number) => export const mockEliza = ( override?: MessageInitShape, addDelay = false, + options?: { + router?: ConnectRouterOptions; + }, ) => createRouterTransport(({ service }) => { service(ElizaService, { @@ -55,6 +58,8 @@ export const mockEliza = ( ); }, }); + }, { + router: options?.router, }); /** @@ -93,6 +98,9 @@ export const mockStatefulBigIntTransport = (addDelay = false) => export const mockPaginatedTransport = ( override?: MessageInitShape, addDelay = false, + options?: { + router?: ConnectRouterOptions; + }, ) => createRouterTransport(({ service }) => { service(ListService, { @@ -115,4 +123,6 @@ export const mockPaginatedTransport = ( return result; }, }); + }, { + router: options?.router, }); From ebe7c3bf788d75470e3b79d9ca92d5cea4ae4e96 Mon Sep 17 00:00:00 2001 From: Paul Sachs Date: Thu, 4 Sep 2025 13:10:49 -0400 Subject: [PATCH 2/3] Format Signed-off-by: Paul Sachs --- .../src/connect-query-key.test.ts | 6 +- .../src/connect-query-key.ts | 13 ++- .../src/create-infinite-query-options.ts | 2 +- .../src/create-query-options.test.ts | 2 +- .../src/call-unary-method.test.ts | 28 +++--- .../src/use-infinite-query.test.ts | 61 +++++++------ .../connect-query/src/use-infinite-query.ts | 1 - packages/connect-query/src/use-query.test.ts | 28 +++--- packages/test-utils/src/index.tsx | 89 ++++++++++--------- 9 files changed, 129 insertions(+), 101 deletions(-) diff --git a/packages/connect-query-core/src/connect-query-key.test.ts b/packages/connect-query-core/src/connect-query-key.test.ts index c460f8de..625fc293 100644 --- a/packages/connect-query-core/src/connect-query-key.test.ts +++ b/packages/connect-query-core/src/connect-query-key.test.ts @@ -167,7 +167,7 @@ describe("createConnectQueryKey", () => { }); sampleQueryClient.setQueryData( key, - create(SayResponseSchema, { sentence: "a proper value" }) + create(SayResponseSchema, { sentence: "a proper value" }), ); sampleQueryClient.setQueryData(key, (prev) => { @@ -197,9 +197,7 @@ describe("createConnectQueryKey", () => { schema: ElizaService.method.say, input: create(SayRequestSchema, { sentence: "hi" }), cardinality: "finite", - headers: [ - ["x-custom-header", "custom-value"], - ], + headers: [["x-custom-header", "custom-value"]], }); expect(key[1].headers).toEqual({ "x-custom-header": "custom-value", diff --git a/packages/connect-query-core/src/connect-query-key.ts b/packages/connect-query-core/src/connect-query-key.ts index cb9e9e3b..2302012d 100644 --- a/packages/connect-query-core/src/connect-query-key.ts +++ b/packages/connect-query-core/src/connect-query-key.ts @@ -255,7 +255,11 @@ export function createConnectQueryKey< ); } } - if (params.schema.kind === "rpc" && "headers" in params && params.headers !== undefined) { + if ( + params.schema.kind === "rpc" && + "headers" in params && + params.headers !== undefined + ) { props.headers = createHeadersKey(params.headers); } return ["connect-query", props] as ConnectQueryKey; @@ -266,9 +270,12 @@ export function createConnectQueryKey< */ function createHeadersKey(headers: HeadersInit): Record { const result: Record = {}; - const arrayToIterate = Array.isArray(headers) || headers instanceof Headers ? headers : Object.entries(headers); + const arrayToIterate = + Array.isArray(headers) || headers instanceof Headers + ? headers + : Object.entries(headers); for (const [key, value] of arrayToIterate) { result[key] = value; } return result; -} \ No newline at end of file +} diff --git a/packages/connect-query-core/src/create-infinite-query-options.ts b/packages/connect-query-core/src/create-infinite-query-options.ts index d7ebae43..e1bb7d5a 100644 --- a/packages/connect-query-core/src/create-infinite-query-options.ts +++ b/packages/connect-query-core/src/create-infinite-query-options.ts @@ -209,7 +209,7 @@ export function createInfiniteQueryOptions< schema, transport, input, - headers + headers, }); const structuralSharing = createStructuralSharing(schema.output); const queryFn = diff --git a/packages/connect-query-core/src/create-query-options.test.ts b/packages/connect-query-core/src/create-query-options.test.ts index f57cfd29..8ec06969 100644 --- a/packages/connect-query-core/src/create-query-options.test.ts +++ b/packages/connect-query-core/src/create-query-options.test.ts @@ -50,7 +50,7 @@ describe("createQueryOptions", () => { cardinality: "finite", headers: { "x-custom-header": "custom-value", - } + }, }); const opt = createQueryOptions( sayMethodDescriptor, diff --git a/packages/connect-query/src/call-unary-method.test.ts b/packages/connect-query/src/call-unary-method.test.ts index ca49bbdd..457aa8aa 100644 --- a/packages/connect-query/src/call-unary-method.test.ts +++ b/packages/connect-query/src/call-unary-method.test.ts @@ -77,17 +77,23 @@ describe("callUnaryMethod", () => { const promise = new Promise((res) => { resolve = res; }); - const transport = mockEliza({ - sentence: "Response 1", - }, false, { - router: { - interceptors: [(next) => (req) => { - expect(req.header.get("x-custom-header")).toEqual("custom-value"); - resolve(); - return next(req); - }] - } - }); + const transport = mockEliza( + { + sentence: "Response 1", + }, + false, + { + router: { + interceptors: [ + (next) => (req) => { + expect(req.header.get("x-custom-header")).toEqual("custom-value"); + resolve(); + return next(req); + }, + ], + }, + }, + ); const input: SayRequest = create(SayRequestSchema, { sentence: "query 1", }); diff --git a/packages/connect-query/src/use-infinite-query.test.ts b/packages/connect-query/src/use-infinite-query.test.ts index c7827c94..3930d79f 100644 --- a/packages/connect-query/src/use-infinite-query.test.ts +++ b/packages/connect-query/src/use-infinite-query.test.ts @@ -402,38 +402,41 @@ describe("useSuspenseInfiniteQuery", () => { const promise = new Promise((res) => { resolve = res; }); - const transport = mockPaginatedTransport({ - items: ["Intercepted!"], - page: 0n, - }, false, { - router: { - interceptors: [(next) => (req) => { - expect(req.header.get("x-custom-header")).toEqual("custom-value"); - resolve(); - return next(req); - }] - } - }); - const { result } = renderHook( - () => { - return useSuspenseInfiniteQuery( - methodDescriptor, - { - page: 0n, - }, - { - getNextPageParam: (lastPage) => lastPage.page + 1n, - pageParamKey: "page", - transport, - headers: { - "x-custom-header": "custom-value", + const transport = mockPaginatedTransport( + { + items: ["Intercepted!"], + page: 0n, + }, + false, + { + router: { + interceptors: [ + (next) => (req) => { + expect(req.header.get("x-custom-header")).toEqual("custom-value"); + resolve(); + return next(req); }, - }, - ); + ], + }, }, - wrapper({}), ); - + const { result } = renderHook(() => { + return useSuspenseInfiniteQuery( + methodDescriptor, + { + page: 0n, + }, + { + getNextPageParam: (lastPage) => lastPage.page + 1n, + pageParamKey: "page", + transport, + headers: { + "x-custom-header": "custom-value", + }, + }, + ); + }, wrapper({})); + await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); diff --git a/packages/connect-query/src/use-infinite-query.ts b/packages/connect-query/src/use-infinite-query.ts index b2574ff6..0fdfd92e 100644 --- a/packages/connect-query/src/use-infinite-query.ts +++ b/packages/connect-query/src/use-infinite-query.ts @@ -85,7 +85,6 @@ export function useInfiniteQuery< transport: transport ?? transportFromCtx, getNextPageParam, pageParamKey, - }); return tsUseInfiniteQuery({ ...baseOptions, diff --git a/packages/connect-query/src/use-query.test.ts b/packages/connect-query/src/use-query.test.ts index 8a5481e2..5ab4b707 100644 --- a/packages/connect-query/src/use-query.test.ts +++ b/packages/connect-query/src/use-query.test.ts @@ -271,17 +271,23 @@ describe("useSuspenseQuery", () => { const promise = new Promise((res) => { resolve = res; }); - const transport = mockEliza({ - sentence: "Response 1", - }, false, { - router: { - interceptors: [(next) => (req) => { - expect(req.header.get("x-custom-header")).toEqual("custom-value"); - resolve(); - return next(req); - }] - } - }); + const transport = mockEliza( + { + sentence: "Response 1", + }, + false, + { + router: { + interceptors: [ + (next) => (req) => { + expect(req.header.get("x-custom-header")).toEqual("custom-value"); + resolve(); + return next(req); + }, + ], + }, + }, + ); const { result } = renderHook( () => { return useSuspenseQuery( diff --git a/packages/test-utils/src/index.tsx b/packages/test-utils/src/index.tsx index ae19d980..0e4a80b5 100644 --- a/packages/test-utils/src/index.tsx +++ b/packages/test-utils/src/index.tsx @@ -14,7 +14,10 @@ import type { MessageInitShape } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf"; -import { createRouterTransport, type ConnectRouterOptions } from "@connectrpc/connect"; +import { + createRouterTransport, + type ConnectRouterOptions, +} from "@connectrpc/connect"; import { BigIntService, @@ -46,21 +49,24 @@ export const mockEliza = ( router?: ConnectRouterOptions; }, ) => - createRouterTransport(({ service }) => { - service(ElizaService, { - say: async (input: SayRequest) => { - if (addDelay) { - await sleep(1000); - } - return create( - SayResponseSchema, - override ?? { sentence: `Hello ${input.sentence}` }, - ); - }, - }); - }, { - router: options?.router, - }); + createRouterTransport( + ({ service }) => { + service(ElizaService, { + say: async (input: SayRequest) => { + if (addDelay) { + await sleep(1000); + } + return create( + SayResponseSchema, + override ?? { sentence: `Hello ${input.sentence}` }, + ); + }, + }); + }, + { + router: options?.router, + }, + ); /** * a stateless mock for BigIntService @@ -102,27 +108,30 @@ export const mockPaginatedTransport = ( router?: ConnectRouterOptions; }, ) => - createRouterTransport(({ service }) => { - service(ListService, { - list: async (request) => { - if (addDelay) { - await sleep(1000); - } - if (override !== undefined) { - return override; - } - const base = (request.page - 1n) * 3n; - const result = { - page: request.page, - items: [ - `${base + 1n} Item`, - `${base + 2n} Item`, - `${base + 3n} Item`, - ], - }; - return result; - }, - }); - }, { - router: options?.router, - }); + createRouterTransport( + ({ service }) => { + service(ListService, { + list: async (request) => { + if (addDelay) { + await sleep(1000); + } + if (override !== undefined) { + return override; + } + const base = (request.page - 1n) * 3n; + const result = { + page: request.page, + items: [ + `${base + 1n} Item`, + `${base + 2n} Item`, + `${base + 3n} Item`, + ], + }; + return result; + }, + }); + }, + { + router: options?.router, + }, + ); From 58fb927304cd2b6d48b5aee3531e2e9d08b86a2f Mon Sep 17 00:00:00 2001 From: Paul Sachs Date: Mon, 8 Sep 2025 10:32:17 -0400 Subject: [PATCH 3/3] Normalize header keys Signed-off-by: Paul Sachs --- .../src/connect-query-key.test.ts | 31 +++++++++++++++++++ .../src/connect-query-key.ts | 10 +++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/connect-query-core/src/connect-query-key.test.ts b/packages/connect-query-core/src/connect-query-key.test.ts index 625fc293..888fdf4f 100644 --- a/packages/connect-query-core/src/connect-query-key.test.ts +++ b/packages/connect-query-core/src/connect-query-key.test.ts @@ -216,6 +216,37 @@ describe("createConnectQueryKey", () => { "x-custom-header": "custom-value", }); }); + it("normalizes header values", () => { + const keyA = createConnectQueryKey({ + schema: ElizaService.method.say, + input: create(SayRequestSchema, { sentence: "hi" }), + cardinality: "finite", + headers: { + foo: "a", + Foo: "b", + }, + }); + const keyB = createConnectQueryKey({ + schema: ElizaService.method.say, + input: create(SayRequestSchema, { sentence: "hi" }), + cardinality: "finite", + headers: { + foo: "a, b", + }, + }); + const keyC = createConnectQueryKey({ + schema: ElizaService.method.say, + input: create(SayRequestSchema, { sentence: "hi" }), + cardinality: "finite", + headers: [ + ["foo", "a"], + ["foo", "b"], + ], + }); + + expect(keyA[1].headers).toEqual(keyB[1].headers); + expect(keyA[1].headers).toEqual(keyC[1].headers); + }); }); describe("infinite queries", () => { diff --git a/packages/connect-query-core/src/connect-query-key.ts b/packages/connect-query-core/src/connect-query-key.ts index 2302012d..75504d22 100644 --- a/packages/connect-query-core/src/connect-query-key.ts +++ b/packages/connect-query-core/src/connect-query-key.ts @@ -46,6 +46,7 @@ type SharedConnectQueryOptions = { input?: Record | "skipped"; /** * Headers to be sent with the request. + * Note that invalid HTTP header names will raise a TypeError, and that the Set-Cookie header is not supported. */ headers?: Record; }; @@ -131,6 +132,7 @@ type KeyParamsForMethod = { pageParamKey?: keyof MessageInitShape; /** * Set `headers` in the key. + * Note that invalid HTTP header names will raise a TypeError, and that the Set-Cookie header is not supported. */ headers?: HeadersInit; }; @@ -267,14 +269,12 @@ export function createConnectQueryKey< /** * Creates a record of headers from a HeadersInit object. + * */ function createHeadersKey(headers: HeadersInit): Record { const result: Record = {}; - const arrayToIterate = - Array.isArray(headers) || headers instanceof Headers - ? headers - : Object.entries(headers); - for (const [key, value] of arrayToIterate) { + + for (const [key, value] of new Headers(headers)) { result[key] = value; } return result;