axiosnuxt.jsserver-side-renderingnuxt3.js

In Nuxt 3, how to navigate to a login page if any REST endpoint returns 401?


I am using Nuxt 3 with server-side rendering. For REST requests, I use Axios.

When an user logs in, a refresh token cookie is saved in the browser. Then for all requests to any of the REST endpoints, an access token is included in the header. If no access token is found, one is requested, using the refresh token. If the access token is expired, a new one is requested and the failed request is send again. All of this is handled by Axios interceptors.

Now, the problem that I'm having is that if the refresh token no longer is valid, it is not possible to create a new access token at all. So the access token endpoint returns code 401. But when I try to catch that 401 in my Axios interceptors and navigate to the login page, it will only do so client-side.

To debug the issue, I created this simple page. It still uses Axios, but there are no interceptors. The basic problem seems to be the same though, which is that server-side navigation cannot happen from a promise chain.

<template>
  <div>
    {{ data }}
    {{ error }}
  </div>
</template>

<script setup lang="ts">
const userService = useUserService();
const { data, error } = await useAsyncData('data', () => {
  return userService.getSelf().catch(() => {
    try {
      console.log('handling error');
      navigateTo('/login');
      console.log('navigated to /login');
    } catch (ex) {
      console.log(ex);
    }
  })
});
</script>

Server-side the log says this:

handling error

WARN  [nuxt] useAsyncData should return a value that is not null or undefined or the request may be duplicated on the client side.

[nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function. This is probably not a Nuxt bug. Find out more at https://nuxt.com/docs/guide/concepts/auto-imports#vue-and-nuxt-composables.

So clearly navigateTo fails with an exception since it never logs 'navigated to /login'. As a result of this, the page is rendered. The page that you basically do not have access to any longer.

Client-side the logs says this:

GET http://127.0.0.1:8080/users/self 401 (Unauthorized)
restricted.vue:13 handling error
restricted.vue:15 navigated to /login

So after rendering the /restricted page and submitting the request to the REST endpoint again, client-side, the navigate to /login is successful and you are redirected to /login

Now, if I simply check if the data object is set, and navigate to /login if not, everything works fine. It will return status 302 on the request to /restricted without rendering that page first. Exactly like I would want it to. That code looks like this:

<template>
  <div>
    {{ data }}
    {{ error }}
  </div>
</template>

<script setup lang="ts">
const userService = useUserService();
const { data, error } = await useAsyncData('data', () => {
  return userService.getSelf()
});
if (!data.value) {
  navigateTo('/login')
}
</script>

Of course, this is not a good way to handle it, since the refresh token can become invalid at any given moment, on any of the 100s pages, and during any of the 100s of different REST endpoint calls. What I need is some kind of central handling of all this.

What am I missing? Are there better ways to handle something like this?


Solution

  • What I ended up doing instead was something like this:

    const userService = useUserService();
    const { data, error } = await useAsyncData('data', () => {
      return userService.getSelf().catch(() => {
        if (import.meta.server) {
          const response = requestEvent.node.res;
          if (!response.headersSent) {
            response.writeHead(302, {Location: '/login'});
            response.end();
          } else {
            router.push('/login');
          }
        }
      })
    });
    

    With this I get a correct 302 response and redirect to the login page, if the initial server-side rendered request fails authentication.