11
votes

Since some weeks I'm working on an rest api using spring-mvc. The REST-API is working properly and I`m almost done until one last problem when it comes to error handling with specific error-objects.

The REST-API is using JSON as format to serialize Java-Objects. When an error occurs during service-execution an specific error-object gets created and sent back to the client.

Everything is working fine when my rest-services are marked as "produces=application/json". But there are also some services which only need to return simple text using "produces=text/plain". When an error occurs in one of these services Spring-MVC will throw an HttpMediaTypeNotAcceptableException. Seems to be correct cause client asks for content-type "text/plain" but server response with "application/json".

Can you tell me what's the correct solution for this problem?

  1. Only using JSON as response content-type and wrapping simple text always in an special class object. => Seems to me not really REST like, cause REST should support multiple content-types.

  2. Every service serving "text" will be marked as "produces=application/json;text/plain" and Client also need to send both in "accept-header". => When doing it this way the API seems to support two content-types for same resource. But that`s not right. Only in case of an error the API will return JSON, otherwise it will be always "text".

Sounds for me like a really special REST question and couldn`t find related questions on this topic.

2
" When an error occurs in one of these services Spring-MVC will throw an HttpMediaTypeNotAcceptableException." Can you elaborate this? What type of error do you mean? - Nitek
Spring is comparing the "accept-header" with the response content-type. In a situation where "text/plain" was sent in accept-header and response content-type is incompatibel Spring will throw this exception (AbstractMessageConverterMethodProcessor) - Vhaos

2 Answers

3
votes

User should always specify what content it's expecting with Accept header. It's you job to return the error that was thrown/caught on the server side in the format that was specified in Accept header. In spring as far as I know it could be achieved with a special mapper. Below you can find such mapper written in groovy to handle text/html.

import groovy.xml.MarkupBuilder
import org.springframework.http.HttpInputMessage
import org.springframework.http.HttpOutputMessage
import org.springframework.http.converter.AbstractHttpMessageConverter

import static org.springframework.http.MediaType.TEXT_HTML

class ExceptionResponseHTMLConverter extends AbstractHttpMessageConverter<ExceptionResponse> {
  ExceptionResponseHTMLConverter() {
    super(TEXT_HTML)
  }

  @Override
  boolean supports(Class clazz) {
    clazz.equals(ExceptionResponse)
  }

  @Override
  ExceptionResponse readInternal(Class clazz, HttpInputMessage msg) {
    throw new UnsupportedOperationException()
  }

  @Override
  void writeInternal(ExceptionResponse e, HttpOutputMessage msg) {
    def sw = new StringWriter()
    new MarkupBuilder(sw).error {
      error(e.error)
      exception(e.exception)
      message(e.message)
      path(e.path)
      status(e.status)
      timestamp(e.timestamp)
    }
    msg.body << sw.toString().bytes
  }
}

And ExceptionResponse class:

class ExceptionResponse {
  String error
  String exception
  String message
  String path
  Integer status
  Long timestamp
}
5
votes

I was facing the same issue, and I was having the exact same question about the REST best practices.

All the articles I read about handling errors in API responses use JSON. Example here.

I don't think all of those APIs always wrap the data in JSON. Sometimes you just have to serve files, or text or non-json stuff... Also, I've stumbled upon RFC7807, which proposes a standard way to expose errors/probems with JSON format, even using its own content-type application/problem+json. Thus we can safely assume that using a different Content Type for HTTP 200 than for HTTP error codes is rather a good practice.

About how to do it with Spring Framework, it's actually very simple. Once you've understood that the "produces ={}" is basically a declarative way to say that your response will be of some type, you can imagine that it's also possible to programmatically set the type you want to return.

Here is an example API that should return application/octet-stream (a binary file).

@GetMapping(path = "/1/resources/hello", produces = {MediaType.APPLICATION_OCTET_STREAM_VALUE})
public ResponseEntity<StreamingResponseBody> getFile(@RequestParam(value = "charset", required = false, defaultValue = "UTF-8") String charset) {
    return ResponseEntity.ok().body(outputStream -> outputStream.write("Hello there".getBytes(Charset.forName(charset))));
}

When it works, it will return a file with the right content-type. Now, if you want to handle the error case (in this case, a wrong charset parameter), you can create an Exception Handler:

@ExceptionHandler(UnsupportedCharsetException.class)
public ResponseEntity<?> handleCharsetException(UnsupportedCharsetException e) {
    return ResponseEntity.badRequest().contentType(MediaType.APPLICATION_JSON_UTF8).body(new ErrorResponse("1", "Wrong charset"));
}

And now, the error case also works as expected:

GET http://localhost/1/resources/hello?charset=CRAP

HTTP/1.1 400 Bad Request
Connection: keep-alive
Transfer-Encoding: chunked
Content-Type: application/json;charset=UTF-8
Date: Mon, 25 Mar 2019 17:37:39 GMT

{
  "code": "1",
  "message": "Wrong charset"
}