source: node_modules/@reduxjs/toolkit/src/query/tests/fetchBaseQuery.test.tsx

Last change on this file was a762898, checked in by istevanoska <ilinastevanoska@…>, 5 months ago

Added visualizations

  • Property mode set to 100644
File size: 44.4 KB
RevLine 
[a762898]1import { createSlice } from '@reduxjs/toolkit'
2import type { FetchArgs } from '@reduxjs/toolkit/query'
3import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
4import { headersToObject } from 'headers-polyfill'
5import { HttpResponse, delay, http } from 'msw'
6import nodeFetch from 'node-fetch'
7import queryString from 'query-string'
8import { vi } from 'vitest'
9import { setupApiStore } from '../../tests/utils/helpers'
10import type { BaseQueryApi } from '../baseQueryTypes'
11import { server } from './mocks/server'
12
13const defaultHeaders: Record<string, string> = {
14 fake: 'header',
15 delete: 'true',
16 delete2: '1',
17}
18
19const baseUrl = 'https://example.com'
20
21const baseQuery = fetchBaseQuery({
22 baseUrl,
23 prepareHeaders: (headers, { getState }) => {
24 const { token } = (getState() as RootState).auth
25
26 // If we have a token set in state, let's assume that we should be passing it.
27 if (token) {
28 headers.set('authorization', `Bearer ${token}`)
29 }
30 // A user could customize their behavior here, so we'll just test that custom scenarios would work.
31 const potentiallyConflictingKeys = Object.keys(defaultHeaders)
32 potentiallyConflictingKeys.forEach((key) => {
33 // Check for presence of a default key, if the incoming endpoint headers don't specify it as '', then set it
34 const existingValue = headers.get(key)
35 if (!existingValue && existingValue !== '') {
36 headers.set(key, String(defaultHeaders[key]))
37 // If an endpoint sets a header with a value of '', just delete the header.
38 } else if (headers.get(key) === '') {
39 headers.delete(key)
40 }
41 })
42
43 return headers
44 },
45})
46
47const api = createApi({
48 baseQuery,
49 endpoints(build) {
50 return {
51 query: build.query({ query: () => ({ url: '/echo', headers: {} }) }),
52 mutation: build.mutation({
53 query: () => ({ url: '/echo', method: 'POST', credentials: 'omit' }),
54 }),
55 }
56 },
57})
58
59const authSlice = createSlice({
60 name: 'auth',
61 initialState: {
62 token: '',
63 },
64 reducers: {
65 setToken(state, action) {
66 state.token = action.payload
67 },
68 },
69})
70
71const storeRef = setupApiStore(api, { auth: authSlice.reducer })
72type RootState = ReturnType<typeof storeRef.store.getState>
73
74let commonBaseQueryApi: BaseQueryApi = {} as any
75beforeEach(() => {
76 let abortController = new AbortController()
77 commonBaseQueryApi = {
78 signal: abortController.signal,
79 abort: (reason) =>
80 // @ts-ignore
81 abortController.abort(reason),
82 dispatch: storeRef.store.dispatch,
83 getState: storeRef.store.getState,
84 extra: undefined,
85 type: 'query',
86 endpoint: 'doesntmatterhere',
87 }
88})
89
90describe('fetchBaseQuery', () => {
91 describe('basic functionality', () => {
92 it('should return an object for a simple GET request when it is json data', async () => {
93 const req = baseQuery('/success', commonBaseQueryApi, {})
94 expect(req).toBeInstanceOf(Promise)
95 const res = await req
96 expect(res).toBeInstanceOf(Object)
97 expect(res.data).toEqual({ value: 'success' })
98 })
99
100 it('should return undefined for a simple GET request when the response is empty', async () => {
101 const req = baseQuery('/empty', commonBaseQueryApi, {})
102 expect(req).toBeInstanceOf(Promise)
103 const res = await req
104 expect(res).toBeInstanceOf(Object)
105 expect(res.meta?.request).toBeInstanceOf(Request)
106 expect(res.meta?.response).toBeInstanceOf(Object)
107
108 expect(res.data).toBeNull()
109 })
110
111 it('should return an error and status for error responses', async () => {
112 const req = baseQuery('/error', commonBaseQueryApi, {})
113 expect(req).toBeInstanceOf(Promise)
114 const res = await req
115 expect(res).toBeInstanceOf(Object)
116 expect(res.meta?.request).toBeInstanceOf(Request)
117 expect(res.meta?.response).toBeInstanceOf(Object)
118 expect(res.error).toEqual({
119 status: 500,
120 data: { value: 'error' },
121 })
122 })
123
124 it('should handle a connection loss semi-gracefully', async () => {
125 const fetchFn = vi
126 .fn()
127 .mockRejectedValueOnce(new TypeError('Failed to fetch'))
128
129 const req = fetchBaseQuery({
130 baseUrl,
131 fetchFn,
132 })('/success', commonBaseQueryApi, {})
133 expect(req).toBeInstanceOf(Promise)
134 const res = await req
135 expect(res).toBeInstanceOf(Object)
136 expect(res.meta?.request).toBeInstanceOf(Request)
137 expect(res.meta?.response).toBe(undefined)
138 expect(res.error).toEqual({
139 status: 'FETCH_ERROR',
140 error: 'TypeError: Failed to fetch',
141 })
142 })
143 })
144
145 describe('non-JSON-body', () => {
146 it('success: should return data ("text" responseHandler)', async () => {
147 server.use(
148 http.get(
149 'https://example.com/success',
150 () => HttpResponse.text(`this is not json!`),
151 { once: true },
152 ),
153 )
154
155 const req = baseQuery(
156 { url: '/success', responseHandler: 'text' },
157 commonBaseQueryApi,
158 {},
159 )
160 expect(req).toBeInstanceOf(Promise)
161 const res = await req
162 expect(res).toBeInstanceOf(Object)
163 expect(res.meta?.request).toBeInstanceOf(Request)
164 expect(res.meta?.response).toBeInstanceOf(Object)
165 expect(res.data).toEqual(`this is not json!`)
166 })
167
168 it('success: should fail gracefully (default="json" responseHandler)', async () => {
169 server.use(
170 http.get(
171 'https://example.com/success',
172 () => HttpResponse.text(`this is not json!`),
173 { once: true },
174 ),
175 )
176
177 const req = baseQuery('/success', commonBaseQueryApi, {})
178 expect(req).toBeInstanceOf(Promise)
179 const res = await req
180 expect(res).toBeInstanceOf(Object)
181 expect(res.meta?.request).toBeInstanceOf(Request)
182 expect(res.meta?.response).toBeInstanceOf(Object)
183 expect(res.error).toEqual({
184 status: 'PARSING_ERROR',
185 error: expect.stringMatching(/SyntaxError: Unexpected token/),
186 originalStatus: 200,
187 data: `this is not json!`,
188 })
189 })
190
191 it('success: parse text without error ("content-type" responseHandler)', async () => {
192 server.use(
193 http.get(
194 'https://example.com/success',
195 () => HttpResponse.text(`this is not json!`),
196 { once: true },
197 ),
198 )
199
200 const req = baseQuery(
201 {
202 url: '/success',
203 responseHandler: 'content-type',
204 },
205 commonBaseQueryApi,
206 {},
207 )
208 expect(req).toBeInstanceOf(Promise)
209 const res = await req
210 expect(res).toBeInstanceOf(Object)
211 expect(res.meta?.response?.headers.get('content-type')).toEqual(
212 'text/plain',
213 )
214 expect(res.meta?.request).toBeInstanceOf(Request)
215 expect(res.meta?.response).toBeInstanceOf(Object)
216 expect(res.data).toEqual(`this is not json!`)
217 })
218
219 it('success: parse json without error ("content-type" responseHandler)', async () => {
220 server.use(
221 http.get(
222 'https://example.com/success',
223 () => HttpResponse.json(`this will become json!`),
224 { once: true },
225 ),
226 )
227
228 const req = baseQuery(
229 {
230 url: '/success',
231 responseHandler: 'content-type',
232 },
233 commonBaseQueryApi,
234 {},
235 )
236 expect(req).toBeInstanceOf(Promise)
237 const res = await req
238 expect(res).toBeInstanceOf(Object)
239 expect(res.meta?.response?.headers.get('content-type')).toEqual(
240 'application/json',
241 )
242 expect(res.meta?.request).toBeInstanceOf(Request)
243 expect(res.meta?.response).toBeInstanceOf(Object)
244 expect(res.data).toEqual(`this will become json!`)
245 })
246
247 it('server error: should fail normally with a 500 status ("text" responseHandler)', async () => {
248 server.use(
249 http.get('https://example.com/error', () =>
250 HttpResponse.text(`this is not json!`, { status: 500 }),
251 ),
252 )
253
254 const req = baseQuery(
255 { url: '/error', responseHandler: 'text' },
256 commonBaseQueryApi,
257 {},
258 )
259 expect(req).toBeInstanceOf(Promise)
260 const res = await req
261 expect(res).toBeInstanceOf(Object)
262 expect(res.meta?.request).toBeInstanceOf(Request)
263 expect(res.meta?.response).toBeInstanceOf(Object)
264 expect(res.error).toEqual({
265 status: 500,
266 data: `this is not json!`,
267 })
268 })
269
270 it('server error: should fail normally with a 500 status as text ("content-type" responseHandler)', async () => {
271 const serverResponse = 'Internal Server Error'
272 server.use(
273 http.get('https://example.com/error', () =>
274 HttpResponse.text(serverResponse, { status: 500 }),
275 ),
276 )
277
278 const req = baseQuery(
279 { url: '/error', responseHandler: 'content-type' },
280 commonBaseQueryApi,
281 {},
282 )
283 expect(req).toBeInstanceOf(Promise)
284 const res = await req
285 expect(res).toBeInstanceOf(Object)
286 expect(res.meta?.request).toBeInstanceOf(Request)
287 expect(res.meta?.response).toBeInstanceOf(Object)
288 expect(res.meta?.response?.headers.get('content-type')).toEqual(
289 'text/plain',
290 )
291 expect(res.error).toEqual({
292 status: 500,
293 data: serverResponse,
294 })
295 })
296
297 it('server error: should fail normally with a 500 status as json ("content-type" responseHandler)', async () => {
298 const serverResponse = {
299 errors: { field1: "Password cannot be 'password'" },
300 }
301 server.use(
302 http.get('https://example.com/error', () =>
303 HttpResponse.json(serverResponse, { status: 500 }),
304 ),
305 )
306
307 const req = baseQuery(
308 { url: '/error', responseHandler: 'content-type' },
309 commonBaseQueryApi,
310 {},
311 )
312 expect(req).toBeInstanceOf(Promise)
313 const res = await req
314 expect(res).toBeInstanceOf(Object)
315 expect(res.meta?.request).toBeInstanceOf(Request)
316 expect(res.meta?.response).toBeInstanceOf(Object)
317 expect(res.meta?.response?.headers.get('content-type')).toEqual(
318 'application/json',
319 )
320 expect(res.error).toEqual({
321 status: 500,
322 data: serverResponse,
323 })
324 })
325
326 it('server error: should fail gracefully (default="json" responseHandler)', async () => {
327 server.use(
328 http.get('https://example.com/error', () =>
329 HttpResponse.text(`this is not json!`, { status: 500 }),
330 ),
331 )
332
333 const req = baseQuery('/error', commonBaseQueryApi, {})
334 expect(req).toBeInstanceOf(Promise)
335 const res = await req
336 expect(res).toBeInstanceOf(Object)
337 expect(res.meta?.request).toBeInstanceOf(Request)
338 expect(res.meta?.response).toBeInstanceOf(Object)
339 expect(res.error).toEqual({
340 status: 'PARSING_ERROR',
341 error: expect.stringMatching(/SyntaxError: Unexpected token/),
342 originalStatus: 500,
343 data: `this is not json!`,
344 })
345 })
346 })
347
348 describe('arg.body', () => {
349 test('an object provided to body will be serialized when content-type is json', async () => {
350 const data = {
351 test: 'value',
352 }
353
354 let request: any
355 ;({ data: request } = await baseQuery(
356 { url: '/echo', body: data, method: 'POST' },
357 { ...commonBaseQueryApi, type: 'mutation' },
358 {},
359 ))
360
361 expect(request.headers['content-type']).toBe('application/json')
362 expect(request.body).toEqual(data)
363 })
364
365 test('an array provided to body will be serialized when content-type is json', async () => {
366 const data = ['test', 'value']
367
368 let request: any
369 ;({ data: request } = await baseQuery(
370 { url: '/echo', body: data, method: 'POST' },
371 commonBaseQueryApi,
372 {},
373 ))
374
375 expect(request.headers['content-type']).toBe('application/json')
376 expect(request.body).toEqual(data)
377 })
378
379 test('an object provided to body will not be serialized when content-type is not json', async () => {
380 const data = {
381 test: 'value',
382 }
383
384 let request: any
385 ;({ data: request } = await baseQuery(
386 {
387 url: '/echo',
388 body: data,
389 method: 'POST',
390 headers: { 'content-type': 'text/html' },
391 },
392 commonBaseQueryApi,
393 {},
394 ))
395
396 expect(request.headers['content-type']).toBe('text/html')
397 expect(request.body).toEqual('[object Object]')
398 })
399
400 test('an array provided to body will not be serialized when content-type is not json', async () => {
401 const data = ['test', 'value']
402
403 let request: any
404 ;({ data: request } = await baseQuery(
405 {
406 url: '/echo',
407 body: data,
408 method: 'POST',
409 headers: { 'content-type': 'text/html' },
410 },
411 commonBaseQueryApi,
412 {},
413 ))
414
415 expect(request.headers['content-type']).toBe('text/html')
416 expect(request.body).toEqual(data.join(','))
417 })
418
419 it('supports a custom jsonContentType', async () => {
420 const baseQuery = fetchBaseQuery({
421 baseUrl,
422 jsonContentType: 'application/vnd.api+json',
423 })
424
425 let request: any
426 ;({ data: request } = await baseQuery(
427 {
428 url: '/echo',
429 body: {},
430 method: 'POST',
431 },
432 commonBaseQueryApi,
433 {},
434 ))
435
436 expect(request.headers['content-type']).toBe('application/vnd.api+json')
437 })
438
439 it('supports a custom jsonReplacer', async () => {
440 const body = {
441 items: new Set(['A', 'B', 'C']),
442 }
443
444 let request: any
445 ;({ data: request } = await baseQuery(
446 {
447 url: '/echo',
448 body,
449 method: 'POST',
450 },
451 commonBaseQueryApi,
452 {},
453 ))
454
455 expect(request.headers['content-type']).toBe('application/json')
456 expect(request.body).toEqual({ items: {} }) // Set is not properly marshalled by default
457
458 // Use jsonReplacer
459 const baseQueryWithReplacer = fetchBaseQuery({
460 baseUrl,
461 jsonReplacer: (key, value) =>
462 value instanceof Set ? [...value] : value,
463 })
464
465 ;({ data: request } = await baseQueryWithReplacer(
466 {
467 url: '/echo',
468 body,
469 method: 'POST',
470 },
471 commonBaseQueryApi,
472 {},
473 ))
474
475 expect(request.headers['content-type']).toBe('application/json')
476 expect(request.body).toEqual({ items: ['A', 'B', 'C'] }) // Set is marshalled correctly by jsonReplacer
477 })
478 })
479
480 describe('arg.params', () => {
481 it('should not serialize missing params', async () => {
482 let request: any
483 ;({ data: request } = await baseQuery(
484 { url: '/echo' },
485 commonBaseQueryApi,
486 {},
487 ))
488
489 expect(request.url).toEqual(`${baseUrl}/echo`)
490 })
491
492 it('should serialize numeric and boolean params', async () => {
493 const params = { a: 1, b: true }
494
495 let request: any
496 ;({ data: request } = await baseQuery(
497 { url: '/echo', params },
498 commonBaseQueryApi,
499 {},
500 ))
501
502 expect(request.url).toEqual(`${baseUrl}/echo?a=1&b=true`)
503 })
504
505 it('should merge params into existing url querystring', async () => {
506 const params = { a: 1, b: true }
507
508 let request: any
509 ;({ data: request } = await baseQuery(
510 { url: '/echo?banana=pudding', params },
511 commonBaseQueryApi,
512 {},
513 ))
514
515 expect(request.url).toEqual(`${baseUrl}/echo?banana=pudding&a=1&b=true`)
516 })
517
518 it('should accept a URLSearchParams instance', async () => {
519 const params = new URLSearchParams({ apple: 'fruit' })
520
521 let request: any
522 ;({ data: request } = await baseQuery(
523 { url: '/echo', params },
524 commonBaseQueryApi,
525 {},
526 ))
527
528 expect(request.url).toEqual(`${baseUrl}/echo?apple=fruit`)
529 })
530
531 it('should strip undefined values from the end params', async () => {
532 const params = { apple: 'fruit', banana: undefined, randy: null }
533
534 let request: any
535 ;({ data: request } = await baseQuery(
536 { url: '/echo', params },
537 commonBaseQueryApi,
538 {},
539 ))
540
541 expect(request.url).toEqual(`${baseUrl}/echo?apple=fruit&randy=null`)
542 })
543
544 it('should support a paramsSerializer', async () => {
545 const baseQuery = fetchBaseQuery({
546 baseUrl,
547 paramsSerializer: (params: Record<string, unknown>) =>
548 queryString.stringify(params, { arrayFormat: 'bracket' }),
549 })
550
551 const api = createApi({
552 baseQuery,
553 endpoints(build) {
554 return {
555 query: build.query({
556 query: () => ({ url: '/echo', headers: {} }),
557 }),
558 mutation: build.mutation({
559 query: () => ({
560 url: '/echo',
561 method: 'POST',
562 credentials: 'omit',
563 }),
564 }),
565 }
566 },
567 })
568
569 const params = {
570 someArray: ['a', 'b', 'c'],
571 }
572
573 let request: any
574 ;({ data: request } = await baseQuery(
575 { url: '/echo', params },
576 commonBaseQueryApi,
577 {},
578 ))
579
580 expect(request.url).toEqual(
581 `${baseUrl}/echo?someArray[]=a&someArray[]=b&someArray[]=c`,
582 )
583 })
584
585 it('should supports a custom isJsonContentType function', async () => {
586 const testBody = {
587 i_should_be_stringified: true,
588 }
589 const baseQuery = fetchBaseQuery({
590 baseUrl,
591 isJsonContentType: (headers) =>
592 [
593 'application/vnd.api+json',
594 'application/json',
595 'application/vnd.hal+json',
596 ].includes(headers.get('content-type') ?? ''),
597 })
598
599 let request: any
600 ;({ data: request } = await baseQuery(
601 {
602 url: '/echo',
603 method: 'POST',
604 body: testBody,
605 headers: { 'content-type': 'application/vnd.hal+json' },
606 },
607 commonBaseQueryApi,
608 {},
609 ))
610
611 expect(request.body).toMatchObject(testBody)
612 })
613 })
614
615 describe('validateStatus', () => {
616 test('validateStatus can return an error even on normal 200 responses', async () => {
617 // This is a scenario where an API may always return a 200, but indicates there is an error when success = false
618 const res = await baseQuery(
619 {
620 url: '/nonstandard-error',
621 validateStatus: (response, body) =>
622 response.status === 200 && body.success === false ? false : true,
623 },
624 commonBaseQueryApi,
625 {},
626 )
627
628 expect(res.error).toEqual({
629 status: 200,
630 data: {
631 success: false,
632 message: 'This returns a 200 but is really an error',
633 },
634 })
635 })
636 })
637
638 describe('arg.headers and prepareHeaders', () => {
639 test('uses the default headers set in prepareHeaders', async () => {
640 let request: any
641 ;({ data: request } = await baseQuery(
642 { url: '/echo' },
643 commonBaseQueryApi,
644 {},
645 ))
646
647 expect(request.headers['fake']).toBe(defaultHeaders['fake'])
648 expect(request.headers['delete']).toBe(defaultHeaders['delete'])
649 expect(request.headers['delete2']).toBe(defaultHeaders['delete2'])
650 })
651
652 test('adds endpoint-level headers to the defaults', async () => {
653 let request: any
654 ;({ data: request } = await baseQuery(
655 { url: '/echo', headers: { authorization: 'Bearer banana' } },
656 commonBaseQueryApi,
657 {},
658 ))
659
660 expect(request.headers['authorization']).toBe('Bearer banana')
661 expect(request.headers['fake']).toBe(defaultHeaders['fake'])
662 expect(request.headers['delete']).toBe(defaultHeaders['delete'])
663 expect(request.headers['delete2']).toBe(defaultHeaders['delete2'])
664 })
665
666 test('it does not set application/json when content-type is set', async () => {
667 let request: any
668 ;({ data: request } = await baseQuery(
669 {
670 url: '/echo',
671 headers: {
672 authorization: 'Bearer banana',
673 'content-type': 'custom-content-type',
674 },
675 },
676 commonBaseQueryApi,
677 {},
678 ))
679
680 expect(request.headers['authorization']).toBe('Bearer banana')
681 expect(request.headers['content-type']).toBe('custom-content-type')
682 expect(request.headers['fake']).toBe(defaultHeaders['fake'])
683 expect(request.headers['delete']).toBe(defaultHeaders['delete'])
684 expect(request.headers['delete2']).toBe(defaultHeaders['delete2'])
685 })
686
687 test('respects the headers from an endpoint over the base headers', async () => {
688 const fake = 'fake endpoint value'
689
690 let request: any
691 ;({ data: request } = await baseQuery(
692 { url: '/echo', headers: { fake, delete: '', delete2: '' } },
693 commonBaseQueryApi,
694 {},
695 ))
696
697 expect(request.headers['fake']).toBe(fake)
698 expect(request.headers['delete']).toBeUndefined()
699 expect(request.headers['delete2']).toBeUndefined()
700 })
701
702 test('prepareHeaders can return undefined', async () => {
703 let request: any
704
705 const token = 'accessToken'
706
707 const _baseQuery = fetchBaseQuery({
708 baseUrl,
709 prepareHeaders: (headers) => {
710 headers.set('authorization', `Bearer ${token}`)
711 },
712 })
713
714 const doRequest = async () =>
715 _baseQuery({ url: '/echo' }, commonBaseQueryApi, {})
716
717 ;({ data: request } = await doRequest())
718
719 expect(request.headers['authorization']).toBe(`Bearer ${token}`)
720 })
721
722 test('prepareHeaders is able to be an async function', async () => {
723 let request: any
724
725 const token = 'accessToken'
726 const getAccessTokenAsync = async () => token
727
728 const _baseQuery = fetchBaseQuery({
729 baseUrl,
730 prepareHeaders: async (headers) => {
731 headers.set('authorization', `Bearer ${await getAccessTokenAsync()}`)
732 return headers
733 },
734 })
735
736 const doRequest = async () =>
737 _baseQuery({ url: '/echo' }, commonBaseQueryApi, {})
738
739 ;({ data: request } = await doRequest())
740
741 expect(request.headers['authorization']).toBe(`Bearer ${token}`)
742 })
743
744 test('prepareHeaders is able to be an async function returning undefined', async () => {
745 let request: any
746
747 const token = 'accessToken'
748 const getAccessTokenAsync = async () => token
749
750 const _baseQuery = fetchBaseQuery({
751 baseUrl,
752 prepareHeaders: async (headers) => {
753 headers.set('authorization', `Bearer ${await getAccessTokenAsync()}`)
754 },
755 })
756
757 const doRequest = async () =>
758 _baseQuery({ url: '/echo' }, commonBaseQueryApi, {})
759
760 ;({ data: request } = await doRequest())
761
762 expect(request.headers['authorization']).toBe(`Bearer ${token}`)
763 })
764
765 test('prepareHeaders is able to select from a state', async () => {
766 let request: any
767
768 const doRequest = async () => {
769 const abortController = new AbortController()
770 return baseQuery(
771 { url: '/echo' },
772 {
773 signal: abortController.signal,
774 abort: (reason) =>
775 // @ts-ignore
776 abortController.abort(reason),
777 dispatch: storeRef.store.dispatch,
778 getState: storeRef.store.getState,
779 extra: undefined,
780 type: 'query',
781 endpoint: '',
782 },
783 {},
784 )
785 }
786
787 ;({ data: request } = await doRequest())
788
789 expect(request.headers['authorization']).toBeUndefined()
790
791 // Set a token and the follow up request should have the header injected by prepareHeaders
792 const token = 'fakeToken!'
793 storeRef.store.dispatch(authSlice.actions.setToken(token))
794 ;({ data: request } = await doRequest())
795
796 expect(request.headers['authorization']).toBe(`Bearer ${token}`)
797 })
798
799 test('prepareHeaders provides extra api information for getState, extra, endpoint, type and forced', async () => {
800 let _getState, _arg: any, _extra, _endpoint, _type, _forced
801
802 const baseQuery = fetchBaseQuery({
803 baseUrl,
804 prepareHeaders: (
805 headers,
806 { getState, arg, extra, endpoint, type, forced },
807 ) => {
808 _getState = getState
809 _arg = arg
810 _endpoint = endpoint
811 _type = type
812 _forced = forced
813 _extra = extra
814
815 return headers
816 },
817 })
818
819 const fakeAuth0Client = {
820 getTokenSilently: async () => 'fakeToken',
821 }
822
823 const doRequest = async () => {
824 const abortController = new AbortController()
825 return baseQuery(
826 { url: '/echo' },
827 {
828 signal: abortController.signal,
829 abort: (reason) =>
830 // @ts-ignore
831 abortController.abort(reason),
832 dispatch: storeRef.store.dispatch,
833 getState: storeRef.store.getState,
834 extra: fakeAuth0Client,
835 type: 'query',
836 forced: true,
837 endpoint: 'someEndpointName',
838 },
839 {},
840 )
841 }
842
843 await doRequest()
844
845 expect(_getState).toBeDefined()
846 expect(_arg!.url).toBe('/echo')
847 expect(_endpoint).toBe('someEndpointName')
848 expect(_type).toBe('query')
849 expect(_forced).toBe(true)
850 expect(_extra).toBe(fakeAuth0Client)
851 })
852
853 test('can be instantiated with a `ExtraOptions` generic and `extraOptions` will be available in `prepareHeaders', async () => {
854 const prepare = vitest.fn()
855 const baseQuery = fetchBaseQuery({
856 prepareHeaders(headers, api) {
857 expectTypeOf(api.extraOptions).toEqualTypeOf<unknown>()
858 prepare.apply(undefined, arguments as unknown as any[])
859 },
860 })
861 baseQuery('https://example.com', commonBaseQueryApi, {
862 foo: 'baz',
863 bar: 5,
864 })
865 expect(prepare).toHaveBeenCalledWith(
866 expect.anything(),
867 expect.objectContaining({ extraOptions: { foo: 'baz', bar: 5 } }),
868 )
869
870 // ensure types
871 createApi({
872 baseQuery,
873 endpoints(build) {
874 return {
875 testQuery: build.query({
876 query: () => ({ url: '/echo', headers: {} }),
877 extraOptions: {
878 foo: 'asd',
879 bar: 1,
880 },
881 }),
882 testMutation: build.mutation({
883 query: () => ({
884 url: '/echo',
885 method: 'POST',
886 credentials: 'omit',
887 }),
888 extraOptions: {
889 foo: 'qwe',
890 bar: 15,
891 },
892 }),
893 }
894 },
895 })
896 })
897 })
898
899 test('can pass `headers` into `fetchBaseQuery`', async () => {
900 let request: any
901
902 const token = 'accessToken'
903
904 const _baseQuery = fetchBaseQuery({
905 baseUrl,
906 headers: { authorization: `Bearer ${token}` },
907 })
908
909 const doRequest = async () =>
910 _baseQuery({ url: '/echo' }, commonBaseQueryApi, {})
911
912 ;({ data: request } = await doRequest())
913
914 expect(request.headers['authorization']).toBe(`Bearer ${token}`)
915 })
916
917 test('lets a header be undefined', async () => {
918 let request: any
919 ;({ data: request } = await baseQuery(
920 { url: '/echo', headers: undefined },
921 commonBaseQueryApi,
922 {},
923 ))
924
925 expect(request.headers['fake']).toBe(defaultHeaders['fake'])
926 expect(request.headers['delete']).toBe(defaultHeaders['delete'])
927 expect(request.headers['delete2']).toBe(defaultHeaders['delete2'])
928 })
929
930 test('allows for possibly undefined header key/values', async () => {
931 const banana = '1' as '1' | undefined
932 let request: any
933 ;({ data: request } = await baseQuery(
934 { url: '/echo', headers: { banana } },
935 commonBaseQueryApi,
936 {},
937 ))
938
939 expect(request.headers['banana']).toBe('1')
940 expect(request.headers['fake']).toBe(defaultHeaders['fake'])
941 expect(request.headers['delete']).toBe(defaultHeaders['delete'])
942 expect(request.headers['delete2']).toBe(defaultHeaders['delete2'])
943 })
944
945 test('strips undefined values from the headers', async () => {
946 const banana = undefined as '1' | undefined
947 let request: any
948 ;({ data: request } = await baseQuery(
949 { url: '/echo', headers: { banana } },
950 commonBaseQueryApi,
951 {},
952 ))
953
954 expect(request.headers['banana']).toBeUndefined()
955 expect(request.headers['fake']).toBe(defaultHeaders['fake'])
956 expect(request.headers['delete']).toBe(defaultHeaders['delete'])
957 expect(request.headers['delete2']).toBe(defaultHeaders['delete2'])
958 })
959
960 describe('Accepts global arguments', () => {
961 test('Global responseHandler', async () => {
962 server.use(
963 http.get(
964 'https://example.com/success',
965 () => HttpResponse.text(`this is not json!`),
966 { once: true },
967 ),
968 )
969
970 const globalizedBaseQuery = fetchBaseQuery({
971 baseUrl,
972 responseHandler: 'text',
973 })
974
975 const req = globalizedBaseQuery(
976 { url: '/success' },
977 commonBaseQueryApi,
978 {},
979 )
980 expect(req).toBeInstanceOf(Promise)
981 const res = await req
982 expect(res).toBeInstanceOf(Object)
983 expect(res.meta?.request).toBeInstanceOf(Request)
984 expect(res.meta?.response).toBeInstanceOf(Object)
985 expect(res.error).toBeUndefined()
986 expect(res.data).toEqual(`this is not json!`)
987 })
988
989 test('Global responseHandler: content-type with text response', async () => {
990 server.use(
991 http.get(
992 'https://example.com/success',
993 () => HttpResponse.text(`this is plain text!`),
994 { once: true },
995 ),
996 )
997
998 const globalizedBaseQuery = fetchBaseQuery({
999 baseUrl,
1000 responseHandler: 'content-type',
1001 })
1002
1003 const res = await globalizedBaseQuery(
1004 { url: '/success' },
1005 commonBaseQueryApi,
1006 {},
1007 )
1008
1009 expect(res.error).toBeUndefined()
1010 expect(res.data).toEqual(`this is plain text!`)
1011 expect(res.meta?.response?.headers.get('content-type')).toEqual(
1012 'text/plain',
1013 )
1014 })
1015
1016 test('Global responseHandler: content-type with JSON response', async () => {
1017 server.use(
1018 http.get(
1019 'https://example.com/success',
1020 () => HttpResponse.json({ message: 'this is json!' }),
1021 { once: true },
1022 ),
1023 )
1024
1025 const globalizedBaseQuery = fetchBaseQuery({
1026 baseUrl,
1027 responseHandler: 'content-type',
1028 })
1029
1030 const res = await globalizedBaseQuery(
1031 { url: '/success' },
1032 commonBaseQueryApi,
1033 {},
1034 )
1035
1036 expect(res.error).toBeUndefined()
1037 expect(res.data).toEqual({ message: 'this is json!' })
1038 expect(res.meta?.response?.headers.get('content-type')).toEqual(
1039 'application/json',
1040 )
1041 })
1042
1043 test('Global responseHandler: content-type can be overridden at endpoint level', async () => {
1044 server.use(
1045 http.get(
1046 'https://example.com/success',
1047 () => HttpResponse.text(`this is text but will be parsed as json`),
1048 { once: true },
1049 ),
1050 )
1051
1052 const globalizedBaseQuery = fetchBaseQuery({
1053 baseUrl,
1054 responseHandler: 'content-type',
1055 })
1056
1057 // Override global content-type handler with explicit text handler
1058 const res = await globalizedBaseQuery(
1059 { url: '/success', responseHandler: 'text' },
1060 commonBaseQueryApi,
1061 {},
1062 )
1063
1064 expect(res.error).toBeUndefined()
1065 expect(res.data).toEqual(`this is text but will be parsed as json`)
1066 })
1067
1068 test('Global responseHandler: content-type with error response (text)', async () => {
1069 const errorMessage = 'Internal Server Error'
1070 server.use(
1071 http.get('https://example.com/error', () =>
1072 HttpResponse.text(errorMessage, { status: 500 }),
1073 ),
1074 )
1075
1076 const globalizedBaseQuery = fetchBaseQuery({
1077 baseUrl,
1078 responseHandler: 'content-type',
1079 })
1080
1081 const res = await globalizedBaseQuery(
1082 { url: '/error' },
1083 commonBaseQueryApi,
1084 {},
1085 )
1086
1087 expect(res.error).toEqual({
1088 status: 500,
1089 data: errorMessage,
1090 })
1091 expect(res.meta?.response?.headers.get('content-type')).toEqual(
1092 'text/plain',
1093 )
1094 })
1095
1096 test('Global responseHandler: content-type with error response (JSON)', async () => {
1097 const errorData = { error: 'Something went wrong', code: 'ERR_500' }
1098 server.use(
1099 http.get('https://example.com/error', () =>
1100 HttpResponse.json(errorData, { status: 500 }),
1101 ),
1102 )
1103
1104 const globalizedBaseQuery = fetchBaseQuery({
1105 baseUrl,
1106 responseHandler: 'content-type',
1107 })
1108
1109 const res = await globalizedBaseQuery(
1110 { url: '/error' },
1111 commonBaseQueryApi,
1112 {},
1113 )
1114
1115 expect(res.error).toEqual({
1116 status: 500,
1117 data: errorData,
1118 })
1119 expect(res.meta?.response?.headers.get('content-type')).toEqual(
1120 'application/json',
1121 )
1122 })
1123
1124 test('Global validateStatus', async () => {
1125 const globalizedBaseQuery = fetchBaseQuery({
1126 baseUrl,
1127 validateStatus: (response, body) =>
1128 response.status === 200 && body.success === false ? false : true,
1129 })
1130
1131 // This is a scenario where an API may always return a 200, but indicates there is an error when success = false
1132 const res = await globalizedBaseQuery(
1133 {
1134 url: '/nonstandard-error',
1135 },
1136 commonBaseQueryApi,
1137 {},
1138 )
1139
1140 expect(res.error).toEqual({
1141 status: 200,
1142 data: {
1143 success: false,
1144 message: 'This returns a 200 but is really an error',
1145 },
1146 })
1147 })
1148
1149 test('Global timeout', async () => {
1150 server.use(
1151 http.get(
1152 'https://example.com/empty1',
1153 async ({ request, cookies, params, requestId }) => {
1154 await delay(300)
1155
1156 return HttpResponse.json({
1157 ...request,
1158 cookies,
1159 params,
1160 requestId,
1161 url: new URL(request.url),
1162 headers: headersToObject(request.headers),
1163 })
1164 },
1165 { once: true },
1166 ),
1167 )
1168
1169 const globalizedBaseQuery = fetchBaseQuery({
1170 baseUrl,
1171 timeout: 200,
1172 })
1173
1174 const result = await globalizedBaseQuery(
1175 { url: '/empty1' },
1176 commonBaseQueryApi,
1177 {},
1178 )
1179
1180 expect(result?.error).toEqual({
1181 status: 'TIMEOUT_ERROR',
1182 error: expect.stringMatching(/^TimeoutError/),
1183 })
1184 })
1185 })
1186})
1187
1188describe('fetchFn', () => {
1189 test('accepts a custom fetchFn', async () => {
1190 const baseUrl = 'https://example.com'
1191 const params = new URLSearchParams({ apple: 'fruit' })
1192
1193 const baseQuery = fetchBaseQuery({
1194 baseUrl,
1195 fetchFn: nodeFetch as any,
1196 })
1197 let request: any
1198 ;({ data: request } = await baseQuery(
1199 { url: '/echo', params },
1200 commonBaseQueryApi,
1201 {},
1202 ))
1203
1204 expect(request.url).toEqual(`${baseUrl}/echo?apple=fruit`)
1205 })
1206
1207 test('respects mocking window.fetch after a fetch base query is created', async () => {
1208 const baseUrl = 'https://example.com'
1209 const baseQuery = fetchBaseQuery({ baseUrl })
1210
1211 const fakeResponse = {
1212 ok: true,
1213 status: 200,
1214 text: async () => `{ "url": "mock-return-url" }`,
1215 clone: () => fakeResponse,
1216 }
1217
1218 const spiedFetch = vi.spyOn(window, 'fetch')
1219 spiedFetch.mockResolvedValueOnce(fakeResponse as any)
1220
1221 const { data } = await baseQuery({ url: '/echo' }, commonBaseQueryApi, {})
1222 expect(data).toEqual({ url: 'mock-return-url' })
1223
1224 spiedFetch.mockClear()
1225 })
1226})
1227
1228describe('FormData', () => {
1229 test('sets the right headers when sending FormData', async () => {
1230 const body = new FormData()
1231
1232 body.append('username', 'test')
1233
1234 body.append(
1235 'file',
1236 new Blob([JSON.stringify({ hello: 'there' }, null, 2)], {
1237 type: 'application/json',
1238 }),
1239 )
1240
1241 const res = await baseQuery(
1242 { url: '/echo', method: 'POST', body },
1243 commonBaseQueryApi,
1244 {},
1245 )
1246
1247 const request: any = res.data
1248
1249 expect(request.headers['content-type']).not.toContain('application/json')
1250 })
1251
1252 test('FormData works correctly when prepareHeaders sets Content-Type to application/json', async () => {
1253 // This test covers the exact scenario from issue #4669
1254 const baseQueryWithJsonDefault = fetchBaseQuery({
1255 baseUrl,
1256 prepareHeaders: (headers) => {
1257 // Set default Content-Type for all requests
1258 headers.set('Content-Type', 'application/json')
1259 return headers
1260 },
1261 })
1262
1263 const body = new FormData()
1264 body.append('username', 'test')
1265 body.append(
1266 'file',
1267 new Blob([JSON.stringify({ hello: 'there' }, null, 2)], {
1268 type: 'application/json',
1269 }),
1270 )
1271
1272 const res = await baseQueryWithJsonDefault(
1273 { url: '/echo', method: 'POST', body },
1274 commonBaseQueryApi,
1275 {},
1276 )
1277
1278 const request: any = res.data
1279
1280 // The Content-Type should NOT be application/json when FormData is used
1281 expect(request.headers['content-type']).not.toContain('application/json')
1282 // It should contain multipart/form-data (set automatically by the browser)
1283 expect(request.headers['content-type']).toContain('multipart/form-data')
1284 })
1285
1286 test('FormData works when prepareHeaders conditionally removes Content-Type', async () => {
1287 // This tests the workaround solution from the issue comments
1288 const baseQueryWithConditionalHeader = fetchBaseQuery({
1289 baseUrl,
1290 prepareHeaders: (headers, { arg }) => {
1291 // Check if body is FormData and skip setting Content-Type
1292 if ((arg as FetchArgs).body instanceof FormData) {
1293 // Delete Content-Type to let browser set it automatically
1294 headers.delete('Content-Type')
1295 } else {
1296 // Set default Content-Type for non-FormData requests
1297 headers.set('Content-Type', 'application/json')
1298 }
1299 return headers
1300 },
1301 })
1302
1303 const body = new FormData()
1304 body.append('username', 'test')
1305 body.append('file', new Blob(['test content'], { type: 'text/plain' }))
1306
1307 const res = await baseQueryWithConditionalHeader(
1308 { url: '/echo', method: 'POST', body },
1309 commonBaseQueryApi,
1310 {},
1311 )
1312
1313 const request: any = res.data
1314
1315 // Should have multipart/form-data set by browser
1316 expect(request.headers['content-type']).toContain('multipart/form-data')
1317 expect(request.headers['content-type']).not.toContain('application/json')
1318 })
1319
1320 test('endpoint-level headers cannot override to multipart/form-data manually', async () => {
1321 // This tests the fetch API quirk mentioned in the issue
1322 const baseQueryWithJsonDefault = fetchBaseQuery({
1323 baseUrl,
1324 prepareHeaders: (headers) => {
1325 headers.set('Content-Type', 'application/json')
1326 return headers
1327 },
1328 })
1329
1330 const body = new FormData()
1331 body.append('test', 'value')
1332
1333 const res = await baseQueryWithJsonDefault(
1334 {
1335 url: '/echo',
1336 method: 'POST',
1337 body,
1338 // Attempting to manually set multipart/form-data (this won't work as expected)
1339 headers: { 'Content-Type': 'multipart/form-data' },
1340 },
1341 commonBaseQueryApi,
1342 {},
1343 )
1344
1345 const request: any = res.data
1346
1347 // Due to prepareHeaders running after endpoint headers,
1348 // and the fetch API not allowing manual multipart/form-data setting,
1349 // this demonstrates the problem from the issue
1350 // The actual behavior depends on fetchBaseQuery implementation
1351 expect(request.headers['content-type']).toBeDefined()
1352 })
1353
1354 test('non-FormData requests still get application/json from prepareHeaders', async () => {
1355 // Verify that the workaround doesn't break normal JSON requests
1356 const baseQueryWithConditionalHeader = fetchBaseQuery({
1357 baseUrl,
1358 prepareHeaders: (headers, { arg }) => {
1359 if (!((arg as FetchArgs).body instanceof FormData)) {
1360 headers.set('Content-Type', 'application/json')
1361 }
1362 return headers
1363 },
1364 })
1365
1366 const jsonBody = { test: 'value' }
1367
1368 const res = await baseQueryWithConditionalHeader(
1369 { url: '/echo', method: 'POST', body: jsonBody },
1370 commonBaseQueryApi,
1371 {},
1372 )
1373
1374 const request: any = res.data
1375
1376 // Regular JSON requests should still get application/json
1377 expect(request.headers['content-type']).toBe('application/json')
1378 expect(request.body).toEqual(jsonBody)
1379 })
1380})
1381
1382describe('Accept header handling', () => {
1383 test('sets Accept header to application/json for json responseHandler', async () => {
1384 let request: any
1385 ;({ data: request } = await baseQuery(
1386 { url: '/echo', responseHandler: 'json' },
1387 commonBaseQueryApi,
1388 {},
1389 ))
1390
1391 expect(request.headers['accept']).toBe('application/json')
1392 })
1393
1394 test('sets Accept header to application/json by default (json is default responseHandler)', async () => {
1395 let request: any
1396 ;({ data: request } = await baseQuery(
1397 { url: '/echo' },
1398 commonBaseQueryApi,
1399 {},
1400 ))
1401
1402 expect(request.headers['accept']).toBe('application/json')
1403 })
1404
1405 test('sets Accept header for text responseHandler', async () => {
1406 // Create a baseQuery with text as the global responseHandler
1407 const textBaseQuery = fetchBaseQuery({
1408 baseUrl,
1409 responseHandler: 'text',
1410 })
1411
1412 let request: any
1413 // Override to json just for this test so we can inspect the echoed request object
1414 ;({ data: request } = await textBaseQuery(
1415 { url: '/echo', responseHandler: 'json' },
1416 commonBaseQueryApi,
1417 {},
1418 ))
1419
1420 // The endpoint-level 'json' responseHandler overrides the global 'text',
1421 // so the Accept header should be application/json
1422 expect(request.headers['accept']).toBe('application/json')
1423 })
1424
1425 test('does not override explicit Accept header from endpoint', async () => {
1426 let request: any
1427 ;({ data: request } = await baseQuery(
1428 {
1429 url: '/echo',
1430 responseHandler: 'json',
1431 headers: { Accept: 'application/xml' },
1432 },
1433 commonBaseQueryApi,
1434 {},
1435 ))
1436
1437 expect(request.headers['accept']).toBe('application/xml')
1438 })
1439
1440 test('does not override Accept header set in prepareHeaders', async () => {
1441 const customBaseQuery = fetchBaseQuery({
1442 baseUrl,
1443 prepareHeaders: (headers) => {
1444 headers.set('Accept', 'application/vnd.api+json')
1445 return headers
1446 },
1447 })
1448
1449 let request: any
1450 ;({ data: request } = await customBaseQuery(
1451 { url: '/echo', responseHandler: 'json' },
1452 commonBaseQueryApi,
1453 {},
1454 ))
1455
1456 expect(request.headers['accept']).toBe('application/vnd.api+json')
1457 })
1458
1459 test('does not set Accept header for content-type responseHandler', async () => {
1460 let request: any
1461 ;({ data: request } = await baseQuery(
1462 { url: '/echo', responseHandler: 'content-type' },
1463 commonBaseQueryApi,
1464 {},
1465 ))
1466
1467 // Should either not have accept header or have a permissive one
1468 // content-type handler adapts to whatever server sends
1469 const acceptHeader = request.headers['accept']
1470 if (acceptHeader) {
1471 expect(acceptHeader).toMatch(/\*\/\*/)
1472 }
1473 })
1474
1475 test('respects global responseHandler for Accept header', async () => {
1476 const textBaseQuery = fetchBaseQuery({
1477 baseUrl,
1478 responseHandler: 'text',
1479 })
1480
1481 let request: any
1482 // Override to json just for this test so we can inspect the echoed request object
1483 ;({ data: request } = await textBaseQuery(
1484 { url: '/echo', responseHandler: 'json' },
1485 commonBaseQueryApi,
1486 {},
1487 ))
1488
1489 // The endpoint-level 'json' responseHandler overrides the global 'text',
1490 // so the Accept header should be application/json (proving endpoint-level takes precedence)
1491 expect(request.headers['accept']).toBe('application/json')
1492 })
1493})
1494
1495describe('still throws on completely unexpected errors', () => {
1496 test('', async () => {
1497 const error = new Error('some unexpected error')
1498 const req = baseQuery(
1499 {
1500 url: '/success',
1501 validateStatus() {
1502 throw error
1503 },
1504 },
1505 commonBaseQueryApi,
1506 {},
1507 )
1508 expect(req).toBeInstanceOf(Promise)
1509 await expect(req).rejects.toBe(error)
1510 })
1511})
1512
1513describe('timeout', () => {
1514 test('throws a timeout error when a request takes longer than specified timeout duration', async () => {
1515 server.use(
1516 http.get(
1517 'https://example.com/empty2',
1518 async ({ request, cookies, params, requestId }) => {
1519 await delay(300)
1520
1521 return HttpResponse.json({
1522 ...request,
1523 url: new URL(request.url),
1524 cookies,
1525 params,
1526 requestId,
1527 headers: headersToObject(request.headers),
1528 })
1529 },
1530 { once: true },
1531 ),
1532 )
1533
1534 const result = await baseQuery(
1535 { url: '/empty2', timeout: 200 },
1536 commonBaseQueryApi,
1537 {},
1538 )
1539
1540 expect(result?.error).toEqual({
1541 status: 'TIMEOUT_ERROR',
1542 error: expect.stringMatching(/^TimeoutError/),
1543 })
1544 })
1545})
Note: See TracBrowser for help on using the repository browser.