406在Spring控制器中抛出异常时接受带有text / csv的接受头

时间:2017-12-15 11:34:22

标签: java spring rest

我有一个带有返回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 -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正在做这件事。有没有办法阻止这种情况发生?或者还有其他一些我失踪的可能性吗?

2 个答案:

答案 0 :(得分:7)

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

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中提到的那样,我们需要删除HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE中的@RestControllerAdvice请求属性。代码看起来类似于下面的内容(返回带有一些&#34; 500&info&#34的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);
    }
}

ErrorResponse 对象

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

}

答案 1 :(得分:1)

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

根本原因是生成的响应通过writeWithMessageConverters类中的AbstractMessageConverterMethodProcessor方法,并且其中的逻辑最终从{中选择了 first 内容类型{1}}数组并遍历其消息转换器,寻找可以将其转换为该类型的东西。

为确保我们传递该逻辑,必须在该数组中优先使用json内容类型,以便Jackson HTTP消息转换器可以转换错误:

produces

现在,我们剩下的问题是合法回复-在您的情况下,@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!"); } 。为了确保这些内容不会以json的内容类型结尾,您必须返回text/csv并设置内容类型标头。 Spring的ResponseEntity方法中的逻辑将查找并使用它。

NB:这是基于Spring Boot 2