Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TS error Excessive stack depth comparing types when trying to wrap $fetch #470

Open
Baloche opened this issue Sep 5, 2022 · 22 comments
Open

Comments

@Baloche
Copy link

Baloche commented Sep 5, 2022

Environment


  • Operating System: Darwin
  • Node Version: v16.15.1
  • Nuxt Version: 3.0.0-rc.8
  • Package Manager: npm@8.11.0
  • Builder: vite
  • User Config: runtimeConfig, css, vite, typescript, modules, unocss
  • Runtime Modules: @unocss/nuxt@^0.44.7, @vueuse/nuxt@9.1.1
  • Build Modules: -

Reproduction

export const wrappedFetch: typeof $fetch = (request, opts?) => $fetch(request, opts)
wrappedFetch.raw = (request, opts?) => $fetch.raw(request, opts)

Describe the bug

In Nuxt 3.0.0-rc.8, when trying to wrap the $fetch method in a custom method with the same type signature (to set default options for example), typescript throw an Excessive stack depth comparing types error, forcing me to add // @ts-ignore on the line before.

This only happens for $fetch. useFetch can be wrapped without errors.

Is it normal ?

Additional context

No response

Logs

No response

@ozum
Copy link

ozum commented Feb 8, 2023

(nuxt 3.2)

The problem is related to AvailableRouterMethod<R> type called by method attribute in NitroFetchOptions type definition from nitropack package.

interface NitroFetchOptions<R extends NitroFetchRequest> extends FetchOptions {
    method?: Uppercase<AvailableRouterMethod<R>> | AvailableRouterMethod<R>;
}

interface $Fetch<DefaultT = unknown, DefaultR extends NitroFetchRequest = NitroFetchRequest> {
    <T = DefaultT, R extends NitroFetchRequest = DefaultR, O extends NitroFetchOptions<R> = NitroFetchOptions<R>>(request: R, opts?: O): Promise<TypedInternalResponse<R, T, ExtractedRouteMethod<R, O>>>;
    raw<T = DefaultT, R extends NitroFetchRequest = DefaultR, O extends NitroFetchOptions<R> = NitroFetchOptions<R>>(request: R, opts?: O): Promise<FetchResponse<TypedInternalResponse<R, T, ExtractedRouteMethod<R, O>>>>;
    create<T = DefaultT, R extends NitroFetchRequest = DefaultR>(defaults: FetchOptions): $Fetch<T, R>;
}

@daniluk4000
Copy link
Contributor

@pi0 can you take a look at this please? Can still reproduce in latest versions.

@Hebilicious Hebilicious added the bug Something isn't working label Jul 1, 2023 — with Volta.net
@ElYaiko
Copy link

ElYaiko commented Jul 16, 2023

Still having this issue with:

import type { AvailableRouterMethod, NitroFetchRequest, NitroFetchOptions } from 'nitropack';

const $fetchApi = async <
  ResT = void,
  ReqT extends NitroFetchRequest = NitroFetchRequest,
  Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>
>(
  req: ReqT, opts?: NitroFetchOptions<ReqT>
)

@gerritvanaaken
Copy link

Same here, problem still present:

const result:CasirestToken = await $fetch(settings.casirest.dev.endpoint + '/tokens', {
	method: 'POST',
	headers: {
		'X-API-Key': settings.casirest.dev.apikey
	},
	body: creds
});
console.log(result);

@pi0
Copy link
Member

pi0 commented Jul 24, 2023

/cc @danielroe

@maxdzin
Copy link

maxdzin commented Jul 24, 2023

It is the same when trying to do with useFetch or useAsyncData.
For those who stuck with useFetch, I managed it as this:

export const useFetchApi = <T>(
  ...[request, options]: Parameters<typeof useFetch<T>>
): ReturnType<typeof useFetch<T>> => {
  return useFetch<T>(request, {
    ...options,
    async onRequest({ options }) {
      const accessToken = await useAuthStore().getAccessToken()

      if (accessToken) {
        const headers = new Headers(options.headers)

        headers.set('Accept', 'application/json')
        headers.set('Authorization', `Bearer ${accessToken}`)

        options.headers = headers
      }
    },
  })
}

@lacorde
Copy link

lacorde commented Aug 24, 2023

Finally found this workaround for $fetch, if anyone is interested :

import type {
  NitroFetchRequest,
} from "nitropack";

export function $api<
  T = unknown,
  R extends NitroFetchRequest = NitroFetchRequest
>(
  request: Parameters<typeof $fetch<T, R>>[0],
  opts?: Partial<Parameters<typeof $fetch<T, R>>[1]>
) {
  return $fetch<T, R>(request, {
    // add your custom options here
    ...opts,
  });
}

@jervalles
Copy link

jervalles commented Sep 6, 2023

having the same issue with that code:

const { data: cities, error: cityError } = await useAsyncData(
  "posts",
  () =>
    // @ts-ignore
    $fetch("/api/tool/data/city/", {
      query: { departmentcode: departmentsSelect.value?.code },
    }),
  {
    immediate: false,
    watch: [departmentsSelect],
  }
);

if (cityError.value) {
  errorsHandler(cityError.value);
}

my error:

image

@Hebilicious
Copy link
Contributor

having the same issue with that code:

const { data: cities, error: cityError } = await useAsyncData(
  "posts",
  () =>
    // @ts-ignore
    $fetch("/api/tool/data/city/", {
      query: { departmentcode: departmentsSelect.value?.code },
    }),
  {
    immediate: false,
    watch: [departmentsSelect],
  }
);

if (cityError.value) {
  errorsHandler(cityError.value);
}

my error:

image

For this scenario, have you tried using useFetch instead of useAsyncData ? You can pass query parameters too.

@jervalles
Copy link

having the same issue with that code:

const { data: cities, error: cityError } = await useAsyncData(
  "posts",
  () =>
    // @ts-ignore
    $fetch("/api/tool/data/city/", {
      query: { departmentcode: departmentsSelect.value?.code },
    }),
  {
    immediate: false,
    watch: [departmentsSelect],
  }
);

if (cityError.value) {
  errorsHandler(cityError.value);
}

my error:
image

For this scenario, have you tried using useFetch instead of useAsyncData ? You can pass query parameters too.

yes I tried. Same error

@ssotomayor
Copy link

@jervalles try this:

    $fetch<unknown>("/api/tool/data/city/", {
      query: { departmentcode: departmentsSelect.value?.code },
    }),

Worked for me on Nuxt.

@kenhyuwa
Copy link

kenhyuwa commented Jan 7, 2024

finally I wrap $fetch on nuxt3

// types/index.d.ts

type FetchSchemaApi<DefaultApiResponse = unknown> = {
  statusCode: number
  data: DefaultApiResponse
}

declare module 'nitropack' {
  interface $Fetch<DefaultT = unknown, DefaultR extends NitroFetchRequest = NitroFetchRequest> {
    <T = DefaultT, R extends NitroFetchRequest = DefaultR, O extends NitroFetchOptions<R> = NitroFetchOptions<R>>(request: R, opts?: O): Promise<TypedInternalResponse<R, FetchSchemaApi<T>, ExtractedRouteMethod<R, O>>>;
    raw<T = DefaultT, R extends NitroFetchRequest = DefaultR, O extends NitroFetchOptions<R> = NitroFetchOptions<R>>(request: R, opts?: O): Promise<FetchResponse<TypedInternalResponse<R, FetchSchemaApi<T>, ExtractedRouteMethod<R, O>>>>;
    create<T = DefaultT, R extends NitroFetchRequest = DefaultR>(defaults: FetchOptions): $Fetch<FetchSchemaApi<T>, R>;
  }
}
Screenshot 2024-01-07 at 13 43 41

@beejaz
Copy link

beejaz commented Jan 10, 2024

Im on Nuxt 3.9.1 and Vue 3.4.5, I have this error when I use $fetch inside a function. (ignore the redeclare error, just for example)
image

const debounceFn = useDebounceFn(async () => {
  results.value = await $fetch('/api/address/search', {
    query: {
      q: temp.value,
    },
  })
}, 750)

If I move $fetch outside, it doesn't complain
image

results.value = await $fetch('/api/address/search', {
  query: {
    q: temp.value,
  },
})

@Ragura
Copy link

Ragura commented Apr 12, 2024

Have the same problem when using $fetch. Strangely I can't seem to reproduce it in a minimal reproduction, so it might have something to do with a large return type or the Nuxt project structure I have. This is a $fetch GET call without any options, body or query.

Using Nuxt 3.11.2.

@maxdzin
Copy link

maxdzin commented Apr 13, 2024

Yes, this issue is happening in big projects.
The only workaround I found and using for now is to specify certain return type:

$fetch<{
  item: ISomeItemType
  ...
}>(...)

Using the latest Nuxt 3.11.2

@mttzzz
Copy link

mttzzz commented Apr 17, 2024

have same issue :(
image

image

workaround by @kenhyuwa work for me, but typecheck of return type not work anymore

Nuxt project info:                                                                                                                                               07:58:42  

------------------------------
- Operating System: Windows_NT
- Node Version:     v20.7.0
- Nuxt Version:     3.11.2
- CLI Version:      3.11.1
- Nitro Version:    2.9.6
- Package Manager:  pnpm@8.15.6
- Builder:          -
- User Config:      typescript, devtools, app, build, modules, cron, pinia, experimental, auth
- Runtime Modules:  @unocss/nuxt@0.58.9, @bg-dev/nuxt-naiveui@1.13.0, @nuxt/content@2.12.1, @pinia/nuxt@0.5.1, @vueuse/nuxt@10.9.0, @sidebase/nuxt-auth@0.7.1, nuxt-cron@1.5.1

@pi0 pi0 added typescript and removed bug Something isn't working labels May 16, 2024
@Ragura
Copy link

Ragura commented Jul 15, 2024

This issue is still present. The workaround of providing a type to $fetch(), like $fetch<myType>() works, but of course this isn't ideal because we lose automatic type inference. This seems to point towards this automatic inference somehow tripping typescript.

I have tried again and again to reproduce this issue in a minimal app but it just doesn't show up there. It also only shows up for routes of a specific route folder. These routes do deal with numerous deep types inside them, returning large complex JSON data.

I am well aware that this is very difficult to fix if you can't make the error appear anywhere on your end, so it seems we're stuck providing the type hint ourselves for the time being.

@minkoonim
Copy link

this "Excessive stack" error comes and goes, but it's more likely to happen with default GET requests.
Explicitly setting method: 'GET' seems to resolve the issue.

Steps to Reproduce:

/server/api
  └── /something
        ├── index.get.ts
        └── [id].put.ts

Make a default GET request using $fetch, then another request with a different method (e.g., PUT) with param:

const getSomething = $fetch('/api/something')  // Likely to cause "Excessive stack" error
const putSomething = $fetch(/api/something/${id}, { method: 'PUT' })

How I got rid of it:

const getSomething = $fetch('/api/something', { method: 'GET' }) // Explicitly setting GET
const putSomething = $fetch(/api/something/${id}, { method: 'PUT' })

fetch

@ElYaiko
Copy link

ElYaiko commented Nov 13, 2024

this "Excessive stack" error comes and goes, but it's more likely to happen with default GET requests. Explicitly setting method: 'GET' seems to resolve the issue.

Steps to Reproduce:

/server/api
  └── /something
        ├── index.get.ts
        └── [id].put.ts

Make a default GET request using $fetch, then another request with a different method (e.g., PUT) with param:

const getSomething = $fetch('/api/something')  // Likely to cause "Excessive stack" error
const putSomething = $fetch(/api/something/${id}, { method: 'PUT' })

How I got rid of it:

const getSomething = $fetch('/api/something', { method: 'GET' }) // Explicitly setting GET
const putSomething = $fetch(/api/something/${id}, { method: 'PUT' })

fetch fetch

For me still happens, despite I have method defined.

@LouisCassany
Copy link

Same for me, whether I try to wrap $fetch or not, and whether I explicitly set the method or not. It happens with Prisma so I suspect the type are indeed to complex to be inferred.

@ElYaiko
Copy link

ElYaiko commented Nov 14, 2024

Same for me, whether I try to wrap $fetch or not, and whether I explicitly set the method or not. It happens with Prisma so I suspect the type are indeed to complex to be inferred.

Yes, it happens mostly because the types are complex to be inferred, or because there's lots of routes.
This should be optimized somehow, is annoying always having that error everywhere.

@BenocxX
Copy link

BenocxX commented Nov 22, 2024

Same for me. I have a simple login endpoint that returns { redirectTo: string }.

I made a composable around $fetch to handle server-side form validation and BadRequest errors with Zod. Here's my code for it:

/**
 * An helper function that is used to fetch data from the server and handle validation
 * errors. It is used in conjunction with the useForm composable.
 *
 * @param fetcher A function that fetches data from the server, most likely using $fetch
 * @param form The form context object from vee-validate
 * @returns A promise that resolves to the data if the fetch was successful, or rejects with an error object
 */
export const useFormFetch = async <T>(
  fetcher: () => Promise<T>,
  form: FormContext
): Promise<Result<T, typeToFlattenedError<T>>> => {
  try {
    const data = await fetcher();
    useFormError(form).clearErrors();
    return { success: true, data };
  } catch (error: any) {
    if (error.data.statusCode !== 400) {
      throw error;
    }

    const errors = error.data.data.errors as typeToFlattenedError<T>;

    useFormError(form).setErrors(errors);

    return { success: false, error: errors, };
  }
};

And here's how I use it:

const onSubmit = form.handleSubmit(async (values) => {
  const result = await useFormFetch(
    () => $fetch('/api/auth/login', { method: 'POST', body: JSON.stringify(values) }),
    form
  );
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests