Auf der Suche nach dem “Unknown Error 0”

Thorsten Rintelen

Seit über 20 Jahren Software-Developer, Spezialisiert auf Angular, Teamleiter Entwicklung bei der traperto GmbH

Auch wenn ich es nur ungerne sage, aber in unserer Anwendung kann es zu Fehlern kommen. In der Regel ist davon die Kommunikation zwischen Frontend und Backend, also eine REST-Schnittstelle betroffen.
In so einem, natürlich(!) extrem seltenen Fall, hat der Nutzer die Möglichkeit eine automatisierte E-Mail an unseren Support zu senden. Die E-Mail enthält einige Informationen, die mir das debuggen erleichtern sollen, so z.B. der HTTP Code sowie eine mögliche Fehlermeldung vom Server.

In diesen E-Mails habe ich erstmals mit dem Problem des „Unknown Errors“ und dem Fehler-Code „0“ zu tun.

Unknown Error 0 – was kann das sein?

Wie der Name schon sagt, handelt sich dabei um einen unbekannten Fehler.

Das Angular-Frontend versucht mit dem .NET-Backend zu kommunizieren. Aus einem nicht näher bekannten Grund kann das Backend aber nicht antworten, es kann also keine Kommunikation stattfinden.
Das Frontend kann uns dann nur noch sagen, dass etwas schief gelaufen ist, aber nicht genau was. Somit haben wir einen unbekannten Fehler, mit dem Code 0.

Wo liegt jetzt das Problem? Im Frontend? Im Backend? Oder doch am Server?
Das Internet geht davon aus, dass das Problem in deutlich mehr als 90% der Fälle am Server oder Backend liegt.

CORS

Bei der Recherche im Internet habe ich die meisten Lösungsansätze im Bereich CORS gefunden. Die Kommunikation zwischen Frontend und Backend kommt nicht zustande, weil beide auf Grund von CORS Sicherheitsrichtlinien nicht miteinander sprechen dürfen.
In diesem Fall kann das Problem entweder im Backend oder am Server gelöst werden.

In meinem Fall liegt kein CORS Problem vor, so dass ich auf die mögliche Lösung an dieser Stelle nicht weiter eingehen möchte.
Tip: Wer dieses Problem hat, kann im Backend die nötigen Header setzen und die Kommunikation erlauben.

Schwache oder wechselnde Internetverbindung

Denn mein Problem lag (vermutlich) darin, dass die Nutzer entweder nur ganz schwaches Internet hatten oder häufig zwischen WLAN und Internet gewechselt haben.

Um dieses Problem zu umgehen, habe ich in unserer Angular-Anwendung einen interceptor eingebaut, der im Fehlerfall X-Mal alle Y-Sekunden den Request neu absetzt.
Tatsächlich hat dieser interceptor dazu geführt, dass wir kaum bis keine Debug-Mails mehr bzgl. des „unknown errors 0“ erhalten. Ich sehe das daher als kleinen Beweis, dass wir das Problem damit gelöst haben.

RetryWhen mit retry Strategy im Interceptor

Ein Interceptor in der Angular-Welt kann sich an bzw. zwischen die Kommunikation hängen. Häufige wird er dafür verwendet, um dem Backend weitere Header zu senden oder die Antwort vom Backend zu lesen bzw. aufzubereiten.

Ein Interceptor wird im Modul wie folgt provided. Dabei bedeutet „multi“, dass es mehrere Interceptoren geben kann.

providers: [{ provide: HTTP_INTERCEPTORS, useClass: MyInterceptor, multi: true }]

In meinem Fall benötige ich den rxjs Operator retryWhen mit dem ich eine Funktion ausführe. Die Anfrage wird bearbeitet und ich habe die Möglichkeit, mit dem Ergebnis zu arbeiten (innerhalb der pipe).
Solange „timeoutRetryStrategy“ keinen Fehler wirft, wird die Anfrage erneut gefeuert.

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(request).pipe(
            retryWhen(timeoutRetryStrategy()),
            catchError(response => {
             ....
            })
        )
}

In dieser function wiederhole ich die Anfrage solange, bis diese geklappt hat, also ein brauchbarer HTTP-Code zurück kommt, oder die Anzahl der Versuche erreicht ist (hier 3 Versuche mit jeweils 2 Sekunden Pause). 
Sollte dann noch immer ein Fehler vorliegen, wird dieser geworfen und an den Interceptor weitergegeben. Die weitere Behandlung findet dann dort statt.

import { Observable, throwError, timer } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
 
export const timeoutRetryStrategy = () => (attempts: Observable<any>) => {
    const maxRetryAttempts = 3;
    const scalingDuration = 2000;
 
    return attempts.pipe(
        mergeMap((error, i) => {
            const retryAttempt = i + 1;
            // if maximum number of retries have been met
            // or response is a status code we don't wish to retry, throw error
            if (retryAttempt > maxRetryAttempts || (error.status !== 0 && error.status !== 504)) {
                return throwError(error);
            }
 
            return timer(retryAttempt * scalingDuration);
        }),
    );
};

Fazit

Ganz so unbekannt ist ein „unknown error 0“ am Ende doch nicht, wobei jeder für sich die unterschiedlichen Möglichkeiten prüfen muss. Meiner Meinung nach kann es aber nicht schaden, die retry Strategy zu verwenden.
In meinem Fall scheint dies tatsächlich den gewünschten Erfolg gebracht zu haben und ich kann mich jetzt um die bekannten Fehler kümmern.