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
wird vom Backend Core abhängig von der Sprache des Benutzers generiert. Dazu wird über den message
der sprachabhängige Text aus der Sprachdateien gelesen und mit den messageKey
gefüllt.properties
...
"CORE_ERROR_IDENTITY_PROVIDER_GET_USERS_UNSUPPORTED": "Abfrage der Benutzer wird vom Identity Provider nicht unterstützt: {idp}"
...
"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
fehlt, eine Exception mit der Info was fehlt werfen, anstatt hier direkt die Error Response zu erzeugen und zurückzuliefern (siehe Endpunkte).queryId
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
zu werfen (siehe SystemException)de.virtimo.bpc.api.SystemException
Endpunkte
An jedem Endpunkt müssen alle möglichen Exceptions gefangen, geloggt und die Error Response per
generiert und zurückgeliefert werden.ErrorResponse
...
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.
|
Pflicht. Hier die Exception setzen für welche die Error Response erzeugt werden soll. |
|
Optional, aber extrem gewünscht. Der Builder verwendet im Hintergrund den |
|
Optional, aber sehr sinnvoll wenn die Meldung sprachabhängig sein soll. Aus den übergebenen HTTP Headers wird die zu verwendende Sprache aus dem Key |
|
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
bzw. davon abgeleitet geworfen werden.de.virtimo.bpc.api.SystemException
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 |
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);
}
...
}
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"
});
},