Skip to content

Commit 4a2ed54

Browse files
author
Lenz Weber
committed
add timeout option to fetchBaseQuery
1 parent 478ffcf commit 4a2ed54

File tree

7 files changed

+100
-12
lines changed

7 files changed

+100
-12
lines changed

packages/toolkit/src/createAsyncThunk.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export type BaseThunkAPI<
2424
extra: E
2525
requestId: string
2626
signal: AbortSignal
27+
abort: (reason?: string) => void
2728
rejectWithValue: IsUnknown<
2829
RejectedMeta,
2930
(value: RejectedValue) => RejectWithValue<RejectedValue, RejectedMeta>,
@@ -608,6 +609,7 @@ If you want to use the AbortController to react to \`abort\` events, please cons
608609
extra,
609610
requestId,
610611
signal: abortController.signal,
612+
abort,
611613
rejectWithValue: ((
612614
value: RejectedValue,
613615
meta?: RejectedMeta

packages/toolkit/src/query/baseQueryTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { MaybePromise, UnwrapPromise } from './tsHelpers'
33

44
export interface BaseQueryApi {
55
signal: AbortSignal
6+
abort: (reason?: string) => void
67
dispatch: ThunkDispatch<any, any, any>
78
getState: () => unknown
89
extra: unknown

packages/toolkit/src/query/core/buildThunks.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,15 @@ export function buildThunks<
261261
ThunkApiMetaConfig & { state: RootState<any, string, ReducerPath> }
262262
> = async (
263263
arg,
264-
{ signal, rejectWithValue, fulfillWithValue, dispatch, getState, extra }
264+
{
265+
signal,
266+
abort,
267+
rejectWithValue,
268+
fulfillWithValue,
269+
dispatch,
270+
getState,
271+
extra,
272+
}
265273
) => {
266274
const endpointDefinition = endpointDefinitions[arg.endpointName]
267275

@@ -274,6 +282,7 @@ export function buildThunks<
274282
let result: QueryReturnValue
275283
const baseQueryApi = {
276284
signal,
285+
abort,
277286
dispatch,
278287
getState,
279288
extra,

packages/toolkit/src/query/fetchBaseQuery.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface FetchArgs extends CustomRequestInit {
2525
body?: any
2626
responseHandler?: ResponseHandler
2727
validateStatus?: (response: Response, body: any) => boolean
28+
timeout?: number
2829
}
2930

3031
/**
@@ -88,6 +89,15 @@ export type FetchBaseQueryError =
8889
data: string
8990
error: string
9091
}
92+
| {
93+
/**
94+
* * `"TIMEOUT_ERROR"`:
95+
* Request timed out
96+
**/
97+
status: 'TIMEOUT_ERROR'
98+
data?: undefined
99+
error: string
100+
}
91101
| {
92102
/**
93103
* * `"CUSTOM_ERROR"`:
@@ -123,6 +133,7 @@ export type FetchBaseQueryArgs = {
123133
init?: RequestInit | undefined
124134
) => Promise<Response>
125135
paramsSerializer?: (params: Record<string, any>) => string
136+
timeout?: number
126137
} & RequestInit
127138

128139
export type FetchBaseQueryMeta = { request: Request; response?: Response }
@@ -168,6 +179,7 @@ export function fetchBaseQuery({
168179
prepareHeaders = (x) => x,
169180
fetchFn = defaultFetchFn,
170181
paramsSerializer,
182+
timeout: defaultTimeout,
171183
...baseFetchOptions
172184
}: FetchBaseQueryArgs = {}): BaseQueryFn<
173185
string | FetchArgs,
@@ -192,6 +204,7 @@ export function fetchBaseQuery({
192204
params = undefined,
193205
responseHandler = 'json' as const,
194206
validateStatus = defaultValidateStatus,
207+
timeout = defaultTimeout,
195208
...rest
196209
} = typeof arg == 'string' ? { url: arg } : arg
197210
let config: RequestInit = {
@@ -236,11 +249,26 @@ export function fetchBaseQuery({
236249
const requestClone = request.clone()
237250
meta = { request: requestClone }
238251

239-
let response
252+
let response,
253+
timedOut = false,
254+
timeoutId =
255+
timeout &&
256+
setTimeout(() => {
257+
timedOut = true
258+
api.abort()
259+
}, timeout)
240260
try {
241261
response = await fetchFn(request)
242262
} catch (e) {
243-
return { error: { status: 'FETCH_ERROR', error: String(e) }, meta }
263+
return {
264+
error: {
265+
status: timedOut ? 'TIMEOUT_ERROR' : 'FETCH_ERROR',
266+
error: String(e),
267+
},
268+
meta,
269+
}
270+
} finally {
271+
if (timeoutId) clearTimeout(timeoutId)
244272
}
245273
const responseClone = response.clone()
246274

packages/toolkit/src/query/tests/createApi.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ describe('endpoint definition typings', () => {
310310
const commonBaseQueryApi = {
311311
dispatch: expect.any(Function),
312312
endpoint: expect.any(String),
313+
abort: expect.any(Function),
313314
extra: undefined,
314315
forced: expect.any(Boolean),
315316
getState: expect.any(Function),
@@ -352,6 +353,7 @@ describe('endpoint definition typings', () => {
352353
endpoint: expect.any(String),
353354
getState: expect.any(Function),
354355
signal: expect.any(Object),
356+
abort: expect.any(Function),
355357
forced: expect.any(Boolean),
356358
type: expect.any(String),
357359
},
@@ -364,6 +366,7 @@ describe('endpoint definition typings', () => {
364366
endpoint: expect.any(String),
365367
getState: expect.any(Function),
366368
signal: expect.any(Object),
369+
abort: expect.any(Function),
367370
forced: expect.any(Boolean),
368371
type: expect.any(String),
369372
},
@@ -376,6 +379,7 @@ describe('endpoint definition typings', () => {
376379
endpoint: expect.any(String),
377380
getState: expect.any(Function),
378381
signal: expect.any(Object),
382+
abort: expect.any(Function),
379383
// forced: undefined,
380384
type: expect.any(String),
381385
},
@@ -388,6 +392,7 @@ describe('endpoint definition typings', () => {
388392
endpoint: expect.any(String),
389393
getState: expect.any(Function),
390394
signal: expect.any(Object),
395+
abort: expect.any(Function),
391396
// forced: undefined,
392397
type: expect.any(String),
393398
},

packages/toolkit/src/query/tests/errorHandling.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,12 @@ const failQueryOnce = rest.get('/query', (_, req, ctx) =>
3636
describe('fetchBaseQuery', () => {
3737
let commonBaseQueryApiArgs: BaseQueryApi = {} as any
3838
beforeEach(() => {
39+
const abortController = new AbortController()
3940
commonBaseQueryApiArgs = {
40-
signal: new AbortController().signal,
41+
signal: abortController.signal,
42+
abort: (reason) =>
43+
//@ts-ignore
44+
abortController.abort(reason),
4145
dispatch: storeRef.store.dispatch,
4246
getState: storeRef.store.getState,
4347
extra: undefined,

packages/toolkit/src/query/tests/fetchBaseQuery.test.tsx

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createSlice } from '@reduxjs/toolkit'
22
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
3-
import { setupApiStore } from './helpers'
3+
import { setupApiStore, waitMs } from './helpers'
44
import { server } from './mocks/server'
55
// @ts-ignore
66
import nodeFetch from 'node-fetch'
@@ -76,8 +76,12 @@ type RootState = ReturnType<typeof storeRef.store.getState>
7676

7777
let commonBaseQueryApi: BaseQueryApi = {} as any
7878
beforeEach(() => {
79+
let abortController = new AbortController()
7980
commonBaseQueryApi = {
80-
signal: new AbortController().signal,
81+
signal: abortController.signal,
82+
abort: (reason) =>
83+
// @ts-ignore
84+
abortController.abort(reason),
8185
dispatch: storeRef.store.dispatch,
8286
getState: storeRef.store.getState,
8387
extra: undefined,
@@ -513,11 +517,15 @@ describe('fetchBaseQuery', () => {
513517
test('prepareHeaders is able to select from a state', async () => {
514518
let request: any
515519

516-
const doRequest = async () =>
517-
baseQuery(
520+
const doRequest = async () => {
521+
const abortController = new AbortController()
522+
return baseQuery(
518523
{ url: '/echo' },
519524
{
520-
signal: new AbortController().signal,
525+
signal: abortController.signal,
526+
abort: (reason) =>
527+
// @ts-ignore
528+
abortController.abort(reason),
521529
dispatch: storeRef.store.dispatch,
522530
getState: storeRef.store.getState,
523531
extra: undefined,
@@ -526,6 +534,7 @@ describe('fetchBaseQuery', () => {
526534
},
527535
{}
528536
)
537+
}
529538

530539
;({ data: request } = await doRequest())
531540

@@ -563,11 +572,15 @@ describe('fetchBaseQuery', () => {
563572
getTokenSilently: async () => 'fakeToken',
564573
}
565574

566-
const doRequest = async () =>
567-
baseQuery(
575+
const doRequest = async () => {
576+
const abortController = new AbortController()
577+
return baseQuery(
568578
{ url: '/echo' },
569579
{
570-
signal: new AbortController().signal,
580+
signal: abortController.signal,
581+
abort: (reason) =>
582+
// @ts-ignore
583+
abortController.abort(reason),
571584
dispatch: storeRef.store.dispatch,
572585
getState: storeRef.store.getState,
573586
extra: fakeAuth0Client,
@@ -577,6 +590,7 @@ describe('fetchBaseQuery', () => {
577590
},
578591
{}
579592
)
593+
}
580594

581595
await doRequest()
582596

@@ -709,4 +723,29 @@ describe('still throws on completely unexpected errors', () => {
709723
expect(req).toBeInstanceOf(Promise)
710724
await expect(req).rejects.toBe(error)
711725
})
726+
727+
test('timeout behaviour', async () => {
728+
jest.useFakeTimers()
729+
let result: any
730+
server.use(
731+
rest.get('https://example.com/empty', (req, res, ctx) =>
732+
res.once(
733+
ctx.delay(3000),
734+
ctx.json({ ...req, headers: req.headers.all() })
735+
)
736+
)
737+
)
738+
Promise.resolve(
739+
baseQuery({ url: '/empty', timeout: 2000 }, commonBaseQueryApi, {})
740+
).then((r) => {
741+
result = r
742+
})
743+
await waitMs()
744+
jest.runAllTimers()
745+
await waitMs()
746+
expect(result?.error).toEqual({
747+
status: 'TIMEOUT_ERROR',
748+
error: 'AbortError: The user aborted a request.',
749+
})
750+
})
712751
})

0 commit comments

Comments
 (0)