REST-API Different Content-Type on Error Response

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"
}

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
}