Returning Backend Errors to the Caller/Frontend
We have implemented different methods for almost all backend endpoints to return errors to the caller/frontend. Very few of these support multilingual error messages, and the format also varies from endpoint to endpoint.
This best practice describes how an error that occurs at a backend endpoint should be returned to the frontend/caller and how the frontend should then access this error response to display it.
Example Error Response
What we want in the event of an error is an error response that is always structured the same way, like 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/>` is the language-specific text read from the language files and filled in 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 effective to generate the error response via exceptions rather than creating it on the fly and returning it to the caller. So, in places where, for example, a ` is missing, throw an exception with the information about what is missing, rather than generating and returning the error response directly here (see endpoints).queryId
Furthermore, exceptions should not be caught and logged deeper in the code; this should only happen at the endpoint. That way, you have direct access to the caller and can analyze the problem much more easily using the stack trace.
Of course, there are exceptions to this rule: if, for example, IOExceptions, IllegalArgumentExceptions, etc., are thrown deeper in the code, it may make sense to catch them and instead throw a more descriptive ` (see de.virtimo.bpc.api.SystemExceptionSystemException)
Endpoints
At every endpoint, all possible exceptions must be caught, logged, and the error response must be 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 Class
As shown in the example above, the ` de.virtimo.bpc.api.ErrorResponse ` class is used to generate the JSON error response. It is structured according to the Builder pattern and provides the following methods.
|
Required. Set the exception here for which the error response should be generated. |
|
Optional, but highly recommended. The builder uses the |
|
Optional, but very useful if the message is to be language-dependent. The language to be used is read from the key ` |
|
Optional. Only useful if you need the message of a SystemException in a specific language (“de,” “en”). |
SystemException with Error Code, HTTP Response Code, and Multilingual Support
This endpoint also generates a corresponding error response for “normal” exceptions. However, if this response is to support an error code, a specific HTTP response code, and multilingual support, then an ` or a subclass thereof must be thrown.de.virtimo.bpc.api.SystemException
throw new IdentityManagerException(CoreErrorCode.IM_UNSUPPORTED_OPERATION, "CORE_ERROR_IDENTITY_PROVIDER_GET_USERS_UNSUPPORTED", Map.of("idp", idpName));
|
If you do not want to generate a language-dependent error message, you can replace the 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, which may help support teams identify the nature of the problem more quickly when handling support requests. This also specifies which HTTP response code should be returned to the caller. For example, if a resource was not found: Response.Status.NOT_FOUND corresponds to HTTP response code 404.
Frontend
In the frontend, the error message can be accessed and displayed using a BpcCommon utility 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"
});
},