错误响应上的 REST-API 不同的内容类型

Posted

技术标签:

【中文标题】错误响应上的 REST-API 不同的内容类型【英文标题】:REST-API Different Content-Type on Error Response 【发布时间】:2015-09-02 14:10:41 【问题描述】:

几周以来,我一直在使用 spring-mvc 开发一个 rest api。 REST-API 工作正常,我几乎完成了,直到最后一个问题涉及特定错误对象的错误处理。

REST-API 使用 JSON 作为格式来序列化 Java 对象。当服务执行期间发生错误时,会创建一个特定的错误对象并将其发送回客户端。

当我的休息服务被标记为“produces=application/json”时,一切正常。但也有一些服务只需要使用“produces=text/plain”返回简单的文本。 当这些服务之一发生错误时,Spring-MVC 将抛出 HttpMediaTypeNotAcceptableException。似乎是正确的,因为客户端要求内容类型为“text/plain”,但服务器响应为“application/json”。

你能告诉我这个问题的正确解决方案是什么吗?

    仅使用 JSON 作为响应内容类型并将简单文本始终包装在特殊的类对象中。 => 在我看来不像 REST,因为 REST 应该支持多种内容类型。

    每个服务“文本”的服务都将被标记为“produces=application/json;text/plain”,并且客户端还需要在“accept-header”中发送两者。 => 当这样做时,API 似乎支持同一资源的两种内容类型。但这不对。只有在出现错误的情况下,API 才会返回 JSON,否则它将始终是“文本”。

对我来说听起来像是一个非常特殊的 REST 问题,但找不到与此主题相关的问题。

【问题讨论】:

"当这些服务之一发生错误时,Spring-MVC 将抛出 HttpMediaTypeNotAcceptableException。"你能详细说明一下吗?你的意思是什么类型的错误? Spring 将“accept-header”与响应内容类型进行比较。在accept-header中发送“text/plain”并且响应内容类型不兼容的情况下,Spring将抛出此异常(AbstractMessageConverterMethodProcessor) 【参考方案1】:

我遇到了同样的问题,我对 REST 最佳实践也有同样的问题。

我阅读的所有关于处理 API 响应中的错误的文章都使用 JSON。示例here。

我不认为所有这些 API 总是将数据包装在 JSON 中。有时你只需要提供文件、文本或非 json 的东西...... 另外,我偶然发现了RFC7807,它提出了一种使用 JSON 格式公开错误/问题的标准方法,甚至使用它自己的内容类型应用程序/问题+json。 因此,我们可以放心地假设,对 HTTP 200 使用与 HTTP 错误代码不同的内容类型是一种很好的做法。

关于如何用 Spring Framework 来做,其实很简单。一旦您了解“produces =”基本上是一种声明性方式,表示您的响应将是某种类型,您可以想象也可以以编程方式设置您想要返回的类型。

这是一个应该返回 application/octet-stream(二进制文件)的示例 API。

@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))));

当它工作时,它将返回一个具有正确内容类型的文件。 现在,如果你想处理错误情况(在这种情况下,一个错误的字符集参数),你可以创建一个异常处理程序:

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

现在,错误情况也可以正常工作:

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"

【讨论】:

... we can safely assume that using a different Content Type for HTTP 200 than for HTTP error codes is rather a good practice 实际上,客户端通过Accept 标头告诉服务器其所有功能。服务器应选择列出的表示格式之一或返回415 Unsupported Media Type 错误(-> 内容类型协商)。它也可以返回默认的表示格式,尽管客户端可能无法处理它,从而导致互操作性问题。 @nimai THX!我有你在回答中描述的这个确切的星座(produces=APPLICATION_OCTET_STREAM_VALUEerror=APPLICATION_JSON_UTF8),它几乎让我发疯。 关于更改内容类型:这不是 RFC 7807 的建议,恰恰相反。例如。除了 JSON 模型之外,它还定义了一个 XML 数据模型,并且在附录 B 中,它建议将问题细节嵌入其他格式,或者如果 API 不使用 XML 或 JSON,则将模型转换为其他格式的约定。感谢您指出此 RFC! @realMarkusSchmidt 我不同意。 RFC 确实建议使用相同的格式(json 或 XML),但它仍然为“问题”定义了与常规 api 内容类型不同的内容类型,这就是我的观点。【参考方案2】:

用户应始终使用 Accept 标头指定预期的内容。以Accept 标头中指定的格式返回在服务器端引发/捕获的错误是您的工作。据我所知,在春天可以使用特殊的映射器来实现。您可以在下面找到用 groovy 编写的此类映射器来处理 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
  

还有ExceptionResponse类:

class ExceptionResponse 
  String error
  String exception
  String message
  String path
  Integer status
  Long timestamp

【讨论】:

感谢您的快速响应!当我正确理解您的建议时,您将编写一个自定义 MessageConverter,它以字符串格式返回错误对象,这不会导致 HttpMediaTypeNotAcceptableException。我没有考虑过这个选项,并认为它在其他情况下会非常有用,但对于我的问题,我认为它不合适。客户端应该能够以通用/通用方式处理错误(期望它始终是 json)。我认为它更像是一个 REST 风格的问题,而不是一个技术问题。一般如何在 rest api 中处理这个问题。 一般来说,您应该始终以Accept 标头中发送的格式返回数据或错误。在您的情况下,如果客户端确实需要每个响应都采用 JSON 格式,您可以这样做,但是不应发布此 API,因为它的行为方式出乎意料。它可能会有所帮助,请点赞。 您也可以以可解析的方式将响应返回为text/plain 我认为您的回答将是解决我的问题的一种方法。最后,我相信你说的只是响应客户端在接受标头中发送的内容类型是正确的。所以我会将我的 API 更改为始终使用 JSON...

以上是关于错误响应上的 REST-API 不同的内容类型的主要内容,如果未能解决你的问题,请参考以下文章

WCF:具有不同内容类型的请求/响应

空视图上的Django rest框架错误:必须先呈现响应内容,然后才能对其进行迭代

响应式布局的一些主要内容

Quarkus 找不到内容类型多部分/表单数据休息客户端的编写器

AFNetworking 2 响应错误(内容类型:文本/html 而非 JSON)

Asp.Net Web API 错误:“ObjectContent`1”类型无法序列化内容类型“application/xml”的响应正文;字符集=utf-8'