swr icon indicating copy to clipboard operation
swr copied to clipboard

Can't use 'null' as 'url' for axios

Open NotoriousPyro opened this issue 1 year ago • 7 comments

Bug report

Description / Observed Behavior

When trying to use the doco's suggestion of passing null as the URL (https://swr.vercel.app/docs/conditional-fetching), Axios throws an exception:

stack: 'TypeError: Cannot read properties of null (reading 'indexOf')
    at buildURL (webpack-internal:///./node_modules/axios/lib/helpers/buildURL.js:61:29)
    at dispatchXhrRequest (webpack-internal:///./node_modules/axios/lib/adapters/xhr.js:46:47)
    at new Promise (<anonymous>)
    at xhrAdapter (webpack-internal:///./node_modules/axios/lib/adapters/xhr.js:16:10)
    at dispatchRequest (webpack-internal:///./node_modules/axios/lib/core/dispatchRequest.js:58:10)
    at Axios.request (webpack-int…ebpack-internal:///./node_modules/swr/dist/index.mjs:40:53)
    at eval (webpack-internal:///./node_modules/swr/dist/index.mjs:33:71)
    at new Promise (<anonymous>)
    at __awaiter (webpack-internal:///./node_modules/swr/dist/index.mjs:29:12)
    at eval (webpack-internal:///./node_modules/swr/dist/index.mjs:709:108)
    at Array.onRevalidate (webpack-internal:///./node_modules/swr/dist/index.mjs:937:21)
    at revalidateAllKeys (webpack-internal:///./node_modules/swr/dist/index.mjs:395:33)'

If you provide the third type to useSWR to see the typings, you just get red lines because url cannot be null to axios.get:

type AxiosGet = Parameters<typeof axios.get>
const url = (
    requestPath: string | null | undefined = null,
    params: Record<string, any> = {},
    version: string = "v1",
) => {
    if (requestPath === null) return null
    [...]
}
const {data, error} = useSWR<any, any, AxiosGet>(
    [url(requestPath, params), {
        params: params,
    }],
    axios.get
)
No overload matches this call.
  Overload 1 of 8, '(key: [url: string, config?: AxiosRequestConfig<unknown> | undefined], fetcher: ((url: string, config?: AxiosRequestConfig<unknown> | undefined) => any) | null): SWRResponse<...>', gave the following error.
    Type 'string | null' is not assignable to type 'string'.
      Type 'null' is not assignable to type 'string'.
  Overload 2 of 8, '(key: [url: string, config?: AxiosRequestConfig<unknown> | undefined], config: Partial<PublicConfiguration<any, any, (url: string, config?: AxiosRequestConfig<unknown> | undefined) => any>> | undefined): SWRResponse<...>', gave the following error.
    Type 'string | null' is not assignable to type 'string'.
      Type 'null' is not assignable to type 'string'

Therefore any dynamic error handler using useEffect will throw this to the UI.

Expected Behavior

useSWR should have a way to separate the key from the url, however using "" with axios seems to request the root URL of the site so it returns HTML data if you return an empty string instead of null

Repro Steps / Code Example

See above

Additional Context

SWR version. 1.3.0 Add any other context about the problem here.

NotoriousPyro avatar Sep 12 '22 10:09 NotoriousPyro

As a workaround, I send the "" string as URL to axios anyway and allow the request to continue...

Then I just check if the path was actually "" and return null data...

    const url = (
        requestPath: string | null | undefined = null,
        params: Record<string, any> = {},
        version: string = "v1",
    ) => {
        if (requestPath === null) return ""
    }
    const { data, error } = useSWR<any, any, AxiosGet>(
        [url(requestPath, params), {
            params: params,
        }],
        axios.get
    )
    // Workaround for not being able to use null url with axios and useSWR
    if (requestPath === null) return resolve({
        data: null,
        lastModified: new Date()
    })

NotoriousPyro avatar Sep 12 '22 11:09 NotoriousPyro

Seems you're always passing an array key to useSWR which will get stringified, have you tried to use requestPath ? url(...) : null as key?

huozhi avatar Sep 12 '22 12:09 huozhi

I tried multiple variations based on what you said below. When using Parameters it is of type [url: string, options: {}] so the key becomes this.

Using an array is supposed to be supported: https://swr.vercel.app/docs/arguments

            const { data, error } = useSWR<any, any, AxiosGet>(
                requestPath ? [url(requestPath, params), {
                    params: params,
                }] : null,
                axios.get
            )
            const { data, error } = useSWR<any, any, AxiosGet>(
                [requestPath ? url(requestPath, params), {
                    params: params,
                } : null],
                axios.get
            )
            const { data, error } = useSWR<any, any, AxiosGet>(
                [requestPath ? url(requestPath, params) : null, {
                    params: params,
                }],
                axios.get
            )

NotoriousPyro avatar Sep 12 '22 14:09 NotoriousPyro

This is defined here as supporting a tuple: https://github.com/vercel/swr/blob/85c84ceb578cb3d28362659bd9b82a03fd39be6b/_internal/types.ts#L249 SWRKey extends Key which is Arguments...

export declare type Arguments = string | ArgumentsTuple | Record<any, any> | null | undefined | false;

However, axios does not support null urls, so it reports an error...

NotoriousPyro avatar Sep 12 '22 15:09 NotoriousPyro

I wonder if it would be possible to have a separate key apart from the url and other arguments passed to axios, so they key could be set null while the url remains a real url? 🤷

NotoriousPyro avatar Sep 12 '22 15:09 NotoriousPyro

Ok this works... Since axios.request url accepts undefined... Maybe something for the doco if people want the typings to not use Parameters<typeof axios.get> and instead use Parameters<typeof axios.request> ?

type AxiosRequest = Parameters<typeof axios.request>
const { data, error } = useSWR<any, any, AxiosRequest>(
    [{
        url: url(requestPath, params),
        params: params,
    }],
    axios.request
)

const url = (
    requestPath: string | null | undefined,
    params: Record<string, any> = {},
    version: string = "v1",
) => {
    if (!requestPath) return
}

This can be closed then I guess

NotoriousPyro avatar Sep 12 '22 15:09 NotoriousPyro

It seems I spoke too soon, axios.request also throws an error when using undefined as the URL. It would be nice to have control over when axios is called to avoid it... I really want to use the typings so that it is easier to write the options for the axios request. But this just makes its way up to my error handler:

TypeError: Cannot read properties of undefined (reading 'protocol')
    at isURLSameOrigin (isURLSameOrigin.js?8795:57:1)
    at dispatchXhrRequest (xhr.js?1a5c:147:1)
    at new Promise (<anonymous>)
    at xhrAdapter (xhr.js?1a5c:16:1)
    at dispatchRequest (dispatchRequest.js?4dc9:58:1)
    at Axios.request (Axios.js?29fb:109:1)
    at wrap (bind.js?4bea:9:1)
    at eval (index.mjs?d1eb:757:1)
    at step (index.mjs?d1eb:50:1)
    at Object.eval [as next] (index.mjs?d1eb:31:46)
    at eval (index.mjs?d1eb:24:1)
    at new Promise (<anonymous>)
    at __awaiter (index.mjs?d1eb:20:1)
    at eval (index.mjs?d1eb:700:62)
    at eval (index.mjs?d1eb:960:1)
    at commitHookEffectListMount (react-dom.development.js?ac89:23150:1)
    at commitLayoutEffectOnFiber (react-dom.development.js?ac89:23268:1)
    at commitLayoutMountEffects_complete (react-dom.development.js?ac89:24688:1)
    at commitLayoutEffects_begin (react-dom.development.js?ac89:24674:1)
    at commitLayoutEffects (react-dom.development.js?ac89:24612:1)
    at commitRootImpl (react-dom.development.js?ac89:26823:1)
    at commitRoot (react-dom.development.js?ac89:26682:1)
    at finishConcurrentRender (react-dom.development.js?ac89:25981:1)
    at performConcurrentWorkOnRoot (react-dom.development.js?ac89:25809:1)
    at workLoop (scheduler.development.js?bcd2:266:1)
    at flushWork (scheduler.development.js?bcd2:239:1)
    at MessagePort.performWorkUntilDeadline (scheduler.development.js?bcd2:533:1)

To workaround it, I check for an error returned by useSWR and if the url was undefined... if I don't, it makes its way to my error handler:

[...]
if (error) {
    if (
        url !== undefined
        && error.request
        && error.request.status !== 404
    ) return reject(error)

image

NotoriousPyro avatar Sep 13 '22 08:09 NotoriousPyro