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 \\org.apache.httpcomponents\\httpclient\\ 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.

Comment From: spring-projects-issues

Hemanth commented

Hi Lukasz Tolwinski

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