source: node_modules/@reduxjs/toolkit/src/query/fetchBaseQuery.ts

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

Added visualizations

  • Property mode set to 100644
File size: 11.8 KB
RevLine 
[a762898]1import { joinUrls } from './utils'
2import { isPlainObject } from './core/rtkImports'
3import type { BaseQueryApi, BaseQueryFn } from './baseQueryTypes'
4import type { MaybePromise, Override } from './tsHelpers'
5import { anySignal, timeoutSignal } from './utils/signals'
6
7export type ResponseHandler =
8 | 'content-type'
9 | 'json'
10 | 'text'
11 | ((response: Response) => Promise<any>)
12
13type CustomRequestInit = Override<
14 RequestInit,
15 {
16 headers?:
17 | Headers
18 | string[][]
19 | Record<string, string | undefined>
20 | undefined
21 }
22>
23
24export interface FetchArgs extends CustomRequestInit {
25 url: string
26 params?: Record<string, any>
27 body?: any
28 responseHandler?: ResponseHandler
29 validateStatus?: (response: Response, body: any) => boolean
30 /**
31 * A number in milliseconds that represents that maximum time a request can take before timing out.
32 */
33 timeout?: number
34}
35
36/**
37 * A mini-wrapper that passes arguments straight through to
38 * {@link [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)}.
39 * Avoids storing `fetch` in a closure, in order to permit mocking/monkey-patching.
40 */
41const defaultFetchFn: typeof fetch = (...args) => fetch(...args)
42
43const defaultValidateStatus = (response: Response) =>
44 response.status >= 200 && response.status <= 299
45
46const defaultIsJsonContentType = (headers: Headers) =>
47 /*applicat*/ /ion\/(vnd\.api\+)?json/.test(headers.get('content-type') || '')
48
49export type FetchBaseQueryError =
50 | {
51 /**
52 * * `number`:
53 * HTTP status code
54 */
55 status: number
56 data: unknown
57 }
58 | {
59 /**
60 * * `"FETCH_ERROR"`:
61 * An error that occurred during execution of `fetch` or the `fetchFn` callback option
62 **/
63 status: 'FETCH_ERROR'
64 data?: undefined
65 error: string
66 }
67 | {
68 /**
69 * * `"PARSING_ERROR"`:
70 * An error happened during parsing.
71 * Most likely a non-JSON-response was returned with the default `responseHandler` "JSON",
72 * or an error occurred while executing a custom `responseHandler`.
73 **/
74 status: 'PARSING_ERROR'
75 originalStatus: number
76 data: string
77 error: string
78 }
79 | {
80 /**
81 * * `"TIMEOUT_ERROR"`:
82 * Request timed out
83 **/
84 status: 'TIMEOUT_ERROR'
85 data?: undefined
86 error: string
87 }
88 | {
89 /**
90 * * `"CUSTOM_ERROR"`:
91 * A custom error type that you can return from your `queryFn` where another error might not make sense.
92 **/
93 status: 'CUSTOM_ERROR'
94 data?: unknown
95 error: string
96 }
97
98function stripUndefined(obj: any) {
99 if (!isPlainObject(obj)) {
100 return obj
101 }
102 const copy: Record<string, any> = { ...obj }
103 for (const [k, v] of Object.entries(copy)) {
104 if (v === undefined) delete copy[k]
105 }
106 return copy
107}
108
109// Only set the content-type to json if appropriate. Will not be true for FormData, ArrayBuffer, Blob, etc.
110const isJsonifiable = (body: any) =>
111 typeof body === 'object' &&
112 (isPlainObject(body) ||
113 Array.isArray(body) ||
114 typeof body.toJSON === 'function')
115
116export type FetchBaseQueryArgs = {
117 baseUrl?: string
118 prepareHeaders?: (
119 headers: Headers,
120 api: Pick<
121 BaseQueryApi,
122 'getState' | 'extra' | 'endpoint' | 'type' | 'forced'
123 > & { arg: string | FetchArgs; extraOptions: unknown },
124 ) => MaybePromise<Headers | void>
125 fetchFn?: (
126 input: RequestInfo,
127 init?: RequestInit | undefined,
128 ) => Promise<Response>
129 paramsSerializer?: (params: Record<string, any>) => string
130 /**
131 * By default, we only check for 'application/json' and 'application/vnd.api+json' as the content-types for json. If you need to support another format, you can pass
132 * in a predicate function for your given api to get the same automatic stringifying behavior
133 * @example
134 * ```ts
135 * const isJsonContentType = (headers: Headers) => ["application/vnd.api+json", "application/json", "application/vnd.hal+json"].includes(headers.get("content-type")?.trim());
136 * ```
137 */
138 isJsonContentType?: (headers: Headers) => boolean
139 /**
140 * Defaults to `application/json`;
141 */
142 jsonContentType?: string
143
144 /**
145 * Custom replacer function used when calling `JSON.stringify()`;
146 */
147 jsonReplacer?: (this: any, key: string, value: any) => any
148} & RequestInit &
149 Pick<FetchArgs, 'responseHandler' | 'validateStatus' | 'timeout'>
150
151export type FetchBaseQueryMeta = { request: Request; response?: Response }
152
153/**
154 * This is a very small wrapper around fetch that aims to simplify requests.
155 *
156 * @example
157 * ```ts
158 * const baseQuery = fetchBaseQuery({
159 * baseUrl: 'https://api.your-really-great-app.com/v1/',
160 * prepareHeaders: (headers, { getState }) => {
161 * const token = (getState() as RootState).auth.token;
162 * // If we have a token set in state, let's assume that we should be passing it.
163 * if (token) {
164 * headers.set('authorization', `Bearer ${token}`);
165 * }
166 * return headers;
167 * },
168 * })
169 * ```
170 *
171 * @param {string} baseUrl
172 * The base URL for an API service.
173 * Typically in the format of https://example.com/
174 *
175 * @param {(headers: Headers, api: { getState: () => unknown; arg: string | FetchArgs; extra: unknown; endpoint: string; type: 'query' | 'mutation'; forced: boolean; }) => Headers} prepareHeaders
176 * An optional function that can be used to inject headers on requests.
177 * Provides a Headers object, most of the `BaseQueryApi` (`dispatch` is not available), and the arg passed into the query function.
178 * Useful for setting authentication or headers that need to be set conditionally.
179 *
180 * @link https://developer.mozilla.org/en-US/docs/Web/API/Headers
181 *
182 * @param {(input: RequestInfo, init?: RequestInit | undefined) => Promise<Response>} fetchFn
183 * Accepts a custom `fetch` function if you do not want to use the default on the window.
184 * Useful in SSR environments if you need to use a library such as `isomorphic-fetch` or `cross-fetch`
185 *
186 * @param {(params: Record<string, unknown>) => string} paramsSerializer
187 * An optional function that can be used to stringify querystring parameters.
188 *
189 * @param {(headers: Headers) => boolean} isJsonContentType
190 * An optional predicate function to determine if `JSON.stringify()` should be called on the `body` arg of `FetchArgs`
191 *
192 * @param {string} jsonContentType Used when automatically setting the content-type header for a request with a jsonifiable body that does not have an explicit content-type header. Defaults to `application/json`.
193 *
194 * @param {(this: any, key: string, value: any) => any} jsonReplacer Custom replacer function used when calling `JSON.stringify()`.
195 *
196 * @param {number} timeout
197 * A number in milliseconds that represents the maximum time a request can take before timing out.
198 */
199
200export function fetchBaseQuery({
201 baseUrl,
202 prepareHeaders = (x) => x,
203 fetchFn = defaultFetchFn,
204 paramsSerializer,
205 isJsonContentType = defaultIsJsonContentType,
206 jsonContentType = 'application/json',
207 jsonReplacer,
208 timeout: defaultTimeout,
209 responseHandler: globalResponseHandler,
210 validateStatus: globalValidateStatus,
211 ...baseFetchOptions
212}: FetchBaseQueryArgs = {}): BaseQueryFn<
213 string | FetchArgs,
214 unknown,
215 FetchBaseQueryError,
216 {},
217 FetchBaseQueryMeta
218> {
219 if (typeof fetch === 'undefined' && fetchFn === defaultFetchFn) {
220 console.warn(
221 'Warning: `fetch` is not available. Please supply a custom `fetchFn` property to use `fetchBaseQuery` on SSR environments.',
222 )
223 }
224 return async (arg, api, extraOptions) => {
225 const { getState, extra, endpoint, forced, type } = api
226 let meta: FetchBaseQueryMeta | undefined
227 let {
228 url,
229 headers = new Headers(baseFetchOptions.headers),
230 params = undefined,
231 responseHandler = globalResponseHandler ?? ('json' as const),
232 validateStatus = globalValidateStatus ?? defaultValidateStatus,
233 timeout = defaultTimeout,
234 ...rest
235 } = typeof arg == 'string' ? { url: arg } : arg
236
237 let config: RequestInit = {
238 ...baseFetchOptions,
239 signal: timeout
240 ? anySignal(api.signal, timeoutSignal(timeout))
241 : api.signal,
242 ...rest,
243 }
244
245 headers = new Headers(stripUndefined(headers))
246 config.headers =
247 (await prepareHeaders(headers, {
248 getState,
249 arg,
250 extra,
251 endpoint,
252 forced,
253 type,
254 extraOptions,
255 })) || headers
256
257 const bodyIsJsonifiable = isJsonifiable(config.body)
258
259 // Remove content-type for non-jsonifiable bodies to let the browser set it automatically
260 // Exception: keep content-type for string bodies as they might be intentional (text/plain, text/html, etc.)
261 if (
262 config.body != null &&
263 !bodyIsJsonifiable &&
264 typeof config.body !== 'string'
265 ) {
266 config.headers.delete('content-type')
267 }
268
269 if (!config.headers.has('content-type') && bodyIsJsonifiable) {
270 config.headers.set('content-type', jsonContentType)
271 }
272
273 if (bodyIsJsonifiable && isJsonContentType(config.headers)) {
274 config.body = JSON.stringify(config.body, jsonReplacer)
275 }
276
277 // Set Accept header based on responseHandler if not already set
278 if (!config.headers.has('accept')) {
279 if (responseHandler === 'json') {
280 config.headers.set('accept', 'application/json')
281 } else if (responseHandler === 'text') {
282 config.headers.set('accept', 'text/plain, text/html, */*')
283 }
284 // For 'content-type' responseHandler, don't set Accept (let server decide)
285 }
286
287 if (params) {
288 const divider = ~url.indexOf('?') ? '&' : '?'
289 const query = paramsSerializer
290 ? paramsSerializer(params)
291 : new URLSearchParams(stripUndefined(params))
292 url += divider + query
293 }
294
295 url = joinUrls(baseUrl, url)
296
297 const request = new Request(url, config)
298 const requestClone = new Request(url, config)
299 meta = { request: requestClone }
300
301 let response
302 try {
303 response = await fetchFn(request)
304 } catch (e) {
305 return {
306 error: {
307 status:
308 (e instanceof Error ||
309 (typeof DOMException !== 'undefined' &&
310 e instanceof DOMException)) &&
311 e.name === 'TimeoutError'
312 ? 'TIMEOUT_ERROR'
313 : 'FETCH_ERROR',
314 error: String(e),
315 },
316 meta,
317 }
318 }
319 const responseClone = response.clone()
320
321 meta.response = responseClone
322
323 let resultData: any
324 let responseText: string = ''
325 try {
326 let handleResponseError
327 await Promise.all([
328 handleResponse(response, responseHandler).then(
329 (r) => (resultData = r),
330 (e) => (handleResponseError = e),
331 ),
332 // see https://github.com/node-fetch/node-fetch/issues/665#issuecomment-538995182
333 // we *have* to "use up" both streams at the same time or they will stop running in node-fetch scenarios
334 responseClone.text().then(
335 (r) => (responseText = r),
336 () => {},
337 ),
338 ])
339 if (handleResponseError) throw handleResponseError
340 } catch (e) {
341 return {
342 error: {
343 status: 'PARSING_ERROR',
344 originalStatus: response.status,
345 data: responseText,
346 error: String(e),
347 },
348 meta,
349 }
350 }
351
352 return validateStatus(response, resultData)
353 ? {
354 data: resultData,
355 meta,
356 }
357 : {
358 error: {
359 status: response.status,
360 data: resultData,
361 },
362 meta,
363 }
364 }
365
366 async function handleResponse(
367 response: Response,
368 responseHandler: ResponseHandler,
369 ) {
370 if (typeof responseHandler === 'function') {
371 return responseHandler(response)
372 }
373
374 if (responseHandler === 'content-type') {
375 responseHandler = isJsonContentType(response.headers) ? 'json' : 'text'
376 }
377
378 if (responseHandler === 'json') {
379 const text = await response.text()
380 return text.length ? JSON.parse(text) : null
381 }
382
383 return response.text()
384 }
385}
Note: See TracBrowser for help on using the repository browser.