Error Handling
The JAX-RS stack expanded by the rest-expanders has an extensive error handling mechanism which contains several layers of validation, as well as transformation of errors to a fixed standardized format. This errors are returned through the API based on the Problem Details format.
Error format
Errors returned from the API are formatted as follows:
{
"type": "string",
"title": "string",
"status": 0,
"identifier": "string",
"code": "string",
"extraInfo": {}
}
type
: Contains a path intended to provide information or documentation about the error type.title
: A message describing the error.status
: The HTTP status code associated with the error.identifier
: A unique identifier for the error that occurred, which can be used to identify the error in the log files.code
: The code which identifies the specific error type.extraInfo
: An object which defines additional information about the error. This object can have a different structure for each error type, though by default only two variants are used, one for the majority of errors and one specifically for validation errors.
Server error types
There are several default server error types, each of which are categorized by HTTP status.
400 Bad request
Type | Name | Description |
---|---|---|
BR001 | BR_VALIDATION | A validation error was returned by one of the validation steps in the JAX-RS stack. This is error is the result of a call to the API containing invalid data. See validation for more information. |
BR002 | BR_JSON | A serialization error occurs in the JSON.org library. |
BR003 | BR_JSON_MAPPING | A fatal error occurred in Jackson when trying to map a JSON object. |
BR004 | BR_JSON_PROCESSING | A fatal error occurred in Jackson when trying to serialize a JSON object. |
BR005 | BR_INVALID_FORMAT | A value could not be serialized because it has an invalid format. |
BR006 | BR_JSON_PARSE | A JSON object could not be parsed due to an invalid structure. |
BR007 | BR_QUERY_PARAM | An error occurred while trying to parse a query parameter. |
BR008 | BR_TIMESTAMP_FORMAT | An error occurred due to a timestamp being provided in an incorrect format. |
BR009 | BR_HEADER_PARAM | An error occurred while trying to parse a header parameter. |
BR010 | BR_PATH_PARAM | An error occurred while trying to parse a path parameter. |
BR011 | BR_COOKIE_PARAM | An error occurred while trying to parse a cookie parameter. |
BR012 | BR_FORM_PARAM | An error occurred while trying to parse a form parameter. |
BR013 | BR_MATRIX_PARAM | An error occurred while trying to parse a matrix parameter. |
BR014 | BR_MALFORMED_HEADER | A header was supplied with a value that did not match the expected format. |
BR015 | BR_MALFORMED_SORT_PARAMETERS | Sorting parameters were passed to the API which did not match the expected format. |
BR016 | BR_UNKNOWN_SORT_PARAMETER | A sorting parameter which does not exist was passed to the API. |
BR017 | BR_CUSTOM_VALUE_TYPE_FORMAT | A value for a custom ValueFieldType could not be correctly mapped. |
401 Not authorized
Type | Name | Description |
---|---|---|
UA001 | UA_NOT_AUTHORIZED | Returned when the user for the call is not authorized to access the requested resource. |
403 Forbidden
Type | Name | Description |
---|---|---|
FB001 | FB_FORBIDDEN | Returned when an authenticated user tries to interact with a resource for which it does not have authorization. |
FB002 | FB_CORS | The client made a request that violates CORS policies enforced by the server. |
404 Not found
Type | Name | Description |
---|---|---|
NF001 | NF_RESOURCE | A resource was requested from an endpoint by its unique resource identifier and no resource matching this identifier was found (or authorized). |
NF002 | NF_ENDPOINT | An endpoint which does not exist was called. |
405 Method not allowed
Type | Name | Description |
---|---|---|
NA001 | NA_METHOD | An HTTP method that is not available was used to call an endpoint. |
406 Not Acceptable
Type | Name | Description |
---|---|---|
AC001 | AC_NOT_ACCEPTABLE | Returned if the server cannot provide a response that was deemed acceptable based on the content negotiation process. Example: The client requests a content type text/csv with the Accept header, on an endpoint for which the server only supports returning application/json . |
415 Unsupported media type
Type | Name | Description |
---|---|---|
UM001 | UM_NOT_SUPPORTED | A payload was sent to the server with a content type that is not supported by that endpoint. Example: The client sends a POST call, with a payload of content type text/csv , on an endpoint for which the server only accepts application/json . |
500 Internal server error
Type | Name | Description |
---|---|---|
SE001 | SE_ILLEGAL_STATE | An unhandled IllegalStateException was thrown during the call. |
SE002 | SE_SEARCH_FAILED | A SearchResultException was caught, which is thrown when a Diagnostic is returned to an endpoint of which the key ends with .findFailed . |
SE003 | SE_CREATE_FAILED | A CreateFailedException was caught, which is thrown when a Diagnostic is returned to an endpoint of which the key ends with .createFailed . |
SE004 | SE_MODIFICATION_FAILED | A ModificationFailedException was caught, which is thrown when a Diagnostic is returned to an endpoint of which the key ends with .modifyFailed . |
SE005 | SE_DELETE_FAILED | A DeleteFailedException was caught, which is thrown when a Diagnostic is returned to an endpoint of which the key ends with .deleteFailed . |
SE006 | SE_ILLEGAL_DB_STATE | [DEPRECATED] An IllegalDBStateException was caught, which is thrown when a Diagnostic is returned to an endpoint of which the key is cruds.findIllegalDBState . |
SE007 | SE_NULL_POINTER | An unhandled NullPointerException was thrown during the call. |
SE008 | SE_METHOD_NOT_FOUND | Thrown when method reflection fails. This can occur when JAX-RS has to dynamically call a Java method defined as part of the API implementation. |
SE009 | SE_TRANSACTION_ROLLED_BACK | [DEPRECATED] Thrown when an EJB transaction is rolled back. |
SE010 | SE_ILLEGAL_ARGUMENT | An unhandled IllegalArgumentException was thrown during the call. |
SE011 | SE_GENERIC | An internal server error (InternalServerErrorException ) was caught for which the cause could not specifically be identified. |
SE012 | SE_HEADER_VALUE | A HeaderValueException was caught, which indicates that the value of a header could not be read correctly. |
SE013 | SE_UNKNOWN_RESPONSE_ERROR | Returned for any error during the building of an API response for which the Exception could not be identified. |
SE014 | SE_UNKNOWN | Returned for any error during the processing of an API request for which the Exception could not be identified. |
Custom error handlers
Custom error handlers can be added to extend the functionality of the REST API.
These handlers can be used to catch both 3rd party exceptions, defined in the Java API or custom exceptions defined in the application.
The exception classes should be available in the control layer
for the handler to work correctly, otherwise a ClassNotFoundException
could occur.
When adding custom error types, make sure to choose the error code carefully.
Do not simply append the next code in the sequence of already existing codes, as this will cause conflicts if new codes are added by the rest expanders.
The application will fail at runtime if a collision occurs.
Example: Don't choose AC002
, as the REST expanders might add this code as well at some point.
Instead, choose something with a unique prefix, such as CE001
(custom error 1) or CSE001
(custom internal server error 1).
A custom error handlers requires four elements:
- A new error should be registered in the
ServerError
class by calling theregister(status, code, message)
method, which takes the HTTP status of the error, the unique error code and an unlocalized identifier for the error message. This can be added in thecustom-errors
anchors. - A translation for the error, mapping the message key to a human-readable sentence should be added to the
ServerErrorMessages.properties
file in thecustom-translations
anchors. Injecting parameters into these messages is not supported, unlike with validation errors. - If the error handler does not handle an existing exception, a custom exception class should be added.
Example of a custom exception class
package my.favorite.jaxrs.exception;
import net.democritus.contract.NotNull;
public final class ILoveException extends RuntimeException {
public ILoveException(@NotNull String message) {
super(message);
}
} - Finally, an exception handler should be added. This can be a subclass of
ExceptionMapper<E extends Throwable>
for exceptions that occur when processing an incoming request, or a subclass ofResponseErrorMapper
to handle exceptions that occur while processing the response. Usually it will be the first of these two. The class should have ajavax.ws.rs.ext.Provider
annotation in order for Jersey to pick it up and add it to the REST pipeline.Example of a custom exception handlerpackage myComponent.jaxrs.provider.error;
import myComponent.jaxrs.response.RestErrorResponse;
import myComponent.jaxrs.response.error.ServerError;
import myComponent.jaxrs.response.error.ServerErrorInfoModel;
import my.favorite.jaxrs.exception.ILoveException;
import net.palver.logging.Logger;
import net.palver.logging.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.container.ResourceContext;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import java.util.*;
@Provider
public class ILoveExceptionHandler implements ExceptionMapper<ILoveException> {
private static final Logger logger = LoggerFactory.getLogger(ILoveExceptionHandler.class);
@Context
private ResourceContext resourceContext;
/**
* Sets the {@link ResourceContext} for the call. This is only injected automatically if the handler is called
* directly from the JAX-RS stack.
*
* @param resourceContext The resource context object.
* @return The handler instance to chain invocations.
*/
public ILoveExceptionHandler setResourceContext(ResourceContext resourceContext) {
this.resourceContext = resourceContext;
return this;
}
@Override
public Response toResponse(ILoveException exception) {
Response response = RestErrorResponse.builder()
.setError(ServerError.CE_I_LOVE_ERRORS)
.setExtraInfo(mapException(exception))
.build();
logException((RestErrorResponse) response.getEntity(), exception);
return response;
}
private ServerErrorInfoModel mapException(ILoveException exception) {
ServerErrorInfoModel errorModel = new ServerErrorInfoModel();
errorModel.setMessage(exception.getLocalizedMessage());
return errorModel;
}
private void logException(RestErrorResponse entity, ILoveException exception) {
HttpServletRequest servlet = resourceContext.getResource(HttpServletRequest.class);
StringBuilder logMessage = new StringBuilder()
.append("[")
.append(entity.getCode())
.append(" - ")
.append(entity.getIdentifier())
.append("] ")
.append(servlet.getMethod())
.append(" ")
.append(servlet.getPathInfo())
.append(servlet.getQueryString() == null ? "" : "?" + servlet.getQueryString())
.append(": ")
.append(entity.getTitle())
;
if (logger.isInfoEnabled()) {
logger.info(
logMessage.toString()
);
}
}
}