Reputation: 9815
I am using NGRX and I want to have simple GET requests to an API to be retried five times. Reason for this is that I am using Azure Cosmos-DB and I am being throttled sometimes. (free-tier).
I've created an http-interceptor for this which is pretty straight forward and looks like that
@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()
);
}
}
This works just fine and each failing http-request is retried five times with a delay of 1000ms.
Problem now is, that the failure action in the effect is not being triggered when the requests actually failed for five times.
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
)
)
)
);
What is weird is, that when the http-interceptor uses the operator retry
rather than retryWhen
it works just fine. Sadly, with that operator you cannot define a delay
, which is required in my case.
Another interesting fact is, that when using the very same retry logic on the service used in the effect, it works just fine.
// 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()
);
While this is not much code and could be copied onto each http-service that I am using, I would love to use an interceptor for it instead.
I couldn't seem to find the cause for that and I hope, that anyone out there knows why this is not working.
Upvotes: 4
Views: 1574
Reputation: 11979
Another interesting fact is, that when using the very same retry logic on the service used in the effect, it works just fine.
retry(n)
will simply pass along the error notification when n
attempts are reached. When attempts < n
, it will unsubscribe from the source, then it will re-subscribe to it.
retryWhen(fn)
maintains an inner subscription, which is the result of the observable provided with the function fn
. The fn
's single argument is an errors subject that will push values every time an error occurs.
So, if you have
retryWhen(subjectErrors => subjectErrors.pipe(a(), b()))
is essentially the same as:
const subjectErrors = new Subject();
subjectErrors.pipe(a(), b()).subscriber(innerSubscriber);
An innerSubscriber
is used inside retryWhen
too, and its main role is to inform when a value(next notification
) arrives.
When this happens, the source will be re-subscribed. In this case, the source is an observable that makes an http request, and when an error occurs, it will send an error
notification; if the request is successful, it will emit a next
notification, followed by a complete
notification.
take(5)
from retryWhen(errors => errors.pipe(delay(1000), take(5)))
means that when 5 attempts are reached, the innerSubscriber
(that one from above) will emit a complete
notification.
Then you have last
, which has no default value. This means that when it receives a complete
notification, without receiving any next
notifications before, it will emit an error(default behavior).
Then the error is caught by catchError()
in your effect. So this should explain why this approach works.
What is weird is, that when the http-interceptor uses the operator retry rather than retryWhen it works just fine.
This is something I couldn't explain why because, using the information from the previous section, it should work fine, the created streams should be identical.
If you have
load () {
return this.http.get(...);
}
and assuming that interceptor is the only one you're using, in
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()
should be the same as
of(request).pipe(
concatMap(
req => new Observable(s => { /* making the request.. */ }).pipe(
retryWhen(errors => errors.pipe(delay(1000), take(5))),
last(),
)
)
)
which should be the same as with what happens in the previous section.
So, if I'm not missing anything, the catchError
should be reached.
If not, it could mean that you either have some other catchError
or last
has received a value and thus it does not throw an error.
Does it log something in this case?
// 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()
);
As can be seen in the source code, the observable in which the request happens will send through its subscriber an event which indicates that the request has been dispatched.
With this in mind, you could use the filter
operator:
return httpHandler.handle(nextRequest).pipe(
filter(ev => ev.type !== 0),
retryWhen(errors => errors.pipe(delay(1000), take(5))),
last()
);
Upvotes: 4