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: - bug.zip (4.17 kB)
Issue Links: - #14004 Update reference documentation on handling 401 response in the RestTemplate ("duplicates")
1 votes, 3 watchers
Comment From: spring-projects-issues
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!
Comment From: spring-projects-issues
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
Comment From: spring-projects-issues
Hemanth commented
Seems like this issue is similar to:
14004
Comment From: spring-projects-issues
Lukasz Tolwinski commented
Hi Brian Clozel
adding dep
\
Comment From: spring-projects-issues
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; }
});
Comment From: spring-projects-issues
Brian Clozel commented
Indeed, this is a duplicate of #14004
Comment From: spring-projects-issues
Lukasz Tolwinski commented
yep. the only thing you need is to set a different HttpClient
template.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
Comment From: HackerTheMonkey
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>
Comment From: qclucky7
Springboot 2.2.2.RELEASE Caused by: java.lang.ClassNotFoundException: org.apache.http.client.HttpClient at java.net.URLClassLoader.findClass(URLClassLoader.java:382) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ... 90 more
Comment From: thomasturrell
I believe that in Spring 6 the correct dependancy is:
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
Comment From: ShenFeng312
public ClientHttpRequestFactory buildRequestFactory() {
if (this.requestFactory != null) {
return this.requestFactory.apply(this.requestFactorySettings);
}
if (this.detectRequestFactory) {
return ClientHttpRequestFactories.get(this.requestFactorySettings);
}
return null;
}
org.springframework.boot.web.client.RestTemplateBuilder#buildRequestFactory
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
is right