You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Nuxt Server API遇未授权错误时如何重定向至指定路由?

问题:Nuxt服务端API拦截器中处理未授权错误并重定向失败

我用Nuxt开发项目,所有外部API请求都通过server/api文件夹处理。需求是当API返回"Unauthorized: Invalid or expired token"错误时,重定向到/页面。我已经用axios拦截器捕获了这个错误并打印日志,但尝试用useRouter().push('/')或redirect('/')时,控制台报错"Vue app aliases are not allowed in server runtime."。

相关代码如下:

server/api/apiService.ts

import axios from 'axios';
import packageJson from '~/package.json';
import { useRouter } from 'nuxt/app';

const config = useRuntimeConfig();

// ---------- dynamic 
interface EnvironmentConfigs {
    [key: string]: string;
}

const environmentConfigs: EnvironmentConfigs = {
    development: config.developmentBaseurl,
    staging: config.stagingBaseurl,
    production: config.productionBaseurl
};

const baseURL = environmentConfigs[packageJson.environment_config];

if (!baseURL) {
    throw new Error('Invalid environment configuration');
}

export const devApiClient = axios.create({
    baseURL,
})

// ---------------------------------------------------------------

// Add response interceptor to handle token expiration
devApiClient.interceptors.response.use(
    (response) => {
        // Do something with response data
        return response;
    },
    (error) => {
        // Do something with response error
        if (error.response.data === 'Unauthorized: Invalid or expired token\n') {
            console.error('Token expired, redirecting to login page...');
            const router = useRouter();
            router.push('/login');
        }

        return Promise.reject(error);
    }
);

server/api/apiEventHandler.ts

import { apiErrorHandler } from './apiErrorHandler';
import packageJson from '~/package.json';

const API_VERSION = packageJson.version;

type AuthType = 'Bearer' | 'Basic';
type DataFormat = 'URLSearchParams' | 'FormData';

export function apiEventHandler(handlerFn: (
    headers: any, params: any, type: any) => Promise<any>,
    authType: AuthType = 'Bearer', // Default to 'Bearer'
    dataFormat: DataFormat = 'URLSearchParams' // Default to URLSearchParams
) {
    return eventHandler(async (event: any) => {
        try {
            let query, token;

            if (dataFormat === 'FormData') {
                query = await readMultipartFormData(event);
                token = query?.find((part: any) => part.name === 'token')?.data.toString();
            } else {
                query = JSON.parse(await readBody(event));
                token = query?.token;
            }
            
            // Initialize headers based on authType
            const headers: any = authType === 'Bearer'
                ? { 'Authorization': `Bearer ${token}` }
                : { 'Authorization': `Basic ${btoa(`${useRuntimeConfig().ssoUsername}:${useRuntimeConfig().ssoPassword}`)}` };

            // Initialize params based on dataFormat
            const params: any = dataFormat === 'URLSearchParams'
                ? new URLSearchParams()
                : new FormData();

            // const params = new URLSearchParams();
            params.append('version', API_VERSION);
            // Check if query.data exists to handle params construction
            if (query.data) {
                for (const item of query.data) {
                    // Dynamically append parameters based on item keys
                    for (const key in item) {
                        if (Object.prototype.hasOwnProperty.call(item, key)) {
                            params.append(key, item[key]);
                        }
                    }
                }
            }

            return await handlerFn(headers, params, query);
        } catch (error: any) {
            return apiErrorHandler(error);
        }
    });
}

server/api/apiErrorHandler.ts

export function apiErrorHandler(error: any) {
    // return { error: error.message || 'Unknown error' };

    // Check if the error response is available
    if (error.response) {
        const { data } = error.response;

        // Check if there is a specific error message
        if (data && typeof data === 'string') {
            return { error: data };
        }

        // Fallback, return the error response to the client
        return {
            // error: data.error.details.reason || data.error || 'An error occurred while processing the request'
            error: data.error || 'An error occurred while processing the request'
        };
    } else {
        // If no error response, return a generic error message
        return {
            error: error.message || 'An internal server error occurred'
        };
    }
}

server/api/apiEndpoints.ts

// list of API Endpoints

export const API_ENDPOINT = {
    LOGIN: 'EXTERNAL_API_ENDPOINT_HERE'
};

composables/useApiPath.ts

export const useApiPaths = () => {
    const config = useRuntimeConfig();
    const baseURL = config.app.baseURL;
    
    const apiPaths = {
        apiLogin: `${baseURL}api/auth/apiLogin`
};

    // console.log('Config:', config);
    // console.log('API Paths:', apiPaths);
    return apiPaths;
};

server/api/auth/apiLogin.ts

import { devApiClient } from '../apiService'
import { API_ENDPOINT } from '../apiEndpoints';
import { apiEventHandler } from '../apiEventHandler';

const handlerFn = async (headers: any, params: any) => {
    // Add specific headers for this API call
    headers['Content-Type'] = 'application/x-www-form-urlencoded';
    
    try {
        const response = await devApiClient.post(API_ENDPOINT.LOGIN, params, { headers });
        const responseData = response.data;

        if (responseData.success) {
            return responseData.data;
        }

        throw new Error('API returned error: ' + responseData.message);
    } catch (error: any) {
        throw error; // Allow the global error handler to handle this
    }
}

export default apiEventHandler(handlerFn);

页面调用API方式

const getLogin = async () => {
    const dataArray = [{ contact: emailField, code: passwordField }];

    const response = await fetch(useApiPaths().apiLogin, {
        method: 'POST',
        body: JSON.stringify({
            token: authStore.sso_token, data: dataArray
        }),
    });
    const responseData = await response.json();
}

我试过两种方案:

  • 使用useRouter().push('/')
  • 使用redirect('/')

都触发相同错误,请问怎么正确在axios拦截器中捕获未授权错误并完成重定向?有没有其他可行思路?


解决方案

核心问题:你在服务端运行时(server/api下的代码)尝试调用客户端的路由工具(useRouter)或者直接用redirect,但Nuxt的服务端上下文和客户端上下文是隔离的,服务端无法直接控制客户端路由跳转。

正确思路:服务端返回特定错误标识,客户端处理重定向

  1. 修改服务端错误处理,标记未授权错误
    在apiErrorHandler.ts中,当捕获到"Unauthorized: Invalid or expired token"错误时,返回一个带有特定状态码和标识的响应:

    export function apiErrorHandler(error: any) {
        if (error.response) {
            const { data } = error.response;
            // 检测未授权错误
            if (data === 'Unauthorized: Invalid or expired token\n') {
                // 返回401状态码和明确的错误类型
                return createError({
                    statusCode: 401,
                    statusMessage: 'Unauthorized',
                    data: { type: 'token-expired' }
                });
            }
            // 其他错误处理逻辑...
            if (data && typeof data === 'string') {
                return { error: data };
            }
            return {
                error: data.error || 'An error occurred while processing the request'
            };
        } else {
            return {
                error: error.message || 'An internal server error occurred'
            };
        }
    }
    

    同时,在apiService.ts的拦截器中,直接抛出错误即可,不需要尝试重定向:

    devApiClient.interceptors.response.use(
        (response) => response,
        (error) => {
            if (error.response.data === 'Unauthorized: Invalid or expired token\n') {
                console.error('Token expired');
            }
            return Promise.reject(error);
        }
    );
    
  2. 客户端调用API时处理错误,执行重定向
    在页面的API调用函数中,捕获401错误,然后用客户端的路由工具跳转:

    const getLogin = async () => {
        const dataArray = [{ contact: emailField, code: passwordField }];
        const router = useRouter();
    
        try {
            const response = await fetch(useApiPaths().apiLogin, {
                method: 'POST',
                body: JSON.stringify({
                    token: authStore.sso_token, data: dataArray
                }),
            });
            
            if (!response.ok) {
                const errorData = await response.json();
                // 检测token过期错误
                if (response.status === 401 && errorData.type === 'token-expired') {
                    // 客户端执行重定向
                    await router.push('/');
                    return;
                }
                // 处理其他错误
                console.error('API error:', errorData);
                return;
            }
            
            const responseData = await response.json();
            // 处理正常响应
        } catch (error) {
            console.error('Request failed:', error);
        }
    }
    

额外优化:全局客户端错误拦截

如果不想每个API调用都重复写错误处理,可以在客户端全局拦截fetch请求:

在plugins/error-handler.ts中创建全局插件:

export default defineNuxtPlugin(() => {
    const router = useRouter();

    // 重写fetch方法添加全局错误处理
    const originalFetch = window.fetch;
    window.fetch = async (...args) => {
        const response = await originalFetch(...args);
        if (!response.ok && response.status === 401) {
            try {
                const errorData = await response.json();
                if (errorData.type === 'token-expired') {
                    await router.push('/');
                }
            } catch (e) {
                // 处理无法解析JSON的情况
                console.error('Failed to parse error response:', e);
            }
        }
        return response;
    };
});

这样所有客户端的fetch请求遇到401且token过期时,都会自动重定向到首页。


内容的提问来源于stack exchange,提问作者Lovi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 16:04:59