28. August 2024 By Jannis Kaiser
Bringing error messages safely to the frontend with Spring-Boot
Since version 2.3, the Spring Boot Framework hides more precise details about errors that occur when processing a request by default. This concerns, among other things, the messages from exceptions and the messages that are specified in the validation annotations. However, it is often necessary in a project to have details about the error available in the frontend.
For example, it is not a good user experience if a generic error message ‘This request could not be processed successfully’ is displayed for a 400-bad request response from the backend. In this case, the frontend should use the error to distinguish whether the error has occurred because the registration link has expired or because the user name has already been taken. To do this, however, it needs the relevant details about the error.
In this blog post, I will explain why error details can be security-relevant and how Spring Boot can still provide users with the necessary information.
Why are error details hidden by default
Until Spring Boot version 2.3, details about the error such as the message of the exception or the validation annotation were always included in the response. The Spring Boot project implemented this change with good reason. Previously, these details were included in the error response for all exceptions.
However, this poses a security risk for errors that occur deeper in the application. This is because a detailed error message about a failed SQL query or a failed call to a third-party system can be used by an attacker, for example, to extract implementation details about the application or even customer data.
The solution would now be to hide the details of technical or unexpected errors and only provide enough information to distinguish between the error cases in the case of certain technical errors.
Insecure activation of error messages
These details can still be reactivated today using the configurations server.error.include-message=never|on-param|always
and server.error.include-binding-errors=never|on-param|always
. However, both the value always and on-param harbour the above-mentioned danger and should therefore not be used.
always
always writes the details to an error response. on-param
writes the details to the error response if the request contains the parameter message=true.
However, as the request is also controlled by the attacker, this does not help security.
With include-message
activated, an error response has the following format:
{
"timestamp": "2024-08-07T19:07:36.173+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "Malformed SQL query",
"path": "/"
}
Without include-message
, the same format is retained, but without the message entry. The above configurations should therefore be left at the default value never
.
Define specialised exceptions
A tried and tested approach that is well supported by Spring is to define an exception class for each functional error case. This exception can then be provided with the Spring annotation @ResponseStatus
in order to be automatically converted into an error response with the corresponding Http status when it occurs.
@ResponseStatus(value = HttpStatus.BAD_REQUEST,
reason = "DUPLICATE_USERNAME")
public class DuplicateUsernameException extends RuntimeException {}
The reason parameter is the parameter that should later appear as a message in the response. Here it is set to the value DUPLICATE_USERNAE
, which can be interpreted by the front end.
How can the error response be supplemented with the necessary details in the event of technical errors?
Possible solution with a ControllerAdvice
With a ControllerAdvice
ControllerAdvice
, Spring offers the option of customising the response for selected errors. For the technical error that a user name has already been assigned, this could look as follows, for example:
@ControllerAdvice
public class DuplicateUsernameExceptionHandler
extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = { DuplicateUsernameException.class })
protected ResponseEntity<CustomErrorModel> handleDuplicateUsername(
DuplicateUsernameException ex, WebRequest request) {
var errorModel = CustomErrorModel.forDuplicateUser(ex.getUsername());
return handleExceptionInternal(
ex, errorModel, new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
}
}
However, this approach requires that a self-defined data model is defined for the error response. Errors that are handled in this way then have the self-defined format, while all other errors that may occur in the application continue to correspond to Spring's standard format.
The approach with a ControllerAdvice
is therefore only suitable if the API requires a separate format to be returned for all error responses. This is usually only necessary for very large projects or externally provided APIs.
For many projects, it would be better to follow Spring's standard format and still only return extended details for selected errors.
Standard format, but still in control?
With the ErrorAttributes
bean, Spring offers the option of deciding for each request in which an error has occurred which of the available details about the error should be included in the response. The response retains the format shown above, only the entries to be included are selected.
Like the previous approach, the approach I am now presenting requires a separate exception class to be defined for each technical error. The aim is to create a way to mark certain technical exceptions for which the error message should be included in the response.
To configure this, you can register the following ErrorAttributes
bean in the Spring context:
@Bean
public ErrorAttributes errorAttributes() {
// Die Standardimplementierung von ErrorAttributes verwenden ...
return new DefaultErrorAttributes() {
// ... aber eine kleine Änderung bei den ErrorAttributes vornehmen
@Override
public Map<String, Object> getErrorAttributes(
WebRequest webRequest, ErrorAttributeOptions options) {
final var error = getError(webRequest);
if (shouldIncludeErrorMessage(error)) {
options = options.including(Include.MESSAGE);
}
return super.getErrorAttributes(webRequest, options);
}
};
}
private static boolean shouldIncludeErrorMessage(Throwable error) {
// todo: decide when to include the exception message
return false;
}
As we want to retain most of the standard behaviour, we use an instance of DefaultErrorAttributes
and only overwrite the method that decides which attributes should be included. In this method, we use getError
to query the error that occurred during processing. Then we check in shouldIncludeErrorMessage
whether the error fulfils a condition specified by us. If this is the case, we add the value MESSAGE
to the ErrorAttributeOptions
and then return the customised ErrorAttributeOptions
to the standard implementation.
We are therefore only making a minimally invasive intervention here and leaving everything else as it is. But with this intervention, we have a good integration point in the error handling, with which we can decide which details should be included in the response depending on the error that has occurred.
Now we just have to decide in shouldIncludeErrorMessage
how the error should be handled. One possibility would be to list in this method the errors for which it should take effect:
private static boolean shouldIncludeErrorMessage(Throwable error) {
return error != null
&& error.getClass() == DuplicateUsernameException.class;
}
With this small configuration, we can stick to the standard format of the error response, hide the details by default, but still return them for selected errors. It would be better to be able to define this behaviour directly in the exception class.
Marking technical errors via annotation
In Java, there are several ways to mark classes so that this marking can be queried later in the programme. This can be done, for example, via a common superclass or the implementation of an interface. However, as the class hierarchy of exceptions can also be used for other purposes, I present an orthogonal solution with annotations here.
First, a new annotation is defined that identifies the exception classes for which the error should be contained in the response:
// Die Annotation gilt nur an Klassen, nicht an Feldern oder Methoden
@Target(ElementType.TYPE)
// Die Annotation wird zur Laufzeit der Anwendung benötigt
@Retention(RetentionPolicy.RUNTIME)
public @interface IncludeExceptionMessage {}
We can then adapt the previously defined shouldIncludeErrorMessage
method so that it reacts to all exceptions with this annotation:
private static boolean shouldIncludeErrorMessage(Throwable error) {
return error != null &&
error.getClass().isAnnotationPresent(IncludeExceptionMessage.class);
}
And finally, we mark our technical error with the new annotation:
@IncludeExceptionMessage
@ResponseStatus(value = HttpStatus.BAD_REQUEST,
reason = "DUPLICATE_USERNAME")
public class DuplicateUsernameException extends RuntimeException {}
Now we can define new errors in isolation in any part of the application and don't have to worry about adapting a central configuration.
Would you like to find out more about exciting topics from the world of adesso? Then take a look at our previously published blog posts.