【问题标题】:406 when exception thrown in Spring controller with accept header of text/csv406 在 Spring 控制器中引发异常时接受 text/csv 标头
【发布时间】:2017-12-15 11:34:22
【问题描述】:

我有一个控制器,它的方法返回 text/csv。这适用于正常的成功案例,但如果抛出异常,并且我有 Accept: text/csv 的标头,我会收到 406 响应。例如:

@RequestMapping(value = "/foo", method = RequestMethod.GET, produces = "text/csv")
public String getCsv() {
    throw new IllegalArgumentException();
}

这是一个完全普通的 Spring Boot 应用程序(Maven 项目,导入 spring-boot-starter-web-services),只包含具有上述方法的控制器。

我认为原因是框架正在将异常转换为 JSON 错误响应。如果我删除 produces 属性并发送 Accept: */* 我会得到异常的 JSON 表示。显然 JSON 不是 text/csv,因此是 406(不可接受)响应。

这是一个显示问题的 curl 请求/响应示例:

curl -v http://localhost:8080/foo -H 'accept: text/csv'
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /foo HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.0
> accept: text/csv
> 
< HTTP/1.1 406 
< X-Application-Context: application
< Content-Length: 0
< Date: Sat, 16 Dec 2017 23:04:05 GMT
< 
* Connection #0 to host localhost left intact

但是,有趣的是,如果我查看 Spring 应用程序中的 /trace 端点,我会看到一些不同的东西:

{
    "timestamp": 1513465445542,
    "info": {
        "method": "GET",
        "path": "/foo",
        "headers": {
            "request": {
                "host": "localhost:8080",
                "user-agent": "curl/7.47.0",
                "accept": "text/csv"
            },
            "response": {
                "X-Application-Context": "application",
                "status": "500"
            }
        },
        "timeTaken": "1"
    }
}

所以,Spring 认为它返回 500,但当它卷曲时,它是 406。如果我从 PostMan 发送请求,我会看到完全相同的内容。

我不确定是什么导致了从 500 到 406 的变化。我认为它不是客户端,所以我最好的猜测是 Tomcat 正在这样做。有没有办法阻止这种情况发生?还是我错过了其他一些可能性?

【问题讨论】:

    标签: java spring rest


    【解决方案1】:

    ==== 原始答案(解释预期行为)====

    Accept 标头指定客户端期望服务器响应的格式类型。对此的任何差异都会导致HTTP 406 - Not Acceptable 错误。但是,此错误并不意味着操作失败,而是表明客户端对指定格式的期望失败。

    在您的情况下,Accept 标头带有text/csv,但服务器以application/json 响应,因此出现406 错误,因为存在明显的不匹配。

    要纠正这种行为,不需要对服务器/弹簧端进行任何更改。相反,客户端应该开始发送Accept 标头,该标头将携带application/json,text/csv 的值。这将确保客户端期望这两种格式并在有效/错误响应的情况下支持它们。

    更多详情请参考here

    编辑 2017 年 12 月 22 日

    Spring 团队 here 确认观察到的行为是一个错误。尚无已知的解决方法。

    编辑 2018 年 1 月 4 日

    正如Spring JIRA comments 中提到的一种解决方法,我们需要删除@RestControllerAdvice 中的HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE 请求属性。代码可能如下所示(返回带有一些“信息”的 500 - 还返回对象的序列化版本)。

    休息控制器建议

    @RestControllerAdvice
    public class ExampleControllerAdvice {
    
        @ExceptionHandler(value = Exception.class)
        public ResponseEntity<ErrorResponse> handleException(HttpServletRequest request, Exception e) {
            ErrorResponse response = new ErrorResponse();
            response.setErrorMsg("Server error " + e); // or whatever you want
            response.setErrorCode("ERROR007"); // or whatever you want
            request.removeAttribute(
                      HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
    
            return new ResponseEntity<ErrorResponse>(response, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    

    错误响应对象

    public class ErrorResponse {
    
        private String errorCode;
        private String errorMsg;
    
        public String getErrorCode() {
            return errorCode;
        }
    
        public void setErrorCode(String errorCode) {
            this.errorCode = errorCode;
        }
    
        public String getErrorMsg() {
            return errorMsg;
        }
    
        public void setErrorMsg(String errorMsg) {
            this.errorMsg = errorMsg;
        }
    
    }
    

    编辑 2019 年 6 月 27 日

    这是 fixed 现在在 Spring Framework 中。请求属性 HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE 现在在处理异常之前被 Spring 自动删除。

    【讨论】:

    • 好的,所以我能够重现该问题。您看到的 500 错误是因为控制器代码抛出了 IllegalArgumentException。 406 是因为 spring 无法为错误响应找到合适的消息转换器。我为 spring 启用了 DEBUG 日志,并在日志中得到了这个异常org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation这是 406 的来源)。我也尝试配置额外的消息转换器,但还没有突破。不过会继续努力。
    • 太好了,是的,这听起来就像我脑海中漂浮的模糊想法,但表达得更简洁,并且有实际证据:) 我不知道消息转换器是否是正确的解决方案,因为我认为响应应该仍然是 application/json,忽略 Accept 标头。我认为您最初是正确的,客户端应该请求 text/CSV, application/json,以便获得详细的错误消息,但 Spring 似乎不尊重客户端将接受任何一种类型的事实。
    • 我创建了一个bug on spring forum。希望从那里得到回应/解决方案。
    • 我已经尝试了解决方法,它按预期工作 - 我通过将 @ExceptionHandler 放在控制器类中稍微简化了它,因为我只需要在单个控制器中,但原理是相同的。谢谢邦德先生 :)
    • 我在 springboot 1.5.9.RELEASE 中遇到了这个问题即使 Accept 标头为 */" 很奇怪。
    【解决方案2】:

    在使用spring-hateoas 从全局异常处理程序返回VndErrors.VndError 时,我也看到了这个问题。

    根本原因是生成的响应通过AbstractMessageConverterMethodProcessor 类中的writeWithMessageConverters 方法,其中的逻辑最终从produces 数组中选择first 内容类型,然后循环通过其消息转换器寻找可以将其转换为该类型的东西。

    为了确保我们通过该逻辑,json 内容类型必须在该数组中的第一个,以便 Jackson HTTP 消息转换器可以转换错误:

    @GetMapping(value = "/foo", produces = { MediaType.APPLICATION_JSON_UTF8_VALUE, "text/csv" } )
    public ResponseEntity<String> getCsv() {
    
      if(hasItFailed()) {    
        throw new IllegalArgumentException();
      }
    
      return ResponseEntity
                .ok()
                .header(HttpHeaders.CONTENT_TYPE, "text/csv")
                .body("it worked!");
    }
    

    现在我们剩下的是合法响应的问题 - 在您的情况下是 text/csv。为确保它们不会以 json 的内容类型结束,您必须返回 ResponseEntity 并设置内容类型标头。 Spring 的writeWithMessageConverters 方法中的逻辑会查找并使用它。

    注意:这是基于 spring boot 2

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-12-24
      • 2011-11-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多