Search code examples
springspring-booterror-handlinghttp-status

How to force Spring Boot to set correct HTTP status code to error response?


I made this simple Spring Boot app-example to showcase my problem with the way Spring Boot handles error by default:

package com.pany.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

}

This is the Spring Security configuration:

package com.pany.app;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/**").anonymous();
    }

}

This is the REST controller:

package com.pany.app;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.HttpClientErrorException;

@RestController
public class MyController {

    @GetMapping(path = "/", produces = { "application/json" })
    public void home() {
        throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "This is my custom 400 Bad Request message");
    }

}

Now, with Spring Boot 2.1.8.RELEASE, when calling the URL http://localhost:8080/ I get an HTTP response with 500 code and the following JSON response:

{
  "timestamp": "2020-09-14T07:24:22.061+0000",
  "status": 500,
  "error": "Internal Server Error",
  "message": "400 This is my custom 400 Bad Request message",
  "path": "/"
}

However the HTTP status code is not as expected (500 instead of 400), as well as various fields in the JSON ("status", "error", "message" which has unexpected "code" in front of it).

I also tested this with Spring Boot 2.3.3.RELEASE... and now it even no longer works as before!!

{
  "timestamp": "2020-09-14T07:34:23.154+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "message": "",
  "path": "/"
}

All this is handled through such classes/interfaces as ErrorController, AbstractErrorController and the BasicErrorController (all set up in the ErrorMvcAutoConfiguration). Apparently there is a redirect to URL /error with the original request / and/or response wrapped into something else e.g. I could debug my request as beeing this: SecurityContextHolderAwareRequestWrapper[ FirewalledRequest[ org.apache.catalina.core.ApplicationHttpRequest@3751bea4]]

I found out that the JSON I get as a result is generated through a bean called DefaultErrorAttributes which extracts the HTTP status code from the request under the key "javax.servlet.error.status_code".

And I read there: https://github.com/spring-projects/spring-boot/issues/4694#issuecomment-163597864 (Dec 2015) that:

The problem's actually in BasicErrorController and is specific to HTML responses. BasicErrorController.error sets the response status using the javax.servlet.error.status_code attribute that's set by ErrorPageFilter, but BasicErrorController.errorHtml does not. This results in text/html error responses have a status of 200 OK.

In fact in my case, the ErrorPageFilter is never called.

Also found this other issue on the Spring Boot project: https://github.com/spring-projects/spring-boot/issues/20412 which is quite related to my problem but still no solution provided.

Returning to how my controller is designed (remember this is just an extremely simplified example): how can "customize" Spring Boot in order to get an HTTP response with code 400 and this JSON as response:

{
  "timestamp": "2020-09-14T07:24:22.061+0000",
  "status": 400,
  "error": "Bad Request",
  "message": "This is my custom 400 Bad Request message",
  "path": "/"
}

Thanks.


Solution

  • A simple alternative to @Controlleradvice is having custom exception as following

    @Slf4j
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public class NotFoundException extends RuntimeException{
    
      public NotFoundException(String message) {
        super(message);
        log.warn(message);
      }
    
    }
    

    Use the required Http code with @ResponseStatus annotation.

    and throw this exception when required

    throw new NotFoundException("Your message for not found goes here");