Skip to content

Commit 3e29c4b

Browse files
msutkowskiphryneas
andauthored
Allow endpoints to have optional/undefined headers (#108)
* Allow more flexible headers that can be undefined * Adds doc blocks for JS users to `fetchBaseQuery` Co-authored-by: Lenz Weber <[email protected]>
1 parent 0875c04 commit 3e29c4b

File tree

2 files changed

+96
-6
lines changed

2 files changed

+96
-6
lines changed

src/fetchBaseQuery.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { joinUrls } from './utils';
22
import { isPlainObject } from '@reduxjs/toolkit';
33
import { BaseQueryFn } from './apiTypes';
4+
import { Override } from './tsHelpers';
45

56
export type ResponseHandler = 'json' | 'text' | ((response: Response) => Promise<any>);
67

7-
export interface FetchArgs extends RequestInit {
8+
type CustomRequestInit = Override<
9+
RequestInit,
10+
{ headers?: Headers | string[][] | Record<string, string | undefined> | undefined }
11+
>;
12+
13+
export interface FetchArgs extends CustomRequestInit {
814
url: string;
915
params?: Record<string, any>;
1016
body?: any;
@@ -36,6 +42,36 @@ export interface FetchBaseQueryError {
3642
data: unknown;
3743
}
3844

45+
function cleanUndefinedHeaders(headers: any) {
46+
if (!isPlainObject(headers)) {
47+
return headers;
48+
}
49+
const copy: Record<string, any> = { ...headers };
50+
for (const [k, v] of Object.entries(copy)) {
51+
if (typeof v === 'undefined') delete copy[k];
52+
}
53+
return copy;
54+
}
55+
56+
/**
57+
* This is a very small wrapper around fetch that aims to simplify requests.
58+
*
59+
* @param {string} baseUrl
60+
* The base URL for an API service.
61+
* Typically in the format of http://example.com/
62+
*
63+
* @param {(headers: Headers, api: { getState: () => unknown }) => Headers} prepareHeaders
64+
* An optional function that can be used to inject headers on requests.
65+
* Provides a Headers object, as well as the `getState` function from the
66+
* redux store. Can be useful for authentication.
67+
*
68+
* @link https://developer.mozilla.org/en-US/docs/Web/API/Headers
69+
*
70+
* @param {(input: RequestInfo, init?: RequestInit | undefined) => Promise<Response>} fetchFn
71+
* Accepts a custom `fetch` function if you do not want to use the default on the window.
72+
* Useful in SSR environments if you need to use a library such as `isomorphic-fetch` or `cross-fetch`
73+
*
74+
*/
3975
export function fetchBaseQuery({
4076
baseUrl,
4177
prepareHeaders = (x) => x,
@@ -44,10 +80,6 @@ export function fetchBaseQuery({
4480
}: {
4581
baseUrl?: string;
4682
prepareHeaders?: (headers: Headers, api: { getState: () => unknown }) => Headers;
47-
/**
48-
* Accepts a custom `fetch` function if you do not want to use the default on the window.
49-
* Useful in SSR environments if you need to pass isomorphic-fetch or cross-fetch
50-
*/
5183
fetchFn?: (input: RequestInfo, init?: RequestInit | undefined) => Promise<Response>;
5284
} & RequestInit = {}): BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}> {
5385
return async (arg, { signal, getState }) => {
@@ -69,7 +101,7 @@ export function fetchBaseQuery({
69101
...rest,
70102
};
71103

72-
config.headers = prepareHeaders(new Headers(headers), { getState });
104+
config.headers = prepareHeaders(new Headers(cleanUndefinedHeaders(headers)), { getState });
73105

74106
if (!config.headers.has('content-type')) {
75107
config.headers.set('content-type', 'application/json');

test/fetchBaseQuery.test.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,64 @@ describe('fetchBaseQuery', () => {
348348
expect(request.headers['authorization']).toBe(`Bearer ${token}`);
349349
});
350350
});
351+
352+
test('lets a header be undefined', async () => {
353+
let request: any;
354+
({ data: request } = await baseQuery(
355+
{ url: '/echo', headers: undefined },
356+
{
357+
signal: undefined,
358+
dispatch: storeRef.store.dispatch,
359+
getState: storeRef.store.getState,
360+
},
361+
{}
362+
));
363+
364+
expect(request.headers['content-type']).toBe('application/json');
365+
expect(request.headers['fake']).toBe(defaultHeaders['fake']);
366+
expect(request.headers['delete']).toBe(defaultHeaders['delete']);
367+
expect(request.headers['delete2']).toBe(defaultHeaders['delete2']);
368+
});
369+
370+
test('allows for possibly undefined header key/values', async () => {
371+
const banana = '1' as '1' | undefined;
372+
let request: any;
373+
({ data: request } = await baseQuery(
374+
{ url: '/echo', headers: { banana } },
375+
{
376+
signal: undefined,
377+
dispatch: storeRef.store.dispatch,
378+
getState: storeRef.store.getState,
379+
},
380+
{}
381+
));
382+
383+
expect(request.headers['content-type']).toBe('application/json');
384+
expect(request.headers['banana']).toBe('1');
385+
expect(request.headers['fake']).toBe(defaultHeaders['fake']);
386+
expect(request.headers['delete']).toBe(defaultHeaders['delete']);
387+
expect(request.headers['delete2']).toBe(defaultHeaders['delete2']);
388+
});
389+
390+
test('strips undefined values from the headers', async () => {
391+
const banana = undefined as '1' | undefined;
392+
let request: any;
393+
({ data: request } = await baseQuery(
394+
{ url: '/echo', headers: { banana } },
395+
{
396+
signal: undefined,
397+
dispatch: storeRef.store.dispatch,
398+
getState: storeRef.store.getState,
399+
},
400+
{}
401+
));
402+
403+
expect(request.headers['content-type']).toBe('application/json');
404+
expect(request.headers['banana']).toBeUndefined();
405+
expect(request.headers['fake']).toBe(defaultHeaders['fake']);
406+
expect(request.headers['delete']).toBe(defaultHeaders['delete']);
407+
expect(request.headers['delete2']).toBe(defaultHeaders['delete2']);
408+
});
351409
});
352410

353411
describe('fetchFn', () => {

0 commit comments

Comments
 (0)