I have build an error controller that should be the "last line" for catching exceptions within my Spring REST service. However it seems that I can't return POJOs as response type. Why does Jackson not working for this case?
My Class looks like:
@RestController
public class CustomErrorController implements ErrorController
{
private static final String PATH = "/error";
@Override
public String getErrorPath()
{
return PATH;
}
@RequestMapping (value = PATH)
public ResponseEntity<WebErrorResponse> handleError(HttpStatus status, HttpServletRequest request)
{
WebErrorResponse response = new WebErrorResponse();
// original requested URI
String uri = String.valueOf(request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI));
// status code
String code = String.valueOf(request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE));
// status message
String msg = String.valueOf(request.getAttribute(RequestDispatcher.ERROR_MESSAGE));
response.title = "Internal Server Error";
response.type = request.getMethod() + ": " + uri;
response.code = Integer.valueOf(code);
response.message = msg;
// build headers
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
// build the response
return new ResponseEntity<>(response, headers, status);
}
public class WebErrorResponse
{
/**
* The error message.
*/
public String message;
/**
* The status code.
*/
public int code;
/**
* The error title.
*/
public String title;
/**
* The error type.
*/
public String type;
}
This should work but the only response is a Jetty Error Message with 406 - Not Acceptable.
Changing the the response entity body type to String works perfect. Whats wrong? Maybe this is a bug?
P.S: Working with Spring 4.2.8, Spring Boot 1.3.8.
FINAL SOLUTION
After many try-and-error loops and round trips in Google I finally found a solution that does what I want. The main problems with error handling in Spring are caused by the default behavior and the small documentation.
Using only Spring without Spring Boot is no problem. But using both to build a web (REST) service is like hell.
So I want to share my solution to help everybody coming accross the same b**lsh*t...
What you will need is:
Configuration (cutout important parts)
@Configuration
public class SpringConfig extends WebMvcConfigurerAdapter
{
// ... init stuff if needed
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer)
{
// setup content negotiation (automatic detection of content types)
configurer
// use format parameter and extension to detect mimetype
.favorPathExtension(true).favorParameter(true)
// set default mimetype
.defaultContentType(MediaType.APPLICATION_XML)
.mediaType(...)
// and so on ....
}
/**
* Configuration of the {@link DispatcherServlet} bean.
*
* <p>This is needed because Spring and Spring Boot auto-configuration override each other.</p>
*
* @see <a href="http://stackoverflow.com/questions/28902374/spring-boot-rest-service-exception-handling">
* Stackoverflow - Spring Boot REST service exception handling</a>
*
* @param dispatcher dispatcher servlet instance
*/
@Autowired
@SuppressWarnings ("SpringJavaAutowiringInspection")
public void setupDispatcherServlet(DispatcherServlet dispatcher)
{
// FIX: for global REST error handling
// enable exceptions if endpoint not found (instead of static error page)
dispatcher.setThrowExceptionIfNoHandlerFound(true);
}
/**
* Creates the error properties used to setup the global REST error controller.
*
* <p>Using {@link ErrorProperties} is compliant to base implementation if Spring Boot's
* {@link org.springframework.boot.autoconfigure.web.BasicErrorController}.</p>
*
*
* @return error properties
*/
@Bean
public ErrorProperties errorProperties()
{
ErrorProperties properties = new ErrorProperties();
properties.setIncludeStacktrace(ErrorProperties.IncludeStacktrace.NEVER);
properties.setPath("/error");
return properties;
}
// ...
}
The Spring exception handler:
@ControllerAdvice(annotations = RestController.class)
public class WebExceptionHandler extends ResponseEntityExceptionHandler
{
/**
* This function handles the exceptions.
*
* @param e the thrown exception
*
* @return error message as XML-document
*/
@ExceptionHandler (Exception.class)
public ResponseEntity<Object> handleErrorResponse(Exception e)
{
logger.trace("Catching Exception in REST API.", e);
return handleExceptionInternal(e, null, null, null, null);
}
@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex,
Object body,
HttpHeaders headers,
HttpStatus status,
WebRequest request)
{
logger.trace("Catching Spring Exception in REST API.");
logger.debug("Using " + getClass().getSimpleName() + " for exception handling.");
// fatal, should not happen
if(ex == null) throw new NullPointerException("empty exception");
// set defaults
String title = "API Error";
String msg = ex.getMessage();
if(status == null) status = HttpStatus.BAD_REQUEST;
// build response body
WebErrorResponse response = new WebErrorResponse();
response.type = ex.getClass().getSimpleName();
response.title = title;
response.message = msg;
response.code = status.value();
// build response headers
if(headers == null) headers = new HttpHeaders();
try {
headers.setContentType(getContentType(request));
}
catch(NullPointerException e)
{
// ignore (empty headers will result in default)
}
catch(IllegalArgumentException e)
{
// return only status code
return new ResponseEntity<>(status);
}
return new ResponseEntity<>(response, headers, status);
}
/**
* Checks the given request and returns the matching response content type
* or throws an exceptions if the requested content type could not be delivered.
*
* @param request current request
*
* @return response content type matching the request
*
* @throws NullPointerException if the request does not an accept header field
* @throws IllegalArgumentException if the requested content type is not supported
*/
private static MediaType getContentType(WebRequest request) throws NullPointerException, IllegalArgumentException
{
String accepts = request.getHeader(HttpHeaders.ACCEPT);
if(accepts==null) throw new NullPointerException();
// XML
if(accepts.contains(MediaType.APPLICATION_XML_VALUE) ||
accepts.contains(MediaType.TEXT_XML_VALUE) ||
accepts.contains(MediaType.APPLICATION_XHTML_XML_VALUE))
return MediaType.APPLICATION_XML;
// JSON
else if(accepts.contains(MediaType.APPLICATION_JSON_VALUE))
return MediaType.APPLICATION_JSON_UTF8;
// other
else throw new IllegalArgumentException();
}
}
And the error controller for Spring Boot:
@Controller
@RequestMapping("/error")
public class CustomErrorController extends AbstractErrorController
{
protected final Logger logger = LoggerFactory.getLogger(getClass());
/**
* The global settings for this error controller.
*/
private final ErrorProperties properties;
/**
* Bean constructor.
*
* @param properties global properties
* @param attributes default error attributes
*/
@Autowired
public CustomErrorController(ErrorProperties properties, ErrorAttributes attributes)
{
super(attributes);
this.properties = new ErrorProperties();
}
@Override
public String getErrorPath()
{
return this.properties.getPath();
}
/**
* Returns the configuration properties of this controller.
*
* @return error properties
*/
public ErrorProperties getErrorProperties()
{
return this.properties;
}
/**
* This function handles runtime and application errors.
*
* @param request the incorrect request instance
*
* @return error message as XML-document
*/
@RequestMapping (produces = {MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_XML_VALUE})
@ResponseBody
public ResponseEntity<Object> handleError(HttpServletRequest request)
{
logger.trace("Catching Exception in REST API.");
logger.debug("Using {} for exception handling." , getClass().getSimpleName());
// original requested REST endpoint
String endpoint = String.valueOf(request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI));
// status code
String code = String.valueOf(request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE));
// thrown exception
Exception ex = ((Exception) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION));
if(ex == null) {
ex = new RuntimeException(String.valueOf(request.getAttribute(RequestDispatcher.ERROR_MESSAGE)));
}
// release nested exceptions (we want source exception only)
if(ex instanceof NestedServletException && ex.getCause() instanceof Exception) {
ex = (Exception) ex.getCause();
}
// build response body
WebErrorResponse response = new WebErrorResponse();
response.title = "Internal Server Error";
response.type = ex.getClass().getSimpleName();
response.code = Integer.valueOf(code);
response.message = request.getMethod() + ": " + endpoint+"; "+ex.getMessage();
// build response headers
HttpHeaders headers = new HttpHeaders();
headers.setContentType(getResponseType(request));
// build the response
return new ResponseEntity<>(response, headers, getStatus(request));
}
/*@RequestMapping (produces = {MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_XML_VALUE})
public ResponseEntity<Map<String, Object>> handleError(HttpServletRequest request)
{
Boolean stacktrace = properties.getIncludeStacktrace().equals(ErrorProperties.IncludeStacktrace.ALWAYS);
Map<String, Object> r = getErrorAttributes(request, stacktrace);
return new ResponseEntity<Map<String, Object>>(r, getStatus(request));
}*/
/**
* Extracts the response content type from the "Accept" HTTP header field.
*
* @param request request instance
*
* @return response content type
*/
private MediaType getResponseType(HttpServletRequest request)
{
String accepts = request.getHeader(HttpHeaders.ACCEPT);
// only XML or JSON allowed
if(accepts.contains(MediaType.APPLICATION_JSON_VALUE))
return MediaType.APPLICATION_JSON_UTF8;
else return MediaType.APPLICATION_XML;
}
}
So thats it, the POJO WebErrorResponse is a plain class using only public Strings and int fields.
The above classes works for a REST API that support XML and JSON. How it works:
I hope that will clarify things for others that are confused as me.
Best regards,
Zipunrar
This is a content negotiation issue. Basically, the request is asking for a response in a specific format, and the server is saying it cannot deliver the response in that format.
There are a couple things that could be the problem here.
application/json
for the Accept
header.Accept
header with a value of application/json
, but the Spring Web MVC configuration is not setup to handle JSON content types.In the first case, it would be wise to specify the type that the requester can handle. I would do this whether or not it is your exact issue.
In the second case, Spring defaults to do content negotiation via XML. You can modify this default behavior by adding a WebMvcConfigurer to your ApplicationContext:
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_JSON);
}
}
Further, it would be wise to be more explicit on your @RequestMapping
annotations. Make sure to utilize the 'hint' parameters of consumes
and produces
(which assists with mapping requests via the Accepts
and Content-type
request headers).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With