Jeff Synnestvedt opened SPR-16781 and commented
When a rest template is customized with a ResponseErrorHandler聽 that does not return true on hasError聽 or does not throw an exception in handleError for an http 401 response then something like the following is thrown:
org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://localhost:36639/someservice": cannot retry due to server authentication, in streaming mode; nested exception is java.net.HttpRetryException: cannot retry due to server authentication, in streaming mode at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:741) at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:684) at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:629) at com.example.demo.DemoApplicationTests.error401_withcustomhandler_noerrors(DemoApplicationTests.java:118) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:73) at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:83) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26) at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75) at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86) at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86) at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)Caused by: java.net.HttpRetryException: cannot retry due to server authentication, in streaming mode at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1674) at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1474) at java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:480) at org.springframework.http.client.SimpleClientHttpResponse.getRawStatusCode(SimpleClientHttpResponse.java:55) at org.springframework.boot.actuate.metrics.web.client.RestTemplateExchangeTags.getStatusMessage(RestTemplateExchangeTags.java:94) at org.springframework.boot.actuate.metrics.web.client.RestTemplateExchangeTags.status(RestTemplateExchangeTags.java:86) at org.springframework.boot.actuate.metrics.web.client.DefaultRestTemplateExchangeTagsProvider.getTags(DefaultRestTemplateExchangeTagsProvider.java:43) at org.springframework.boot.actuate.metrics.web.client.MetricsClientHttpRequestInterceptor.getTimeBuilder(MetricsClientHttpRequestInterceptor.java:97) at org.springframework.boot.actuate.metrics.web.client.MetricsClientHttpRequestInterceptor.intercept(MetricsClientHttpRequestInterceptor.java:70) at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:92) at org.springframework.http.client.InterceptingClientHttpRequest.executeInternal(InterceptingClientHttpRequest.java:76) at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48) at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:53) at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:727) ... 35 more
聽
I am expecting that if my error handler doesn't want to consider 401's errors then it shouldn't throw an error further down the stack.
Affects: 5.0.5
Reference URL: https://github.com/spring-projects/spring-framework-issues/pull/179
Attachments:
Issue Links:
1 votes, 3 watchers
Brian Clozel commented
Hi Jeff Synnestvedt,
First, thanks a lot for your repro project!
I agree this is annoying, but I don't think there's anything we can fix here. But there are ways to work around this behavior.
The ResponseErrorHandler contract is about checking whether the given response should be considered as an error - but it doesn't mean it will catch all errors that can happen when reading/extracting the response body at a later phase (for example, an IOException while reading the response body). This is what's happening in your sample code.
This behavior is still surprising and there are ways to fix that.
You can choose to use a different HTTP client (i.e. not the JDK one); Spring Framework supports several. Adding the following dependency to your sample fixes things:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
This points to an implementation detail in the JDK HTTP client. In Spring Framework, we're using the SimpleClientHttpResponse to make sure that we use the error stream or the regular input stream, depending on the case. In this example, none of them works.
I suspect this might be linked to a combination of the test server/response used in your setup and the JDK client, since sending the same request against "http://httpbin.org/status/401" works well. The difference should be somewhere in the HttpUrlConnection implementation.
I'm closing this issue for now, but don't hesitate to reopen it if you've found a way to improve this behavior. Thanks!
Hemanth commented
I have similar issue.
When rest call returns UNAUTHORIZED then response body is lost/ignored by RestTemplate.
However when I do the same request using any gui-client, then UNAUTHORIZED status AND not-empty response body are returned.
I did also another test: temporarily changed return status to OK on the endpoint and then response body is not-empty as expected. Reverting to UNAUTHORIZED again returns empty body.
For sure when response status is UNAUTHORIZED RestTemplate loses response body.
For me it's a bug.
+Copied above text from:+ https://github.com/spring-projects/spring-security-oauth/issues/441#issuecomment-337850318
Lukasz Tolwinski commented
Hi Brian Clozel
adding dep
\
is enough for Spring Boot and RestTemplate to use it? i've just added that to my project, but it didn't solve the problem (cannot retry due to server authentication) when i try to handle 401 error.
聽
Hemanth commented
I too faced same issue. Adding dependency did not resolve my problem either.
As mentioned in #14004
After adding below lines of code, my problem was resolved!
template.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
template.setErrorHandler(new DefaultResponseErrorHandler() { public boolean hasError(ClientHttpResponse response) throws IOException { HttpStatus statusCode = response.getStatusCode(); return statusCode.series() == HttpStatus.Series.SERVER_ERROR; }
});
聽
Brian Clozel commented
Indeed, this is a duplicate of聽#14004
Lukasz Tolwinski commented
yep. the only thing you need is to set a different HttpClient
聽
聽template.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
聽
I've faced the same issue, adding this dependency on a different HTTP client as suggested by @spring-issuemaster resolved the issue and the 401 is been interpreted as expected. thanks 馃憤
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
Most helpful comment
I've faced the same issue, adding this dependency on a different HTTP client as suggested by @spring-issuemaster resolved the issue and the 401 is been interpreted as expected. thanks 馃憤