After migration to Spring Boot 3.0 test HerokuWebSecurityConfigTest::shouldRedirectWhenProtoProvided() failed dute to: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:65194/heroku": Server redirected too many times (20) for configuration @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT).

Change in test case to fixed port @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) helps to run test correctly.

When using curl to trigger endpoint http://localhost:[RANDOM_PORT_NUMBER]/heroku when the tests are running return 302 with redirection to HTTPS as expected.

~~In Spring Boot 2.7.5 works with random port setup.~~

Stack trace:

org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:65194/heroku": Server redirected too many  times (20)

at org.springframework.web.client.RestTemplate.createResourceAccessException(RestTemplate.java:888)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:868)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:764)
    at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:405)
    at org.owasp.wrongsecrets.HerokuWebSecurityConfigTest.shouldRedirectWhenProtoProvided(HerokuWebSecurityConfigTest.java:30)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
    at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
    at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
    at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: java.net.ProtocolException: Server redirected too many  times (20)
    at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:2001)
    at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1585)
    at java.base/java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:529)
    at org.springframework.http.client.SimpleBufferingClientHttpRequest.executeInternal(SimpleBufferingClientHttpRequest.java:81)
    at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
    at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:862)
    ... 71 more

Comment From: mhalbritter

There's a lot going on in that sample. Can you provide a small reproducer for the problem you're seeing?

Comment From: drnow4u

Small code example to reproduce: https://github.com/MarcinNowak-codes/wrongsecrets/tree/springboot_3_resttemplate_redirect

Comment From: mhalbritter

I've reduced it even further: gh-33451.zip

I can reproduce the issue with Boot 2.7.5, 2.7.6 and 3.0.0. DEFINED_PORT works, RANDOM_PORT fails.

I've switched on trace logging for the HttpUrlConnection:

It looks like that the Location header in the RANDOM_PORT scenario is set to the same url as the request was coming in, without switching http to https. This is repeated 20 times, and then the HttpUrlConnection aborts. This is definitely a bug somewhere.

Comment From: scottfrederick

Like @mhalbritter, I was able to reproduce the Server redirected too many times (20) error with Boot 2.7 and 3.0 with a greatly simplified sample.

In Spring Boot 2.7.5 works with random port setup.

@MarcinNowak-codes Your sample has many dependencies (mostly unused) and several classes that Moritz's sample removed. It is difficult for us to change the Spring Boot version in your sample from 3.0.0 to 2.7.5 because of all the extra dependencies that need version changes also. Can you provide a simplified sample with minimal dependencies that shows the behavior difference between 3.0 and 2.7 that you reported? That might help us narrow the problem down to Spring Boot, Spring Security, or something else.

Comment From: drnow4u

The reduced working version 2.7.5 is https://github.com/MarcinNowak-codes/wrongsecrets/tree/springboot_2.7/2.7.x

The test HerokuWebSecurityConfigTest:: shouldRedirectwhenProtoProvided already tried to bypass the circular redirection problem by catching ResourceAccessException.

In my sample DefinedPortExceptionTest throws an exception:

Caused by: org.apache.http.conn.HttpHostConnectException: Connect to localhost:8443 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused

For the random port test RandomPortExceptionTest another exception is observed:

Caused by: org.apache.http.client.CircularRedirectException: Circular redirect to 'http://localhost:64491/heroku'

This issue can be reproduced on 2.7.5 and is not related to migration to 3.0.0. I crossed out:

~~In Spring Boot 2.7.5 works with random port setup.~~

Comment From: scottfrederick

This issue can be reproduced on 2.7.5 and is not related to migration to 3.0.0.

Thanks for confirming, that helps the diagnosis a lot.

Comment From: philwebb

I think the actual problem here is that running the test on any port other than 8080 or 80 will fail. If you change the DefinedPortWorksTests class in @mhalbritter's sample to the following things also fail:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, properties = "server.port=7070")
public class DefinedPortWorksTests {

The reason for this is that the default org.springframework.security.web.PortMapper implementation knows about these default ports but not any custom ones.

Comment From: philwebb

I don't think this is a Spring Boot bug. The best way I can see to fix this is to allow a custom PortMapper to be plugged in to your tests.

If you update your security config to something like this:

@Bean
@Order(1)
public SecurityFilterChain configureHerokuWebSecurity(HttpSecurity http,
        ObjectProvider<PortMapper> portMapper) throws Exception {
    portMapper.ifAvailable(http.portMapper()::portMapper);
    http.requiresChannel().requestMatchers(this::isHerokuRequest).requiresSecure();
    return http.build();
}

Then you can change your tests to use a custom PortMapper. For example, you can define a convention based mapper:

public class ConventionPortMapper implements PortMapper {

    @Override
    public Integer lookupHttpPort(Integer httpsPort) {
        return (httpsPort != null) ? httpsPort - 1 : null;
    }

    @Override
    public Integer lookupHttpsPort(Integer httpPort) {
        return (httpPort != null) ? httpPort + 1 : null;
    }

}

Then add the following to the test:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Import(ConventionPortMapper.class)
public class RandomPortFailsTests {

    ...

}