Nuxt Server 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的服务端上下文和客户端上下文是隔离的,服务端无法直接控制客户端路由跳转。
正确思路:服务端返回特定错误标识,客户端处理重定向
修改服务端错误处理,标记未授权错误
在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); } );客户端调用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

