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

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服务的错误策略,确保错误能流到组件订阅环节,被全局处理器捕获:

  1. 修改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));
})
  1. 修改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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 18:44:50