#Global 401 error handling on server side

34 messages · Page 1 of 1 (latest)

hasty radishBOT
#

Hi @sage beacon. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

sage beacon
#
import { getCookie } from 'h3'
import { parseError } from '~~/server/utils/errorParser'

export const useApi = (
    event?: H3Event,
    tokenOverride?: string,
    isAuthenticated: boolean = true,
    isAdminRoute = false
) => {
    const config = useRuntimeConfig()
    const isClient = import.meta.client

    const urlPrefix = isAdminRoute ? '/admin' : '/api'
    const baseURL = isClient
        ? config.public.apiBase + urlPrefix
        : config.apiBaseInternal + urlPrefix

    const getToken = () =>
        tokenOverride ||
        (isClient && isAuthenticated
            ? useCookie('token').value
            : event
                ? getCookie(event, 'token')
                : undefined)

    // Standard JSON-based API calls
    const api = async <T>(url: string, options: any = {}): Promise<T> => {
        const token = getToken()

        return await $fetch<T>(url, {
            baseURL,
            ...options,
            headers: {
                accept: 'application/json',
                ...(options?.headers || {}),
                ...(token && isAuthenticated ? { Authorization: `Bearer ${token}` } : {}),
            },
            onResponseError({ response }) {
                parseError(response, event)
            },
        })
    }

    // Multipart upload support
    const useApiMultipart = async <T>(url: string, body: any) => {
        const token = getToken()
        return await $fetch<T>(url, {
            baseURL,
            method: 'POST',
            body,
            headers: {
                ...(token && { Authorization: `Bearer ${token}` }),
                ...body?.headers, // if using stream
            }
        })
    }

    return Object.assign(api, { useApiMultipart })
}
#

And here is my custom parser

import {deleteCookie, H3Event} from "h3";
import {FetchResponse} from "ofetch";


export const parseError = (response: FetchResponse<any>, event?: H3Event) => {
    if (response.status === 401) {
        deleteCookie(event, 'token', { path: '/' })
        throw createError({
            statusCode: 401,
            statusMessage: response?._data?.message ??'Unauthorized',
        })
    }

    const messages = response._data.errors.map(e => e?.propertyPath !== undefined ? e?.propertyPath + ' ' + e.message : e.message).join(', ')

    throw createError({
        statusCode: response.status,
        statusMessage: messages,
    })
};
hasty radishBOT
#

To handle global 401 errors on the server side in Nuxt and redirect the client to the login page—without needing to wrap every API call in a try/catch—you should implement the redirect logic in your custom fetcher or plugin, leveraging Nuxt’s composables and plugin system.

Here’s how you can do it, based on the official Nuxt 3 documentation:

  1. Create a custom $fetch instance in a Nuxt plugin
    In your plugin, you can intercept responses and handle 401 errors globally. When a 401 is detected, you can clear cookies and use navigateTo('/login') to redirect the user. This works both on the client and server side, but for SSR you need to ensure the redirect is handled properly.

    // plugins/api.ts
    export default defineNuxtPlugin((nuxtApp) => {
      const { session } = useUserSession()
    
      const api = $fetch.create({
        baseURL: '<https://api.nuxt.com',> // replace with your backend URL
        onRequest({ options }) {
          if (session.value?.token) {
            options.headers.set('Authorization', `Bearer ${session.value?.token}`)
          }
        },
        async onResponseError({ response }) {
          if (response.status === 401) {
            // Clear cookies or session here if needed
            // Redirect to login
            await nuxtApp.runWithContext(() => navigateTo('/login'))
          }
        }
      })
    
      return {
        provide: {
          api
        }
      }
    })
    

    Now, you can use useNuxtApp().$api in your components or composables, and any 401 error will automatically trigger a redirect to the login page—no need for try/catch in every call.

  2. Use the custom fetcher in your composables or pages

    // composables/useAPI.ts
    import type { UseFetchOptions } from 'nuxt/app'
#

export function useAPI<T>(
url: string | (() => string),
options?: UseFetchOptions<T>,
) {
return useFetch(url, {
...options,
$fetch: useNuxtApp().$api as typeof $fetch
})
}


And in your page/component:
```ts
<script setup>
const { data } = await useAPI('/your-endpoint')
</script>

This approach ensures that all 401 errors are handled in one place, and users are redirected to the login page automatically, both on client and server side, without repetitive error handling in each API call.

References:


Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@sage beacon Kapa.ai is still learning and improving, please let me know how I did by reacting below.

sage beacon
#

@hasty radish can I do this directly in my useApi i sent you?

hasty radishBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Yes, you can handle global 401 error redirection directly in your useAPI composable by passing your custom $fetch instance (with the 401 handling logic) to useFetch via the $fetch option. However, the actual 401 interception and redirection logic should be implemented in the custom $fetch instance, typically provided via a Nuxt plugin. Your useAPI composable would then simply use this instance.

Here’s how the pattern works, as shown in the official Nuxt documentation:

1. Create a custom $fetch instance in a plugin with 401 handling:

// plugins/api.ts
export default defineNuxtPlugin((nuxtApp) => {
  const { session } = useUserSession()

  const api = $fetch.create({
    baseURL: '<https://api.nuxt.com',>
    onRequest({ options }) {
      if (session.value?.token) {
        options.headers.set('Authorization', `Bearer ${session.value?.token}`)
      }
    },
    async onResponseError({ response }) {
      if (response.status === 401) {
        await nuxtApp.runWithContext(() => navigateTo('/login'))
      }
    }
  })

  return {
    provide: {
      api
    }
  }
})

Custom useFetch in Nuxt 3

2. Use this custom $fetch in your useAPI composable:

// composables/useAPI.ts
import type { UseFetchOptions } from 'nuxt/app'

export function useAPI<T>(
  url: string | (() => string),
  options?: UseFetchOptions<T>,
) {
  return useFetch(url, {
    ...options,
    $fetch: useNuxtApp().$api as typeof $fetch
  })
}

Custom useFetch in Nuxt 3

#

Summary:
You do not need to add 401 handling logic directly inside your useAPI composable. Instead, you configure it in your custom $fetch instance (in the plugin), and then use that instance in useAPI. This keeps your error handling centralized and avoids repetitive code.

If you try to handle 401 errors directly inside useAPI (for example, by catching errors and calling navigateTo), you would lose the benefit of centralization and might run into SSR/client differences. The recommended approach is to handle it in the custom $fetch instance as shown above.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@sage beacon Kapa.ai is still learning and improving, please let me know how I did by reacting below.

sage beacon
#

@hasty radish How can I do it in this server utils?

import { getCookie } from 'h3'
import { parseError } from '~~/server/utils/errorParser'

export const useApi = (
    event?: H3Event,
    tokenOverride?: string,
    isAuthenticated: boolean = true,
    isAdminRoute = false
) => {
    const config = useRuntimeConfig()
    const isClient = import.meta.client

    const urlPrefix = isAdminRoute ? '/admin' : '/api'
    const baseURL = isClient
        ? config.public.apiBase + urlPrefix
        : config.apiBaseInternal + urlPrefix

    const getToken = () =>
        tokenOverride ||
        (isClient && isAuthenticated
            ? useCookie('token').value
            : event
                ? getCookie(event, 'token')
                : undefined)

    // Standard JSON-based API calls
    const api = async <T>(url: string, options: any = {}): Promise<T> => {
        const token = getToken()

        return await $fetch<T>(url, {
            baseURL,
            ...options,
            headers: {
                accept: 'application/json',
                ...(options?.headers || {}),
                ...(token && isAuthenticated ? { Authorization: `Bearer ${token}` } : {}),
            },
            onResponseError({ response }) {
                parseError(response, event)
            },
        })
    }

    // Multipart upload support
    const useApiMultipart = async <T>(url: string, body: any) => {
        const token = getToken()
        return await $fetch<T>(url, {
            baseURL,
            method: 'POST',
            body,
            headers: {
                ...(token && { Authorization: `Bearer ${token}` }),
                ...body?.headers, // if using stream
            }
        })
    }

    return Object.assign(api, { useApiMultipart })
}

hasty radishBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

To implement global 401 error handling and redirect the client to the login page in your useApi server util, you need to handle the 401 status inside your onResponseError callback. However, based on the official Nuxt documentation, the recommended and most robust way to handle global redirects (especially for SSR) is to perform the redirect at the plugin or middleware level, not directly in a utility function, because:

  • On the server side, you need to send a 302 redirect response.
  • On the client side, you can use navigateTo('/login').

In your current useApi util, you are calling a parseError function in onResponseError, but you are not performing any redirect. To achieve a global redirect, you would need to enhance your parseError function to handle the 401 status and perform the appropriate redirect logic.

Example approach (based on your util and Nuxt docs):

// server/utils/errorParser.ts
export function parseError(response, event) {
  if (response.status === 401) {
    // Server-side redirect
    if (import.meta.server && event) {
      const res = event.node.res
      if (!res.headersSent) {
        res.writeHead(302, { Location: '/login' })
        res.end()
      }
    }
    // Client-side redirect
    if (import.meta.client) {
      navigateTo('/login')
    }
  }
  // ...other error handling
}

Then, in your useApi util, keep your onResponseError as is:

onResponseError({ response }) {
  parseError(response, event)
},
#

Limitations:

  • This approach is similar to the workaround described in this Stack Overflow answer for handling SSR redirects on 401 errors.
  • The Nuxt documentation recommends handling such logic in a plugin with a custom $fetch instance for better centralization and composability, as shown in the Custom useFetch in Nuxt 3 guide. However, if you must use a utility, the above pattern is the closest match from the knowledge sources.

Summary:
You can perform the redirect in your parseError function by checking the environment and using either a server-side 302 or navigateTo on the client. This will allow you to avoid try/catch in every client call, as the redirect is handled globally in your util.


References:


Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@sage beacon Kapa.ai is still learning and improving, please let me know how I did by reacting below.

sage beacon
#

@hasty radish I do see the redirect in the console but the client side is not redirected

hasty radishBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

If you see the redirect in the console but the client is not actually redirected, this is a known limitation when trying to perform navigation from within a fetch error handler or promise chain, especially during server-side rendering (SSR). The Nuxt documentation and community discussions highlight that navigateTo only works as expected when called in the correct context—such as middleware, plugins, or directly in a Vue component's setup function—not from inside a fetch error handler or utility function during SSR.

From the knowledge sources:

  • On the server side, if you try to redirect after a fetch error (like a 401), the page may still render before the redirect happens, or the redirect may not be respected by the client. This is because the navigation must be handled at the routing/middleware level, not deep inside a utility or after the response has started being sent to the client.
  • On the client side, navigateTo('/login') works if called in the right context, but if you call it from a fetch error handler, it may not trigger navigation as expected, especially if the component has already rendered or if the navigation is not awaited properly.

Recommended solution:
The Nuxt documentation and Stack Overflow discussions recommend handling authentication and redirection in middleware, not in the fetch utility itself. For example, you can use a route middleware to check authentication and redirect before the page is rendered:

#
// middleware/authenticated.ts
export default defineNuxtRouteMiddleware(() => {
  const { loggedIn } = useUserSession()
  if (!loggedIn.value) {
    return navigateTo('/login')
  }
})

This ensures that unauthenticated users are redirected before any protected page or API call is made, avoiding the need to handle redirects in every fetch call or error handler.
Protect App Routes
Stack Overflow: Nuxt 3, how to navigate to a login page if any REST endpoint returns 401?

Summary:
Redirects from within fetch error handlers are unreliable, especially on the server side. Use Nuxt route middleware for authentication checks and redirection to ensure consistent behavior across SSR and client-side navigation.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@sage beacon Kapa.ai is still learning and improving, please let me know how I did by reacting below.

sage beacon
#

@hasty radish I already have a route middleware for auth and the redirecting ther works. But if I'm on a page and do an action that is calling the backend without navigating and stay on the page, the 401 is not redirecting

hasty radishBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You're correct: Nuxt route middleware only runs on navigation, not on API calls made while staying on the same page. If you perform an action (like clicking a button) that triggers a backend call and receive a 401, the middleware won't run, so you need a way to handle this scenario.

Based on the knowledge sources, the recommended approach is to handle 401 errors in your custom fetch logic. On the client side, you can use navigateTo('/login') inside your fetch error handler. However, as you've noticed, this sometimes doesn't trigger a redirect if not called in the right context.

The Stack Overflow discussion confirms that navigateTo works on the client if called after the request fails, but not always on the server or from deep inside a utility. For client-side actions (like button clicks), you can reliably use navigateTo in your fetch error handler:

// In your error parser or fetch error handler
if (response.status === 401 && import.meta.client) {
  navigateTo('/login')
}

This will redirect the user to the login page if a 401 is received during a client-side API call, even if they haven't navigated away from the current page.

Summary:

#

If you are already doing this and the redirect still doesn't work, ensure that your error handler is running on the client (check import.meta.client) and that navigateTo is being called outside of async/await chains that might swallow navigation. If issues persist, you may need to trigger a full page reload as a fallback.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@sage beacon Kapa.ai is still learning and improving, please let me know how I did by reacting below.

sage beacon
#

@hasty radish this means I need to change everywhere I call the backend on my client side ?

hasty radishBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

No, you do not need to change every place where you call the backend on the client side, as long as you centralize your 401 handling in your custom fetch utility or composable.

The recommended approach, as shown in the knowledge sources, is to handle 401 errors globally in your custom fetch logic (for example, in your onResponseError or error parser). This way, any API call that uses your custom fetch will automatically redirect to the login page on a 401, without requiring changes at every call site. Here’s an example pattern:

onResponseError({ response }) {
  if (response.status === 401 && import.meta.client) {
    navigateTo('/login')
  }
}

This ensures that all client-side API calls using your utility will trigger the redirect when a 401 is received, and you do not need to add manual error handling or redirects everywhere you call the backend Stack Overflow: Nuxt 3, how to navigate to a login page if any REST endpoint returns 401?.

So, as long as your API calls go through your centralized utility, you only need to implement the redirect logic once.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@sage beacon Kapa.ai is still learning and improving, please let me know how I did by reacting below.

sage beacon
#

@hasty radish but in my case the custom fetch is only called on the server side

hasty radishBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Thank you for clarifying. Based on your code and your statement, your custom fetch utility (useApi) is only used on the server side. This means that any 401 handling and redirect logic you implement there will only affect server-side requests.

Key point from the knowledge sources:

  • If you want to handle 401 errors and redirect the user on the client side (for example, after an action on a page), your fetch logic must also be used on the client side, not just the server.
  • The Nuxt documentation and community examples show that client-side redirection (using navigateTo('/login')) must be triggered from code that runs in the browser context (i.e., import.meta.client), such as a plugin, composable, or utility that is used both server- and client-side Custom useFetch in Nuxt 3, Stack Overflow: Nuxt 3, how to navigate to a login page if any REST endpoint returns 401?.

If your custom fetch is only used server-side:

  • The client will not be redirected when a 401 occurs after an in-page action, because the redirect logic is never executed in the browser.
  • To achieve global 401 handling and redirection for both SSR and client-side actions, you need to ensure your custom fetch (or a similar error handler) is also used for client-side API calls.
#

Summary:
If your custom fetch is only called on the server side, you cannot handle client-side 401 redirects globally with it. You need to use a fetch utility or plugin that is shared between server and client, and implement the 401 redirect logic for the client context as well. The knowledge sources do not provide a way to handle client-side 401 redirects from a server-only utility.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: