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

using try/catch with an async method inside a middleware causes a nuxt instance unavailable error. #14269

Closed
amrnn90 opened this issue Jul 5, 2022 · 11 comments · Fixed by unjs/unctx#28

Comments

@amrnn90
Copy link

amrnn90 commented Jul 5, 2022

Environment


  • Operating System: Linux
  • Node Version: v17.9.0
  • Nuxt Version: 3.0.0-rc.4
  • Package Manager: npm@8.5.5
  • Builder: vite
  • User Config: modules, runtimeConfig, autoImports
  • Runtime Modules: @nuxtjs/tailwindcss@5.1.3
  • Build Modules: -

Reproduction

Visit the /secret page to see the error:

https://stackblitz.com/edit/nuxt-starter-elpq43?file=middleware%2Fauth.ts

Describe the bug

I'm trying to write an async middleware. However, when catching the rejected promise an error is thrown by nuxt when using navigateTo():

async function fetchUser() {
  throw new Error();
}

export default defineNuxtRouteMiddleware(async (to, from) => {
  let user;

  try {
    user = await fetchUser();
  } catch (e) {
    user = null;
  }

  if (!user) return navigateTo('/');
});

Additional context

No response

Logs

No response

@KonstantinVlasov
Copy link

hi @amrnn90! Did you find workaround?

@antfu antfu self-assigned this Jul 22, 2022
@amrnn90
Copy link
Author

amrnn90 commented Jul 22, 2022

Hi @KonstantinVlasov,
yes, I made a plugin to handle the async operation and share the auth data using Nuxt's useState which I can then check inside the middleware.
You can see an example here:
https://stackblitz.com/edit/nuxt-starter-1dvjxv?file=middleware%2Fauth.ts

Ideally the usage of useState should be abstracted to a composable which contains all auth data and operations used by the rest of the app, so that when the user logs out for example you can update the shared state in a single place and the middleware gets the latest updated value next time it runs.
You can check the code in this repo as an example for implementing this:
https://github.com/amrnn90/breeze-nuxt

@stafyniaksacha
Copy link
Contributor

stafyniaksacha commented Aug 27, 2022

There is also a simple repro here: https://stackblitz.com/edit/nuxt-starter-d61kia?file=pages%2Findex.vue

<script setup lang="ts">
definePageMeta({
  middleware: async () => {
    try {
      // await new Promise((resolve) => setTimeout(resolve, 200)); // having await in both middleware and setup throw
      throw new Error('Nope');
    } catch (error) {
      return navigateTo('/auth');
    }
  },
});

const { data } = await useFetch('/api'); // this throw "nuxt instance unavailable" during initial view
// const { data } = useLazyFetch('/api'); // this works
</script>

<template>
  <div>
    <h1>index</h1>
    <pre>{{ data }}</pre>
  </div>
</template>

@pi0
Copy link
Member

pi0 commented Aug 27, 2022

Hi! Sorry this issue was left unanswered for so long.

Short story: Calling navigateTo in an async context should work out of the box when using defineNuxtPlugin() and defineNuxtRouteMiddleware() wrappers but it is not working when using an await inside try/catch. (Please see the next section below for full story)

Here is a quick solution: (Update: upstream issue fixed. You probably don't need this workaround anymore! Just update the lockfile.)

  • Keep an instance of nuxtApp using useNuxtApp before any async call
  • Call navigateTo via callWithNuxt utility from #app.
import { callWithNuxt } from '#app'

export default defineNuxtRouteMiddleware(async (to, from) => {
  const nuxtApp = useNuxtApp()
  try {
    user = await fetchUser()
  } catch (e) {
    user = null
  }
  if (!user) {
    return callWithNuxt(nuxtApp, navigateTo, ['/auth'])
  }
})

Updated sandbox: https://stackblitz.com/edit/nuxt-starter-u5esgu?file=middleware%2Fauth.ts,app.vue

And the story...

Let me explain what happens when using navigateTo inside an async function and after async/await and how composables work.

The way Vue.js composition API (and Nuxt composables similarly) work is depending on an implicit context. During the lifecycle, vue sets the temporary instance of the current component (and nuxt temporary instance of nuxtApp) to a global variable and unsets it in same tick. When rendering on the server side, there are multiple requests from different users and nuxtApp running in a same global context. Because of this, nuxt and vue immediately unset this global instance to avoid leaking a shared reference between two users or components.

What it does means? Composition API and Nuxt Composables are only available during lifecycle and in same tick before any async operation:

// --- vue internal ---
const _vueInstance = null
const getCurrentInstance = () => _vueInstance
// ---

// Vue / Nuxt sets a global variable referencing to current component in _vueInstance when calling setup()
async setup() {
  getCurrentInstance() // Works
  await someAsyncOperation() // Vue unsets the context in same tick before async operation!
  getCurrentInstance() // null
}

The classic solution to this, is caching the current instance on first call to a local variable like const instance = getCurrentInstance() and use it in next composables but the issue is that any nested composable calls now needs to explicitly accept the instance as an argument and not depend on magical implicit context of composition-api. This is design limitation with composables and not an issue per-se.

To overcome this limitation, Vue does some dark magic when compiling our application code and restores context after each call for <script setup>:

const __instance = getCurrentInstance() // Generated by vue compiler
getCurrentInstance() // Works!
await someAsyncOperation() // Vue unsets the context
__restoreInstance(__instance) // Generated by vue compiler
getCurrentInstance() // Still works!

For a better description of what Vue actually does, see unjs/unctx#2 (comment).

In Nuxt, we have an (internal) utility callWithNuxt utility that we can use to to restore context similar to how <script setup> works which i used for solution above.

Nuxt 3 internally use unjs/unctx to support composables similar to vue for plugins and middleware. This enabled us to make composables like navigateTo() to work without directly passing nuxtApp to them. This brings in all DX and Performance (of tree-shaking) benefits Vue Composition has to the whole Nuxt framework.

With Nuxt composables, we have the same design of Vue Composition API therefore needed a similar solution to magically do this transform. Check out unjs/unctx#2 (Proposal), unjs/unctx#4 (Transform implementation), and nuxt/framework#3884 (Integration to Nuxt).

Vue currently only supports async context restoration for <script setup> for async/await usage. For Nuxt, we additionally added magic transform for defineNuxtPlugin() and defineNuxtRouteMiddleware()! This means when you use them, Nuxt automatically transforms them with context restoration.

The..Bug..: The unctx transformation to automatically restore context seems buggy with try/catch statements containing await which we have to solve in order to remove the requirement of the workaround I suggested above.

@stafyniaksacha
Copy link
Contributor

Hey Pooya! Thanks for the complete answer <3
This story brings to light many things! Maybe this should be somewhere on the documentation?

@pi0
Copy link
Member

pi0 commented Aug 27, 2022

Docs ~> #14723


Your welcome @stafyniaksacha :) I'm sure you got it from the explanation above but for reference, also inline middleware needs the wrapper to leverage the auto transform (and typing!)

definePageMeta({
  middleware: defineNuxtRouteMiddleware(async () => {
    await new Promise((resolve) => setTimeout(resolve, 200)); // having await in
    return navigateTo('/auth');
  }),
})

@antfu
Copy link
Member

antfu commented Aug 29, 2022

Upstream fix: unjs/unctx#28

@pi0
Copy link
Member

pi0 commented Aug 29, 2022

Issue should be solved now! Make sure to update the lockfile for fix.

https://stackblitz.com/edit/nuxt-starter-u5esgu?file=middleware%2Fauth.ts,app.vue

@stafyniaksacha
Copy link
Contributor

You're awesome @pi0 @antfu :)

BracketJohn referenced this issue in sidebase/nuxt-auth Nov 17, 2022
… add docs security section, add docs custom sign in page section (#31)

* feat: make useSession isomorph :confetti:

* feat(docs): more realistic credentials flow

* polish: security section in docs, rename some useSession internals for more consistency

* docs: add isomorphic auth to docs

* Revert "docs: add isomorphic auth to docs"

This reverts commit 4cfaa3e.

* docs: add isomorphic auth to docs

* feat: switch to approach that avoids double-async nested composables (see https://github.com/nuxt/framework/issues/5740\#issuecomment-1229197529)

* polish: remove bad redirect fallback, fix typing

* polish: use FetchOptions directly, add ofetch as dev-dep

* docs: add section on custom signin page
@radudalbea
Copy link

Hi,

I am having the same issue but with a request from a created method. Basically I am trying to do a simple fetch outside the setup function.

async created() {
await this.fetchData();
}

In the fetch method I'm trying to call nuxt auth composable to retrieve the status of the user.

const { status, data } = useAuth();

The error is from useAuth(); I tried using callWithNuxt(who by the way is nowhere mentioned in the docs) and it doesn't work. If I do a entire refactoring and put the call in the setup script or method it works and I understand that I have an option to fix it but the most common use case is to do fetch from create or any other functions. I don't understand how an issue with some many complaints it's closed. Not every component can be written with the setup method in the best way and the main thing of Vue is that it let's you use both composition and options api. Here is still has an advantage over react.

Could you give us another solution to avoid this error?

Thanks.

@embarq
Copy link

embarq commented Aug 31, 2023

Hi,

I am having the same issue but with a request from a created method. Basically I am trying to do a simple fetch outside the setup function.

async created() { await this.fetchData(); }

In the fetch method I'm trying to call nuxt auth composable to retrieve the status of the user.

const { status, data } = useAuth();

The error is from useAuth(); I tried using callWithNuxt(who by the way is nowhere mentioned in the docs) and it doesn't work. If I do a entire refactoring and put the call in the setup script or method it works and I understand that I have an option to fix it but the most common use case is to do fetch from create or any other functions. I don't understand how an issue with some many complaints it's closed. Not every component can be written with the setup method in the best way and the main thing of Vue is that it let's you use both composition and options api. Here is still has an advantage over react.

Could you give us another solution to avoid this error?

Thanks.

I ran into this issue too(it was way too dank to debug and fix btw).

I feel like I understand better now thanks to an expanded explanation in the #14269 (comment).

I was able to fix my stuff by following how it's done in the next-auth: https://github.com/sidebase/nuxt-auth/blob/main/src/runtime/composables/authjs/useAuth.ts#L60.

I have a custom page middleware with some async stuff going on and which had a weird behavior, but after wrapping the middleware function with defineNuxtRouteMiddleware as well as refactoring it, everything worked as expected!

Fix that worked for me:

  • in case some route middleware has any async/await action going on in it - wrap it with defineNuxtRouteMiddleware
  • save the reference to the nuxt app
  • pass the reference everywhere you use nuxt composables(useState, navigateTo etc.)
  • use the callWithNuxt

Example:

// utils/data.ts
export const moreCoolStuff = async (nuxt: NuxtApp) => {
  const result = await callWithNuxt(nuxt, () => useFetch('/api/cool-stuff'))
  // ...
}
  
// composables/cool-stuff.ts
export const useCoolStuff = async (nuxt: NuxtApp) => {
  try {
    await moreCoolStuff(nuxt)
  } catch(error) {
    return await callWithNuxt(nuxt, () => navigateTo('/failed'))
  }
}
<!-- page.vue -->
<script setup lang="ts">
definePageMeta({
  middleware: [
    defineNuxtRouteMiddleware(async (to, from) => {
      const nuxt = useNuxtApp()

      return await useCoolStuff(nuxt, to)
    })
  ]
})
</script>

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

Successfully merging a pull request may close this issue.

8 participants