Best Practice: Backendfehler an den Aufrufer/Frontend zurückliefern

Wir haben bei fast allen Backend-Endpunkten unterschiedliche Methoden umgesetzt um aufgetretene Fehler an den Aufrufer/Frontend zurückzuliefern. Dabei unterstützen die wenigsten davon die Mehrsprachigkeit der Fehlermeldung und das Format ist auch immer unterschiedlich.

In diesem Best Practice wird beschrieben wie ein an einem Backend Endpunkt aufgetretener Fehler an das Frontend/Aufrufer zurückgeliefert werden sollte und wie dann im Frontend auf diese Fehler-Response zugegriffen wird um diese darzustellen.

Beispiel Error Response

Was wir im Fehlerfall wollen ist eine immer gleich aufgebaute Error Response wie dieses Beispiel:

{
    "error": {
        "code": 33001,
        "name": "IM_UNSUPPORTED_OPERATION",
        "message": "Get users is not supported by this identity provider: oidc-keycloak",
        "messageKey": "CORE_ERROR_IDENTITY_PROVIDER_GET_USERS_UNSUPPORTED",
        "properties": {
        	"idp": "oidc-keycloak"
        }
    }
}

Die eigentliche Fehlermeldung message wird vom Backend Core abhängig von der Sprache des Benutzers generiert. Dazu wird über den messageKey der sprachabhängige Text aus der Sprachdateien gelesen und mit den properties gefüllt.

de.json
...
"CORE_ERROR_IDENTITY_PROVIDER_GET_USERS_UNSUPPORTED": "Abfrage der Benutzer wird vom Identity Provider nicht unterstützt: {idp}"
en.json
...
"CORE_ERROR_IDENTITY_PROVIDER_GET_USERS_UNSUPPORTED": "Get users is not supported by this identity provider: {idp}"

Backend

Es hat sich bewährt die Error Response über Exceptions zu generieren, anstatt diese on the Fly anzulegen und an den Aufrufer zurückzuliefern. Also an Stellen bei denen z.B. eine queryId fehlt, eine Exception mit der Info was fehlt werfen, anstatt hier direkt die Error Response zu erzeugen und zurückzuliefern (siehe Endpunkte).

Auch sollten tiefer im Code keine Exceptions gefangen und geloggt werden, das sollte erst am Endpunkt passieren. Dann hat man direkt den Aufrufer und kann über den StackTrace das Problem wesentlich einfacher analysieren.

Es gibt natürlich auch hier Ausnahmen, wenn tiefer im Code z.B. IOExceptions, IllegalArgumentExceptions etc. geworfen werden, dann kann es durchaus sinnvoll sein, diese zu fangen und statt dessen eine aussagekräftigere de.virtimo.bpc.api.SystemException zu werfen (siehe SystemException)

Endpunkte

An jedem Endpunkt müssen alle möglichen Exceptions gefangen, geloggt und die Error Response per ErrorResponse generiert und zurückgeliefert werden.

Endpunkt Beispiel
...
import de.virtimo.bpc.api.ErrorResponse;
...

@GET
@Path("/users")
@Produces({APPLICATION_JSON_UTF8})
@BpcRoleOrRightRequired(right = "IDENTITY_MANAGER_USERS_READ", role = "IDENTITY_MANAGER_ADMIN")
@BpcUserSessionRequired
public Response getUsers(
        @Context HttpHeaders hh
) {
    LOG.info("getUsers");
    try {
        return Response.ok(getIdentityManager().getUsers()).build();
    } catch (Exception ex) {
        LOG.log(Level.SEVERE, "Failed to fetch the list of users from the current IdP.", ex);
        return ErrorResponse.forException(ex).languageFrom(hh).build();
    }
}

ErrorResponse Klasse

Wie an dem Beispiel oben zu sehen wir die Klasse de.virtimo.bpc.api.ErrorResponse zur Generierung der JSON Error Response verwendet. Sie ist nach dem Builder-Pattern aufgebaut und bietet folgende Methoden.

forException(Exception)

Pflicht. Hier die Exception setzen für welche die Error Response erzeugt werden soll.

usingTracker(BpcServicesTracker<ErrorResponseService>)

Optional, aber extrem gewünscht. Der Builder verwendet im Hintergrund den ErrorResponseService. Wird der Tracker nicht gesetzt, dann wird jedesmal ein Tracker erstellt, der Service geholt und der Tracker wieder verworfen. Erzeugt unnötigen Overhead.

languageFrom(HttpHeaders)

Optional, aber sehr sinnvoll wenn die Meldung sprachabhängig sein soll. Aus den übergebenen HTTP Headers wird die zu verwendende Sprache aus dem Key X-BPC-Language gelesen. Wurde der HTTP Header nicht gefunden, dann wird en als Sprachkey verwendet.

language(String)

Optional. Nur sinnvoll, wenn man die Meldung einer SystemException in einer bestimmten Sprache ("de", "en") benötigt.

SystemException mit Error Code, HTTP Response Code und Mehrsprachigkeit

An dem Endpunkt wird auch für "normale" Exceptions eine entsprechende Error Response generiert. Soll diese jedoch einen Error Code, speziellen HTTP Response Code und Mehrsprachigkeit unterstützen, dann muss eine de.virtimo.bpc.api.SystemException bzw. davon abgeleitet geworfen werden.

SystemException Aufruf
throw new IdentityManagerException(CoreErrorCode.IM_UNSUPPORTED_OPERATION, "CORE_ERROR_IDENTITY_PROVIDER_GET_USERS_UNSUPPORTED", Map.of("idp", idpName));

Wenn man keine sprachabhängige Fehlermeldung erzeugen möchte, dann kann man den Key CORE_ERROR_IDENTITY_PROVIDER_GET_USERS_UNSUPPORTED auch durch einen Plain Text ersetzen.

IdentityManagerException.java
import de.virtimo.bpc.api.ErrorCode;
import de.virtimo.bpc.api.SystemException;

public class IdentityManagerException extends SystemException {
...
    public IdentityManagerException(ErrorCode errorCode, String message, Map<String, Object> props) {
        super(errorCode, message, props);
    }
...
}
CoreErrorCode.java
import de.virtimo.bpc.api.WebErrorCode;
import javax.ws.rs.core.Response;

public enum CoreErrorCode implements WebErrorCode {
...
    IM_UNSUPPORTED_OPERATION(33001, Response.Status.SERVICE_UNAVAILABLE),
...
}

Über den WebErrorCode wird eine eindeutige Zahl festgelegt, so dass man bei Supportanfragen evtl. schneller nachvollziehen kann worauf sich das Problem bezieht. Auch wird hier festgelegt, welcher HTTP Response Code an den Aufrufer zurückgeliefert werden soll. Wenn zum Beispiel eine Resource nicht gefunden wurde: Response.Status.NOT_FOUND entspricht dem HTTP Response Code 404.

Frontend

Im Frontend kann über eine BpcCommon Util Funktion auf die Fehlermeldung zugegriffen und dargestellt werden.

failure : function (record, operation) {
   const errorMsg = BpcCommon.Util.getErrorFromFailureResponse(operation.error.response).message;
   BpcCommon.Api.showNotification({
      text : `${BpcCommon.Api.getTranslation("CORE_ERROR")}: ${errorMsg}`,
      toast       : true,
      toastTarget : window,
      type        : "ERROR"
   });
},

Keywords: