Best practice: Returning backend errors to the caller/frontend
We have implemented different methods for almost all backend endpoints to return errors that have occurred to the caller/frontend. Very few of them support the multilingualism of the error message and the format is always different.
This best practice describes how an error that has occurred at a backend endpoint should be returned to the frontend/caller and how this error response is then accessed in the frontend to display it.
Example error response
What we always want in the event of an error is an error response with the same structure as this example:
{
"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"
}
}
}
The actual error message wird vom Backend Core abhängig von der Sprache des Benutzers generiert. Dazu wird über den <INLINE_CODE_1/>` reads the language-dependent text from the language files and fills it with the message.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
It has proven useful to generate the error response via exceptions instead of creating them on the fly and returning them to the caller. So in places where, for example, a is missing, throw an exception with the info what is missing, instead of directly generating and returning the error response here (see endpoints).queryId
Also, no exceptions should be caught and logged deeper in the code, this should only happen at the endpoint. Then you have the caller directly and can analyze the problem much more easily via the StackTrace.
There are of course exceptions here too, if e.g. IOExceptions, IllegalArgumentExceptions etc. are thrown deeper in the code. it can make sense to catch these and throw a more meaningful instead (see SystemException)de.virtimo.bpc.api.SystemException
Endpoints
At each endpoint, all possible exceptions must be caught, logged and the error response generated and returned via .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),
...
}
The WebErrorCode is used to define a unique number so that it may be easier to understand what the problem relates to in the event of support requests. The HTTP response code to be returned to the caller is also defined here. For example, if a resource was not found: Response.Status.NOT_FOUND corresponds to the HTTP response code 404.
Frontend
The error message can be accessed and displayed in the frontend via a BpcCommon Util function.
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"
});
},