Angular无ZoneJs环境下GraphQL错误无法被全局处理器捕获
问题描述
项目中使用全局错误处理器和GraphQL提供者处理所有请求及错误,但GraphQL的errorLink中抛出的错误无法被全局错误处理器捕获(该代码处于Angular上下文之外),组件内抛出的错误则可正常捕获。项目未使用ZoneJs。
GraphQL提供者代码
import { Apollo, APOLLO_OPTIONS } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { ApplicationConfig, inject } from '@angular/core'; import { ApolloClientOptions, InMemoryCache, split } from '@apollo/client/core'; import { environment } from '../environments/environment'; import { onError } from '@apollo/client/link/error'; import { Router } from '@angular/router'; import { HttpHeaders } from '@angular/common/http'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { getMainDefinition } from '@apollo/client/utilities'; import { createClient } from 'graphql-ws'; // @ts-ignore import extractFiles from 'extract-files/extractFiles.mjs'; // @ts-ignore import isExtractableFile from 'extract-files/isExtractableFile.mjs'; const uri = `${environment.base}/graphql` export function apolloOptionsFactory(): ApolloClientOptions<any> { const httpLink = inject(HttpLink); const router = inject(Router) const wsLink = new GraphQLWsLink( createClient({ url: (environment.base).toString() }) ) const errorLink = onError(({ graphQLErrors, networkError }) => { if (graphQLErrors) { graphQLErrors.forEach(e => { if (e.message === 'Unauthorized') router.navigate(['auth/login']) throw new Error(e.message) }) } if (networkError) { throw new Error(networkError.message) } }) return { link: split( ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ) }, wsLink, errorLink.concat(httpLink.create({ uri, extractFiles: (body) => extractFiles(body, isExtractableFile), headers: new HttpHeaders({ 'x-apollo-operation-name': 'some-header-with-file-upload'}) })) ), cache: new InMemoryCache() } } export const graphqlProvider: ApplicationConfig['providers'] = [ Apollo, { provide: APOLLO_OPTIONS, useFactory: apolloOptionsFactory, }, ];
全局错误处理器代码
import { ErrorHandler, Injectable, Injector, inject } from "@angular/core"; import { HttpErrorResponse } from "@angular/common/http"; import { ErrorService } from "./error.service"; import { NotificationService } from "./notification.service"; @Injectable() export class GlobalErrorHandler implements ErrorHandler { injector: Injector = inject(Injector) handleError(error: Error | HttpErrorResponse): void { console.log('SOMETHING SHOULD HAPPEN SINCE ALL ERRORS ARE HANDLED HERE') const errorService = this.injector.get(ErrorService) const notifier = this.injector.get(NotificationService) let message: string message = errorService.getClientErrorMessage(error) notifier.showError(message) } }
GraphQL方法服务代码
export class GraphqlService { public postsQuery!: QueryRef<any> private readonly apollo = inject(Apollo) public mutateFn(mutation: TypedDocumentNode<unknown, any>, variables?: any, isFile: boolean = false): Observable<MutationResult<unknown> | any> { return this.apollo.mutate({ mutation: mutation, variables: variables, errorPolicy: 'all', context: { useMultipart: isFile } }).pipe(map((v) => { return v.data }) ) } }
已尝试的方案
曾尝试使用ApplicationRef.tick(),但出现错误ApplicationRef.tick() is running recursively。
编辑:已按照建议修改代码,但问题仍未解决:
// @ts-ignore const errorCallback = ({ graphQLErrors, networkError }) => { if (graphQLErrors) { graphQLErrors.forEach((e: any) => { if (e.message === 'Unauthorized') router.navigate(['auth/login']) throw new Error(e.message) }) } if (networkError) { throw new Error(networkError.message) } } // @ts-ignore const errorLink = onError(errorCallback.bind(this))
解决方法
由于errorLink的回调运行在Angular上下文之外(未使用ZoneJs),抛出的错误无法自动进入Angular的错误处理流,可通过以下两种方案解决:
方案一:直接调用全局错误处理器
在apolloOptionsFactory中注入全局错误处理器,在errorLink的回调里直接调用其handleError方法,替代抛出错误的方式:
export function apolloOptionsFactory(): ApolloClientOptions<any> { const httpLink = inject(HttpLink); const router = inject(Router); const globalErrorHandler = inject(GlobalErrorHandler); // 注入全局错误处理器 const wsLink = new GraphQLWsLink( createClient({ url: (environment.base).toString() }) ) const errorLink = onError(({ graphQLErrors, networkError }) => { if (graphQLErrors) { graphQLErrors.forEach(e => { if (e.message === 'Unauthorized') { router.navigate(['auth/login']); } // 直接触发全局错误处理逻辑 globalErrorHandler.handleError(new Error(e.message)); }) } if (networkError) { globalErrorHandler.handleError(new Error(networkError.message)); } }) // 其余代码保持不变 }
方案二:将错误传递到Observable订阅流
调整errorLink不直接抛出错误,而是将错误传递到下游Observable,同时修改GraphQL服务的错误策略,确保错误能流到组件订阅环节,被全局处理器捕获:
- 修改
errorLink逻辑:
import { throwError } from 'rxjs'; // ... const errorLink = onError(({ graphQLErrors, networkError, forward }) => { let targetError; if (graphQLErrors) { graphQLErrors.forEach(e => { if (e.message === 'Unauthorized') { router.navigate(['auth/login']); } targetError = new Error(e.message); }) } else if (networkError) { targetError = new Error(networkError.message); } // 将错误传递到下游Observable return forward(throwError(() => targetError)); })
- 修改GraphQL服务的
mutateFn:
import { catchError, throwError } from 'rxjs'; // ... public mutateFn(mutation: TypedDocumentNode<unknown, any>, variables?: any, isFile: boolean = false): Observable<MutationResult<unknown> | any> { return this.apollo.mutate({ mutation: mutation, variables: variables, errorPolicy: 'none', // 改为none,让错误流到订阅端 context: { useMultipart: isFile } }).pipe( map((v) => v.data), catchError(err => { // 重新抛出错误,触发全局错误处理器 throw err; }) ) }
注意:使用方案二时,组件订阅该Observable时不要手动捕获错误(或捕获后重新抛出),否则全局处理器无法捕获到错误。
内容的提问来源于stack exchange,提问作者ivy
相关产品推荐
相关产品推荐

