4

Following Spring Boot documentation I defined my own ErrorAttributes bean (see below), I was able to make the json response to show the information I wanted, including my own error code and message by using a custom exception to wrap that information and generate the error response from it. The only issue with this is that the http status of the response is not matching the one I define in the status attribute, it is not been overridden.

@Bean
public ErrorAttributes errorAttributes() {
    return new DefaultErrorAttributes() {
        @Override
        public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {

            Map<String, Object> errorAttributes = super.getErrorAttributes(requestAttributes, includeStackTrace);

            Throwable error = getError(requestAttributes);

            if (error instanceof MyException) {
                MyException myException = (MyException) error;

                errorAttributes.put("errorCode", myException.getErrorCode());
                errorAttributes.put("message", myException.getMessage());
                errorAttributes.put("status", myException.getStatus());

                HttpStatus correspondentStatus = HttpStatus.valueOf(myException.getStatus());
                errorAttributes.put("error", correspondentStatus.getReasonPhrase());
            }

            return errorAttributes;
        }
    };
}

The response's http status is not matching the status in the json, for example:

HTTP/1.1 500 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 01 Mar 2017 18:48:22 GMT
{
   "timestamp": "2017-03-01T18:48:21.894+0000",
   "status": 403,
   "error": "Forbidden",
   "exception": "com.myapp.MyException",
   "message": "You are not authorized. This user doesn't exist in the db",
   "path": "/account",
   "errorCode": "00013"
}
1

6 Answers 6

9

I found a way of setting the http status from within the logic that creates my custom ErrorAttributes bean, this way I am able to re-use the out of the box Spring Boot error response creation and update it with my custom information without the need of exception handlers and controller advices.

By adding the next line you can set the http status which overrides the current one in the requestAttributes.

requestAttributes.setAttribute("javax.servlet.error.status_code", httpStatus, 0);

Where httpStatus is the status you want to set.

Here is the full bean definition with the added line:

@Bean
public ErrorAttributes errorAttributes() {
    return new DefaultErrorAttributes() {
        @Override
        public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {

            Map<String, Object> errorAttributes = super.getErrorAttributes(requestAttributes, includeStackTrace);

            Throwable error = getError(requestAttributes);

            if (error instanceof MyException) {
                MyException myException = (MyException) error;
                int httpStatus = myException.getStatus();

                errorAttributes.put("errorCode", myException.getErrorCode());
                errorAttributes.put("message", myException.getMessage());
                errorAttributes.put("status", httpStatus);

                HttpStatus correspondentStatus = HttpStatus.valueOf(httpStatus);
                errorAttributes.put("error", correspondentStatus.getReasonPhrase());
                requestAttributes.setAttribute("javax.servlet.error.status_code", httpStatus, 0);
            }

            return errorAttributes;
        }
    };
}

How did I find it? By looking at the DefaultErrorAttributes class, I found there is a method addStatus which is private, but it shows the name of the attribute that is used by the code to generate the response's http-status, that was the clue I was looking for:

private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {
    Integer status = (Integer)this.getAttribute(requestAttributes, "javax.servlet.error.status_code");
...

Looking more into the code I found that the getAttribute method that is being called there is actually calling the method from RequestAttributes interface:

private <T> T getAttribute(RequestAttributes requestAttributes, String name) {
    return requestAttributes.getAttribute(name, 0);
}

Checking inside that interface I found there is also a setAttribute method. It worked.

HTTP/1.1 403 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 01 Mar 2017 20:59:33 GMT
{
   "timestamp": "2017-03-01T20:59:32.774+0000",
   "status": 403,
   "error": "Forbidden",
   "exception": "com.myapp.MyException",
   "message": "You are not authorized. This user doesn't exist in the db",
   "path": "/account",
   "errorCode": "00013"
}
Sign up to request clarification or add additional context in comments.

2 Comments

In order to make this work for Spring Boot 2.3 set an Integer and not an HttpStatus: webRequest.setAttribute("javax.servlet.error.status_code", HttpStatus.I_AM_A_TEAPOT.value(), RequestAttributes.SCOPE_REQUEST);
You can replace the literal "javax.servlet.error.status_code" with the constant ERROR_STATUS_CODE present in RequestDispatcher to ensure it's exactly the same attribute as the one referenced by Tomcat. tomcat.apache.org/tomcat-7.0-doc/servletapi/javax/servlet/…
6

All you are doing is building the body of your error response, as you can see from your sample. Spring is the one handling the status code.

If you want to have full control on all parts of the response then you should use the ControllerAdvice approach as shown in their documentation:

@ControllerAdvice(basePackageClasses = FooController.class)
public class FooControllerAdvice extends ResponseEntityExceptionHandler {

   @ExceptionHandler(MyException.class)
   public ResponseEntity<Message> handleRequestErrorMyException(HttpServletRequest request, MyException myException) {
        HttpStatus status = HttpStatus.valueOf(myException.getStatus();
        return new ResponseEntity<>(new CustomErrorType(status.value(), ex.getMessage()), status);
    }

}

With this bean in place all MyException thrown by any controller under the FooController package will be captured and processed by handleRequestErrorMyException, the response to the original request will be the one returned by this method. Just make sure in your Configuration class that this package gets scanned.

5 Comments

I've been trying to avoid using exception handlers, the ErrorAttributes already has some pre-populated information that I want to return in addition to my custom data, like path, exception, stacktrace,... The only thing that is missing in my approach is the http status, there should be a way of setting it which is what I am trying to find
I don't understand why the ControllerAdvice is not sufficient to meet your requirements. That method gives you access to the exception and to the request information, I don't see what information you will have in the ErrorAttributes bean that you will not have in the ControllerAdvice. The only difference I see is that you'll have to add some attributes manually like timestamp, path but other than that I don't see the issue.
I agree, the controller advice is sufficient and it is actually more sufficient than what I need. My idea on using ErrorAtrributes was to use the out of the box mechanism from Spring Boot without adding extra configuration, and therefore follow the standard error response from Spring boot plus my custom field. That way I don't add a burden of maintenance for those exception handlers, in this case the change is incremental instead of replicating what the current mechanism already provides.
The controllerAdvice is the natural way Spring provides if you need to manipulate the response to a bigger extent than just changing the error body. For example you may have a resource that is not authorized by some user and your application throws an exception then you can capture the exception and send back a 403 as in your example otherwise spring just sees it as an internal error (500). IMHO ErrorAttributes is more appropriate if what you need is to decorate a little bit the error body, otherwise you need a hack to accomplish your purpose, hacky solutions tend to be problematic.
Yes I see your point and I agree that controller advices are the way to go to do more structural customizations, they are flexible and powerful. However in my case and as you mention my customization is just a decoration of the current message, without having to replicate the includeStackTrace mechanism, etc. The only piece that was missing is the matching of the http status. I found a way of doing it as shown in my answer, it is not a hack as it is using the parameter of the method getErrorAttributes which is provided by the interface and therefore supposed to be used in the method logic.
0

Alternatively you can use

MyException extends ResponseStatusException {
 public MyException (String msg) {
 super(HttpStatus.FORBIDDEN, msg);
} 

3 Comments

Please provide a proper description.
Can you please elaborate exactly how will it solve the issue. Thanks.
It's good for exception inheritance, but doesn't work for final status in response
0

try below, not that valid values types are integer for status key, while you were adding enum which is HttpStatus.BAD_REQUEST it should be HttpStatus.BAD_REQUEST.value().

import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.reactive.error.DefaultErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.server.ServerRequest;

import java.util.Map;

@Configuration
public class RestExceptionHandler {

    @Bean
    public ErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes() {
            @Override
            public Map<String, Object> getErrorAttributes(ServerRequest request,
                                                          ErrorAttributeOptions options) {
                Map<String, Object> map = super.getErrorAttributes(
                        request, options);
                map.put("status", HttpStatus.BAD_REQUEST.value());
                //map.put("message", "Fields are missing.");
                return map;
            }
        };
    }
}

1 Comment

Welcome to SO! Please don't post code-only answers but add a little textual explanation about how and why your approach works and what makes it different from the other answers given. You can find out more at our "How to write a good answer" page.
-1

The accepted answer didn't work for me in a Spring Boot 2.3.0 application, I had to subclass ErrorController to override the original status.

This is the code of BasicErrorController, applied as default:

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<>(status);
        }
        Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        return new ResponseEntity<>(body, status);
    }

As you can see the original status is kept on a variable gotten before the invocation of getErrorAttributes(). Thus, adding requestAttributes.setAttribute("javax.servlet.error.status_code", httpStatus, 0); in your custom getErrorAttibutes() doesn't really do anything.

In a custom extension of BasicErrorController (remember to add it as a bean) you can override error() and make sure status gets the value you want:

public class CustomBasicErrorController extends BasicErrorController {

    public CustomBasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
        super(errorAttributes, errorProperties);
    }

    public CustomBasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
        super(errorAttributes, errorProperties, errorViewResolvers);
    }

    @Override
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        Integer status = (Integer) body.get("status");
        if (status == HttpStatus.NO_CONTENT.value()) {
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        }
        return new ResponseEntity<>(body, HttpStatus.valueOf(status));
    }
}

1 Comment

I have added tried this approach but override method of error iss not calling it is calling the super class method. Could you please assist me on this? Cc: @codependent
-1
@Getter
public class AppException extends ResponseStatusException {
  private final ErrorAttributeOptions options;

  public AppException(HttpStatus status, String message, ErrorAttributeOptions options){
      super(status, message);
      this.options = options;
  }
}
  1. Use sendError with status in ExceptionHandler:

    @ExceptionHandler(AppException .class)
    public void appException(HttpServletResponse response) throws IOException {
         response.sendError(ex.getStatus().value());
    }
    

See Spring REST Error Handling Example

  1. You can crossbreed ResponseEntityExceptionHandler with DefaultErrorAttributes like this (Spring Boot 2.3 comes with additional ErrorAttributeOptions):
@RestControllerAdvice
@AllArgsConstructor
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    private final ErrorAttributes errorAttributes;

    @ExceptionHandler(AppException.class)
    public ResponseEntity<Map<String, Object>> appException(AppException ex, WebRequest request) throws IOException {
        Map<String, Object> body = errorAttributes.getErrorAttributes(request, ex.getOptions());
        HttpStatus status = ex.getStatus();
        body.put("status", status.value());
        body.put("error", status.getReasonPhrase());
        return ResponseEntity.status(status).body(body);
    }
}

I've checked it for MESSAGE, EXCEPTION and STACK_TRACE options.
See also Using ErrorAttributes in our custom ErrorController

1 Comment

ErrorAttributeOptions.defaults() gives you the default options for getErrorAttributes, or use ErrorAttributeOptions.of(...) to customize.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.