英文:
How do I change only the status code on a Spring MVC error with Boot?
问题
我正在编写一个Web应用程序,该应用程序使用RestTemplate进行下游调用。如果底层服务返回401未经授权状态码,我希望将401状态码返回给调用应用程序;默认行为是返回500状态码。我希望保留由BasicErrorController
提供的默认Spring Boot错误响应;我想要的唯一更改是设置状态码。
在自定义异常中,我只需使用@ResponseStatus
注解异常类,但我不能在这里这样做,因为HttpClientErrorException.Unauthorized
是由Spring提供的。我尝试了两种使用@ControllerAdvice
的方法:
@ExceptionHandler(HttpClientErrorException.Unauthorized.class)
@ResponseStatus(UNAUTHORIZED)
public void returnsEmptyBody(HttpClientErrorException.Unauthorized ex) {
}
@ExceptionHandler(HttpClientErrorException.Unauthorized.class)
@ResponseStatus(UNAUTHORIZED)
public void doesNotUseBasicErrorController(HttpClientErrorException.Unauthorized ex) {
throw new RuntimeException(ex);
}
如何配置MVC以继续使用所有内置的Boot错误处理,除了明确覆盖状态码?
英文:
I'm writing a Web application that makes downstream calls using RestTemplate. If the underlying service returns a 401 Unauthorized, I want to also return a 401 to the calling application; the default behavior is to return a 500. I want to keep the default Spring Boot error response as provided by BasicErrorController
; the only change I want is to set the status code.
In custom exceptions, I'd just annotate the exception class with @ResponseStatus
, but I can't do that here because HttpClientErrorException.Unauthorized
is provided by Spring. I tried two approaches with @ControllerAdvice
:
@ExceptionHandler(HttpClientErrorException.Unauthorized.class)
@ResponseStatus(UNAUTHORIZED)
public void returnsEmptyBody(HttpClientErrorException.Unauthorized ex) {
}
@ExceptionHandler(HttpClientErrorException.Unauthorized.class)
@ResponseStatus(UNAUTHORIZED)
public void doesNotUseBasicErrorController(HttpClientErrorException.Unauthorized ex) {
throw new RuntimeException(ex);
}
How can I configure MVC to continue to use all of the built-in Boot error handling except for explicitly overriding the status code?
答案1
得分: 2
下面的代码对我有效——在一个包含@RestController
的应用程序中,其一个方法包括throw new HttpClientException(HttpStatus.UNAUTHORIZED)
,在嵌入式Tomcat上运行。如果你在非嵌入式Tomcat上运行(或者我怀疑,在嵌入式非Tomcat上运行),你可能必须做一些至少有些不同的事情,但我希望这个答案至少有些帮助。
@ControllerAdvice
public class Advisor {
@ExceptionHandler(HttpClientException.class)
public String handleUnauthorizedFromApi(HttpClientException ex, HttpServletRequest req) {
if (/* ex instanceof HttpClientException.Unauthorized or whatever */) {
req.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, 401);
}
return "forward:/error";
}
}
解释:当我们在处理请求X(在嵌入式servlet中)时抛出HttpClientException,通常发生的是它会一直冒泡到某个org.apache类。 (我可能会再次启动调试器,弄清楚是哪一个类,但这是一个相当高级的解释,所以并不太重要。)然后,该类将请求X发送回应用程序,但这次请求发送到"/error",而不是原来要去的地方。在Spring Boot应用程序中(只要您不将某些自动配置关闭),这意味着请求X最终由BasicErrorController
中的某个方法处理。
好的,那么为什么这整个系统在我们不做任何处理时会向客户端发送500?因为上面提到的那个org.apache类在请求X上设置了一些内容,表明“处理出现了问题”。这是正确的做法:毕竟,处理请求X的过程中出现了一个异常,servlet 容器不得不捕获它。就容器而言,应用程序出错了。
因此,我们想要做几件事情。首先,我们希望servlet容器不认为我们搞砸了。我们通过告诉Spring在它达到容器之前捕获异常来实现这一点,即编写一个@ExceptionHandler
方法。其次,尽管我们捕获了异常,我们希望请求前往"/error"。我们通过简单地通过forward将请求发送到那里来实现这一点。第三,我们希望BasicErrorController
在发送的响应上设置正确的状态和消息。事实证明,BasicErrorController
(与其直接超类一起工作)会查看请求中的一个属性,以确定要发送给客户端的状态代码。我们因此设置了那个属性。
编辑:我在写这个过程中有点过于激动,忘记提到我不认为使用这个代码是一个好的做法。它会将您绑定到BasicErrorController
的一些实现细节,而且这不是Boot类预期使用的方式。Spring Boot通常假设您希望它完全处理您的错误,或者根本不处理;这也是一个合理的假设,因为零散的错误处理通常不是一个好主意。我对您的建议——即使上面的代码(或类似的代码)最终能够正常工作——是编写一个完全处理错误的@ExceptionHandler
,这意味着它同时设置状态和响应主体,不需要forward到其他地方。
英文:
The below code works for me -- in an app consisting of a @RestController
whose one method consisted of throw new HttpClientException(HttpStatus.UNAUTHORIZED)
, running on an embedded Tomcat. If you're running on a non-embedded Tomcat (or, I suspect, on an embedded non-Tomcat) odds are you'll have to do something at least somewhat different, but I hope this answer is at least somewhat helpful anyway.
@ControllerAdvice
public class Advisor {
@ExceptionHandler(HttpClientException.class)
public String handleUnauthorizedFromApi(HttpClientException ex, HttpServletRequest req) {
if (/* ex instanceof HttpClientException.Unauthorized or whatever */) {
req.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, 401);
}
return "forward:/error";
}
}
Explanation: when a HttpClientException is thrown while we're processing request X (in an embedded servlet), what normally happens is that it bubbles all the way up to some org.apache class. (I might fire the debugger up again and work out which one, but this is a pretty high-level explanation so it doesn't matter much.) That class then sends request X back to the application, except this time the request goes to "/error", not to wherever it was originally going. In a Spring Boot app (as long as you don't turn some autoconfiguration off), that means that request X is ultimately processed by some method in BasicErrorController
.
OK, so why does this whole system send a 500 to the client unless we do something? Because that org.apache class mentioned above sets something on request X which says "processing this went wrong". It is right to do so: processing request X did, after all, result in an exception which the servlet container had to catch. As far as the container is concerned, the app messed up.
So we want to do a couple of things. First, we want the servlet container to not think we messed up. We achieve this by telling Spring to catch the exception before it reaches the container, ie by writing an @ExceptionHandler
method. Second, we want the request to go to "/error" even though we caught the exception. We achieve this by the simple method of sending it there ourselves, via a forward. Third, we want the BasicErrorController
to set the correct status and message on the response it sends. It turns out that BasicErrorController
(working in tandem with its immediate superclass) looks at an attribute on the request to determine what status code to send to the client. (Figuring this out requires reading the class's source code, but that source code is on github and perfectly readable.) We therefore set that attribute.
EDIT: I got a bit carried away writing this and forgot to mention that I don't think using this code is good practice. It ties you to some implementation details of BasicErrorController
, and it's just not the way that the Boot classes are expected to be used. Spring Boot generally assumes that you want it to handle your error completely or not at all; this is a reasonable assumption, too, since piecemeal error handling is generally not a great idea. My recommendation to you -- even if the code above (or something like it) does wind up working -- is to write an @ExceptionHandler
that handles the error completely, meaning it sets both status and response body and doesn't forward to anything.
答案2
得分: 0
你可以自定义RestTemplate的错误处理程序,以抛出你的自定义异常,然后使用@ControllerAdvice处理该异常,就像你提到的那样。
类似于这样:
@Configuration
public class RestConfig {
@Bean
public RestTemplate restTemplate(){
// 构建RestTemplate
RestTemplate res = new RestTemplate();
res.setErrorHandler(new MyResponseErrorHandler());
return res;
}
private class MyResponseErrorHandler extends DefaultResponseErrorHandler {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (HttpStatus.UNAUTHORIZED.equals(response.getStatusCode())) {
// 在这里抛出你的自定义异常
}
}
}
}
英文:
You can customize the error handler of the RestTemplate to throw your custom exception, and then handle that exception with the @ControllerAdvice as you mentioned.
Something like this:
@Configuration
public class RestConfig {
@Bean
public RestTemplate restTemplate(){
// Build rest template
RestTemplate res = new RestTemplate();
res.setErrorHandler(new MyResponseErrorHandler());
return res;
}
private class MyResponseErrorHandler extends DefaultResponseErrorHandler {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (HttpStatus.UNAUTHORIZED.equals(response.getStatusCode())) {
// Throw your custom exception here
}
}
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论