Skip to content

Commit c4e72cc

Browse files
committed
fix(client-nuxt): support reactive headers
Signed-off-by: Liam Stanley <liam@liam.sh>
1 parent 7045d52 commit c4e72cc

5 files changed

Lines changed: 66 additions & 24 deletions

File tree

packages/client-nuxt/src/__tests__/client.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,6 @@ describe('authentication', () => {
458458

459459
const result = await client.get<'useFetch', http.VerboseResponse>({
460460
composable: 'useFetch',
461-
query: { liam: 1 },
462461
security: [
463462
{
464463
name: 'baz',

packages/client-nuxt/src/__tests__/reactive.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,32 @@ test('reactive body re-triggers POST on body ref update', async () => {
152152
expect(result.data.value?.species).toEqual(newPet.value.species);
153153
expect(server.spy('post', '/pets')).toHaveBeenCalledTimes(2);
154154
});
155+
156+
test('reactive headers re-triggers GET on header ref update', async () => {
157+
const client = createClient({ baseURL: 'https://localhost' });
158+
const server = http.newServer([http.mockVerboseHandler('https://localhost')]);
159+
160+
const species = ref(
161+
new Headers({
162+
'X-Example-Header': 'example',
163+
}),
164+
);
165+
166+
const result = await client.get<'useFetch', http.VerboseResponse>({
167+
composable: 'useFetch',
168+
headers: species,
169+
url: '/verbose',
170+
});
171+
172+
const received = new Headers(result.data.value?.headers);
173+
expect(received.get('X-Example-Header')).toEqual('example');
174+
175+
species.value = new Headers({
176+
'X-Example-Header': 'example2',
177+
});
178+
await waitStatusFinished(result.status);
179+
const received2 = new Headers(result.data.value?.headers);
180+
expect(received2.get('X-Example-Header')).toEqual('example2');
181+
182+
server.expectAllCalled();
183+
});

packages/client-nuxt/src/client.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
mergeConfigs,
1414
mergeHeaders,
1515
mergeInterceptors,
16+
removeNullHeaders,
1617
serializeBody,
1718
} from './utils';
1819

@@ -37,18 +38,27 @@ export const createClient = (config: Config = {}): Client => {
3738
// because Nuxt composables control when the request is sent (or re-sent) -- we don't invoke the
3839
// request here. if we do logic outside of interceptors, it has the potential to become stale
3940
// or cause hydration errors.
41+
4042
const opts = {
4143
..._config,
4244
...options,
4345
$fetch: options.$fetch ?? _config.$fetch ?? $fetch,
44-
headers: mergeHeaders(_config.headers, options.headers),
46+
// headers here can be ignored, just here to make sure nuxt keeps reactivity. all headers
47+
// will be merged in interceptors.
48+
headers: removeNullHeaders(options.headers),
4549
onRequest: mergeInterceptors(
4650
({ options: req }) =>
47-
(req.body = serializeBody({
51+
(req.headers = mergeHeaders(
52+
_config.headers,
53+
req.headers,
54+
options.headers,
55+
)),
56+
({ options: req }) => {
57+
req.body = serializeBody({
4858
...options,
4959
bodySerializer: options.bodySerializer ?? _config.bodySerializer,
50-
})),
51-
({ options: req }) => {
60+
});
61+
5262
// remove Content-Type header if body is empty to avoid sending invalid requests
5363
if (req.body === undefined || req.body === '') {
5464
req.headers.delete('Content-Type');

packages/client-nuxt/src/types.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
useLazyAsyncData,
1313
useLazyFetch,
1414
} from 'nuxt/app';
15-
import type { MaybeRefOrGetter } from 'vue';
15+
import type { MaybeRef, MaybeRefOrGetter } from 'vue';
1616

1717
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
1818
type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
@@ -24,6 +24,11 @@ export type QuerySerializer = (
2424
query: Parameters<Client['buildUrl']>[0]['query'],
2525
) => string;
2626

27+
export type FetchHeaders =
28+
| HeadersInit
29+
| MaybeRef<HeadersInit>
30+
| Record<string, unknown>;
31+
2732
/**
2833
* KeysOf, copied from Nuxt, is used depending on the composable to "pick" the keys
2934
* on the return type, so only a sub-set of the data is returned via hydration,
@@ -43,7 +48,7 @@ export interface Config<T extends ClientOptions = ClientOptions>
4348
* Base URL for all requests made by this client.
4449
*/
4550
baseURL?: T['baseURL'];
46-
headers?: FetchOptions['headers'];
51+
headers?: FetchHeaders;
4752
/**
4853
* A function for serializing request query parameters. By default, arrays
4954
* will be exploded in form style, objects will be exploded in deepObject
@@ -60,15 +65,14 @@ export interface RequestOptions<
6065
DefaultT = undefined,
6166
Url extends string = string,
6267
> extends Config,
63-
Pick<RefTDataShape<TDataShape>, 'body' | 'path' | 'query'> {
68+
Pick<RefTDataShape<TDataShape>, 'body' | 'path' | 'query' | 'headers'> {
6469
asyncDataOptions?: AsyncDataOptions<ResT, ResT, KeysOf<ResT>, DefaultT>;
6570
/**
6671
* Any body that you want to add to your request.
6772
*
6873
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
6974
*/
7075
composable: TComposable;
71-
headers?: FetchOptions['headers'];
7276
key?: string;
7377
/**
7478
* Security mechanism(s) to use for the request.
@@ -110,12 +114,7 @@ type MethodFn = <
110114
TError = unknown,
111115
DefaultT = undefined,
112116
>(
113-
options: Omit<
114-
RequestOptions<TComposable, ResT, DefaultT>,
115-
'method' | 'headers'
116-
> & {
117-
headers?: FetchOptions['headers'];
118-
},
117+
options: Omit<RequestOptions<TComposable, ResT, DefaultT>, 'method'>,
119118
) => RequestResult<TComposable, ResT, TError>;
120119

121120
type RequestFn = <
@@ -142,7 +141,7 @@ export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
142141

143142
export interface TDataShape {
144143
body?: FetchOptions['body'];
145-
headers?: unknown;
144+
headers?: FetchHeaders;
146145
path?: FetchOptions['query'];
147146
query?: FetchOptions['query'];
148147
url: string;
@@ -182,11 +181,9 @@ export type Options<
182181
DefaultT = undefined,
183182
> = Omit<
184183
RequestOptions<TComposable, ResT, DefaultT>,
185-
'body' | 'url' | 'query' | 'path'
184+
'body' | 'url' | 'query' | 'path' | 'headers'
186185
> &
187-
Pick<RefTDataShape<TData>, 'body' | 'query' | 'path'> & {
188-
headers?: FetchOptions['headers'];
189-
};
186+
Pick<RefTDataShape<TData>, 'body' | 'query' | 'path' | 'headers'>;
190187

191188
export type OptionsLegacyParser<TData = unknown> = TData extends { body?: any }
192189
? TData extends { headers?: any }

packages/client-nuxt/src/utils.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
serializeObjectParam,
77
serializePrimitiveParam,
88
} from '@hey-api/client-core';
9-
import { toValue } from 'vue';
9+
import { isRef, toValue } from 'vue';
1010

1111
import type {
1212
ArraySeparatorStyle,
@@ -235,9 +235,7 @@ export const mergeConfigs = (a: Config, b: Config): Config => {
235235
* @returns A new Headers object with the merged headers.
236236
*/
237237
export const mergeHeaders = (
238-
...headers: Array<
239-
RequestOptions['headers'] | Record<string, unknown> | undefined
240-
>
238+
...headers: Array<RequestOptions['headers'] | undefined>
241239
): Headers => {
242240
const mergedHeaders = new Headers();
243241

@@ -273,6 +271,15 @@ export const mergeHeaders = (
273271
return mergedHeaders;
274272
};
275273

274+
/**
275+
* removeNullHeaders removes null values from headers, which are accepted to allow
276+
* unsetting a previously set header.
277+
*/
278+
export const removeNullHeaders = (
279+
headers: RequestOptions['headers'],
280+
): Exclude<RequestOptions['headers'], Record<string, unknown>> =>
281+
isRef(headers) ? headers : mergeHeaders(headers);
282+
276283
export const mergeInterceptors = <T>(...args: Array<T | T[]>): Array<T> =>
277284
args.reduce<Array<T>>((acc, item) => {
278285
if (typeof item === 'function') {

0 commit comments

Comments
 (0)