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 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 properties.

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

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 queryId is missing, throw an exception with the info what is missing, instead of directly generating and returning the error response here (see endpoints).

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 de.virtimo.bpc.api.SystemException instead (see SystemException)

Endpoints

At each endpoint, all possible exceptions must be caught, logged and the error response generated and returned via ErrorResponse.

Endpoint example
...
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),
...
}

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"
   });
},

Keywords: