NGRX:http 重试拦截器导致失败操作未被触发

NGRX: http retry interceptor causing failure action not to be fired

我正在使用 NGRX,并且我希望对 API 进行简单的 GET 请求 重试 五次。原因是我使用的是 Azure Cosmos-DB,有时我会受到限制。 (免费套餐)。

我为此创建了一个 http 拦截器,它非常简单,看起来像那样

@Injectable()
export class HttpRetryInterceptor implements HttpInterceptor {
  public intercept(
    request: HttpRequest<any>,
    httpHandler: HttpHandler
  ): Observable<HttpEvent<any>> {
    const nextRequest = request.clone();

    // here the NGRX failure action is not being triggered after five failing requests
    return httpHandler.handle(nextRequest).pipe(
      retryWhen(errors => errors.pipe(delay(1000), take(5))),
      last()
    );
  }
}

这工作得很好,每个失败的 http 请求都会重试五次,延迟 1000 毫秒。

现在的问题是,当请求实际失败五次时,效果中的失败操作并未被触发。

load$ = createEffect(() =>
  this.actions$.pipe(
    ofType(globalStatsActions.load),
    mergeMap(() =>
      this.globalStatsService.load().pipe(
        map(stats =>
          globalStatsActions.loaded({
            latestStats: stats
          })
        ),
        catchError(() => of(globalStatsActions.loadFailed())) // not being called using the http-interceptor
      )
    )
  )
);

奇怪的是,当 http 拦截器使用运算符 retry 而不是 retryWhen 时,它工作得很好。可悲的是,使用该运算符您无法定义 delay,这在我的情况下是必需的。

另一个有趣的事实是,当在效果中使用的服务上使用完全相同的重试逻辑时,它工作得很好。

// here the failure action is being triggered after five failing requests
public load(): Observable<GlobalStats> {
  return this.http.get<GlobalStats>(`${this.baseUrl}stats`)
  .pipe(
    retryWhen(errors => errors.pipe(delay(1000), take(5))),
    last()
  );

虽然代码不多,可以复制到我正在使用的每个 http 服务上,但我更愿意为它使用拦截器。

我似乎找不到原因,我希望那里的任何人都知道为什么这不起作用。

Another interesting fact is, that when using the very same retry logic on the service used in the effect, it works just fine.

当达到 n 次尝试时,

retry(n) 将简单地传递错误通知。当尝试 < n 时,它将取消订阅源,然后它将 重新订阅

retryWhen(fn)维护一个内部订阅,这是函数fn提供的可观察结果。 fn 的单个参数是一个 错误主题 ,它会在每次发生错误时推送值。

所以,如果你有

retryWhen(subjectErrors => subjectErrors.pipe(a(), b()))

本质上等同于:

const subjectErrors = new Subject();
subjectErrors.pipe(a(), b()).subscriber(innerSubscriber);

retryWhen内部也使用了一个innerSubscriber,主要作用是在有值(next notification)到达时通知

发生这种情况时,源将重新订阅。在这种情况下,source 是一个发出 http 请求的 observable,当发生错误时,它会发送一个 error 通知;如果请求成功,它将发出 next 通知,然后是 complete 通知。

take(5) 来自 retryWhen(errors => errors.pipe(delay(1000), take(5))) 意味着当达到 5 次尝试时,innerSubscriber(上面那个)将发出 complete 通知。

那么你有 last,它 没有默认值 。这意味着当它收到 complete 通知时,之前没有收到任何 next 通知,它将发出错误(默认行为)。

然后错误被catchError()捕获在你的效果中。所以这应该可以解释为什么这种方法有效。

What is weird is, that when the http-interceptor uses the operator retry rather than retryWhen it works just fine.

这是我无法解释的原因,因为使用上一节中的信息,它应该可以正常工作,创建的流应该是相同的。

如果你有

load () {
 return this.http.get(...);
}

并假设拦截器是您唯一使用的拦截器,在

mergeMap(() =>
  this.globalStatsService.load().pipe(
    map(stats =>
      globalStatsActions.loaded({
        latestStats: stats
      })
    ),
    catchError(() => of(globalStatsActions.loadFailed())) // not being called using the http-interceptor
  )
)

this.globalStatsService.load() 应与

相同
of(request).pipe(
  concatMap(
    req => new Observable(s => { /* making the request.. */ }).pipe(
      retryWhen(errors => errors.pipe(delay(1000), take(5))),
      last(),
    )
  )
)

这应该与上一节中发生的事情相同。

所以,如果我没有遗漏任何东西,应该达到 catchError

如果不是,则可能意味着您有其他 catchErrorlast 已收到一个值,因此不会引发错误。

在这种情况下它会记录一些东西吗?

    // here the NGRX failure action is not being triggered after five failing requests
    return httpHandler.handle(nextRequest).pipe(
      retryWhen(errors => errors.pipe(delay(1000), take(5))),
      tap(console.log), 
      last()
    );

编辑

正如在 source code 中所见,发生请求的可观察对象将通过其订阅者发送一个事件,指示请求已被分派。

考虑到这一点,您可以使用 filter 运算符:

return httpHandler.handle(nextRequest).pipe(
  filter(ev => ev.type !== 0),
  retryWhen(errors => errors.pipe(delay(1000), take(5))),
  last()
);