nuxt-open-fetch icon indicating copy to clipboard operation
nuxt-open-fetch copied to clipboard

Use request fetch on SSR

Open philschleier opened this issue 1 year ago • 1 comments

Hi,

we are proxying our API through a nuxt catch-all server route. The server route authenticates the API call based on the user's session.

On SSR, this works fine with useFetch(), as it falls back to useRequestFetch() for local calls on the server side:

https://github.com/nuxt/nuxt/blob/a80d1a0d6349bf1003666fc79a513c0d7370c931/packages/nuxt/src/app/composables/fetch.ts#L170-L176

Since the $fetch instance in nuxt-open-fetch is always globalThis.$fetch via createOpenFetch, the above behaviour cannot be used and thus use[Client] behaves differently from useFetch for SSR.

Am I correctly assuming that this can be achieved by basically copying the above snippet to select the $fetch instance into createOpenFetch and would you be open to such a PR?

Thanks

philschleier avatar Apr 03 '24 14:04 philschleier

Hi @philschleier,

Good catch! Yes, PR would be great :)

enkot avatar Apr 08 '24 21:04 enkot

@enkot is this something you can implement? I'm seeing different behavior in SSR as well and it's a blocker in using the package 😬

adamdehaven avatar Jun 17 '24 18:06 adamdehaven

I tried manually providing the useRequestFetch to the $fetch option; however, it does not properly perform the path replacements

adamdehaven avatar Jun 17 '24 18:06 adamdehaven

@philschleier @adamdehaven Should be fixed in https://github.com/enkot/nuxt-open-fetch/releases/tag/v0.9.0 :) Please, let me know if you have any problem.

enkot avatar Jun 18 '24 19:06 enkot

I'll definitely check it out, thanks!

adamdehaven avatar Jun 18 '24 19:06 adamdehaven

@enkot looking at your updated docs example:

export default defineNuxtPlugin({
  enforce: 'pre', // clients will be ready to use by other plugins, Pinia stores etc.
  setup() {
    const clients = useRuntimeConfig().public.openFetch
    const localFetch = useRequestFetch()

    return {
      provide: Object.entries(clients).reduce((acc, [name, client /* should this be options instead? */ ]) => ({
        ...acc,
        [name]: createOpenFetch(localOptions => ({
          ...options,
          ...localOptions,
          onRequest(ctx) {
            console.log('My logging', ctx.request)
            return localOptions?.onRequest?.(ctx)
          }
        }), localFetch)
      }), {})
    }
  }
})

Should client (extracted from the array) actually be options as it was previously? Otherwise, I'm not sure where ...options come from in the new example? (Please double-check me, maybe I'm missing something)

adamdehaven avatar Jun 18 '24 19:06 adamdehaven

I locally switched my configuration to options as suggested above and everything seems to be working correctly for me (so I think just the docs example needs to be fixed)

adamdehaven avatar Jun 18 '24 19:06 adamdehaven

@enkot I'm also noticing that my NuxtApp interface does not automatically pick up my custom client (or any clients).

I tried manually adding the interface but this cannot be resolved when I actually run the build:

import type { paths as ApiPaths } from '#open-fetch-schemas/api'

Error:

Cannot find module '#open-fetch-schemas/api' or its corresponding type declarations.

adamdehaven avatar Jun 18 '24 21:06 adamdehaven

What Nuxt version do you use? Looks like it doesn't resolve on Nuxt 3.11.2 for some reason, but works fine on 3.12.2

enkot avatar Jun 18 '24 22:06 enkot

I'm on 3.12.2 and it's not working for me 😬

I should clarify: when I run prepare or dev it works fine; however, when I run build it errors.

Edit: I'm also using Nuxt layers in a mono repository; unsure if that's related

adamdehaven avatar Jun 18 '24 22:06 adamdehaven

I'm also having to manually declare the $api interface because it doesn't seem to be automatically added for me?

// nuxt-app.d.ts
import type { paths as ApiPaths } from '#open-fetch-schemas/api'
import type { OpenFetchClient } from '#imports'

declare module '#app' {
  interface NuxtApp {
    $api: OpenFetchClient<ApiPaths>
  }
}

adamdehaven avatar Jun 18 '24 22:06 adamdehaven

@adamdehaven Have you tried deleting the lock file and reinstalling deps?? Maybe @nuxt/kit has a different version than Nuxt.

If this doesn't work, could you create a small repro?

enkot avatar Jun 19 '24 07:06 enkot

~~I have tried that, but I can also try manually bumping nuxtkit.~~

Edit: @nuxt/kit is up to date.

The only other difference is my custom client plugin is not enforce: 'pre' but I don't think that should be an issue.

I'm in a mono repo that also uses layers.

I opened this issue (as I've seen the problem several times now): https://github.com/nuxt/nuxt/issues/27693

You could try exporting one extra interface like I outline in the ticket, but unsure if that's an actual solution or just a workaround.

adamdehaven avatar Jun 19 '24 12:06 adamdehaven

@enkot using a mono repo + layers setup like referenced above, I can't get past the error shown below:

image

When I run the dev server everthing is fine; however, when I run the build, my project is choking on the aliased import paths 😢

adamdehaven avatar Jun 21 '24 01:06 adamdehaven

I think I may have figured out the issue 😅

If I create an alias in my nuxt.config.ts as shown here, the file properly resolves for the build:

alias: {
  '#open-fetch-schemas/portal-api': fileURLToPath(new URL('./.nuxt/types/open-fetch/schemas/portal-api.ts', import.meta.url)),
}

I believe the issue is the paths exported for tsconfig alias are incorrect, and that the file containing the types (here as portal-api.ts) that the module generates for each client is *.ts instead of *.d.ts so the import path ends up being incorrect.

I pushed up a PR here that attempts to resolve the issue.

adamdehaven avatar Jun 21 '24 03:06 adamdehaven

Not sure if it's related to this issue (I'm probably doing something silly since I'm new to nuxt-open-fetch) but I'm having problems with SSR using nuxt-open-fetch 0.9.0.

This code server-side renders fine without any hydration errors:

<template>
  {{ data }}
</template>

<script setup lang="ts">
  const data = ref<any>(null);

  // SSR ✅; Ny hydration error ✅
  data.value = (await useFetch("/api/test")).data;
</script>

However, this this code server-side renders (in that I can see the generated code when I view the document source) but flickers (there is a very brief moment when the document is empty). It also generates a hydration error (in which the client expects the data markup not to be rendered):

<template>
  {{ data }}
</template>

<script setup lang="ts">
  const data = ref<any>(null);

  // SSR ✅; Hydration error ❌
  data.value = (await useApi("/test")).data;
</script>

jonkri avatar Jun 21 '24 17:06 jonkri

Oh, perhaps I should mention that I'm also using a catch-all server route.

server/api/[...].ts:

import { defineEventHandler, H3Event, parseCookies, setCookie } from "h3";
import { joinURL } from "ufo";

export default defineEventHandler(async (event: H3Event) => {
  const cookies = parseCookies(event),
    { proxyUrl } = useRuntimeConfig(),
    target = joinURL(proxyUrl, event.path),
    token = cookies?.token;

  setCookie(event, "token", token);

  return proxyRequest(event, target);
});

jonkri avatar Jun 21 '24 17:06 jonkri

I'm not sure what's going on here but I realized that an empty object options argument to useApi got rid of the error:

Without the object (code identical to above):

// SSR ✅; Hydration error ❌
data.value = (await useApi("/test")).data;

With the object:

// SSR ✅; No hydration error ✅
data.value = (await useApi("/test", {})).data;

It does't matter if the object is added or not if I get the client from useNuxtApp:

const { $api } = useNuxtApp();
data.value = await($api as any)("/test"); // SSR ✅; No hydration error ✅
const { $api } = useNuxtApp();
data.value = await($api as any)("/test", {}); // SSR ✅; No hydration error ✅

By the way, $api is of the type unknown for me. I'm using version 3.12.2 of both Nuxt and @nuxt/kit.

jonkri avatar Jun 21 '24 18:06 jonkri