From d0fd5d4ed6b8f7527a38cee248a655892a8a0106 Mon Sep 17 00:00:00 2001 From: Sharang Gupta Date: Thu, 15 Oct 2026 11:52:51 +0100 Subject: [PATCH 1/2] Point at the credentials when the config server sends its login page When a config server is secured with form login and the config client sends missing or wrong credentials, the server redirects the request to its HTML login page. The client then fails with an UnknownContentTypeException ("no suitable HttpMessageConverter found ... content type [text/html]"), which does not say what is wrong. Both the ConfigData loader and the bootstrap property source locator now turn that exception into an IllegalStateException whose message names the config server URI, the status and the content type. For an HTML response it says that a secured config server answers with its login page when the credentials are missing or wrong and names spring.cloud.config.username and spring.cloud.config.password. For any other content type it asks to check that spring.cloud.config.uri points at a config server. Fixes gh-1723 Signed-off-by: Sharang Gupta --- .../client/ConfigServerConfigDataLoader.java | 23 +++++++ .../ConfigServicePropertySourceLocator.java | 4 ++ .../ConfigServerConfigDataLoaderTests.java | 48 +++++++++++++++ ...nfigServicePropertySourceLocatorTests.java | 16 +++++ .../client/MockConfigServerResponses.java | 61 +++++++++++++++++++ 5 files changed, 152 insertions(+) create mode 100644 spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/MockConfigServerResponses.java diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java index a82943ca9b..32eb28e16a 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java @@ -55,6 +55,7 @@ import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.UnknownContentTypeException; import static org.springframework.cloud.config.client.ConfigClientProperties.DEFAULT_PROFILE; import static org.springframework.cloud.config.client.ConfigClientProperties.STATE_HEADER; @@ -222,6 +223,25 @@ else if (ALL_OPTIONS.size() > 2) { return null; } + /** + * Describes a config server response that is not an {@link Environment}. An HTML + * response is usually the login page of a secured config server, so the message then + * points at the credentials of the config client. + * @param uri the config server URI + * @param ex the exception thrown for the response + * @return the message + */ + static String unreadableResponseMessage(String uri, UnknownContentTypeException ex) { + String message = "Could not read the response of the config server at " + uri + " (status " + ex.getStatusCode() + + ", content type " + ex.getContentType() + "). "; + if (MediaType.TEXT_HTML.includes(ex.getContentType())) { + return message + "A secured config server responds with its login page when the credentials of the " + + "config client are missing or wrong: check them, such as spring.cloud.config.username and " + + "spring.cloud.config.password."; + } + return message + "Check that spring.cloud.config.uri points at a config server."; + } + protected void log(Environment result) { if (logger.isInfoEnabled()) { logger.info(String.format("Located environment: name=%s, profiles=%s, label=%s, version=%s, state=%s", @@ -368,6 +388,9 @@ protected Environment getRemoteEnvironment(ConfigDataLoaderContext context, Conf continue; } } + catch (UnknownContentTypeException e) { + throw new IllegalStateException(unreadableResponseMessage(uri, e), e); + } if (response == null) { if (i < noOfUrls - 1 && properties.getMultipleUriStrategy() == MultipleUriStrategy.ALWAYS) { diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java index b38ecbdd2a..1b8ae4a435 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java @@ -57,6 +57,7 @@ import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.UnknownContentTypeException; import static org.springframework.cloud.config.client.ConfigClientProperties.STATE_HEADER; import static org.springframework.cloud.config.client.ConfigClientProperties.TOKEN_HEADER; @@ -310,6 +311,9 @@ private Environment getRemoteEnvironment(ConfigClientRequestTemplateFactory requ continue; } } + catch (UnknownContentTypeException e) { + throw new IllegalStateException(ConfigServerConfigDataLoader.unreadableResponseMessage(uri, e), e); + } if (response == null) { if (i < noOfUrls - 1 && defaultProperties.getMultipleUriStrategy() == MultipleUriStrategy.ALWAYS) { diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoaderTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoaderTests.java index 6d05d80d25..8f44f19c1f 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoaderTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoaderTests.java @@ -62,6 +62,7 @@ import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.UnknownContentTypeException; import static java.lang.String.format; import static org.assertj.core.api.Assertions.assertThat; @@ -70,6 +71,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.cloud.config.client.ConfigClientProperties.AUTHORIZATION; import static org.springframework.cloud.config.environment.EnvironmentMediaType.V2_JSON; @@ -220,6 +222,52 @@ public void failFastWhenNotFound() throws Exception { "fail fast property is set, failing: None of labels [WeSetUpToReturn_NOT_FOUND_ForThisLabel] found"); } + @Test + public void failFastPointsAtCredentialsWhenServerRespondsWithLoginPage() { + when(bootstrapContext.get(RestTemplate.class)) + .thenReturn(new RestTemplate(MockConfigServerResponses.loginPage())); + properties.setUri(new String[] { "http://user:secret@localhost:8888" }); + properties.setFailFast(true); + + assertThatExceptionOfType(ConfigClientFailFastException.class) + .isThrownBy(() -> this.loader.load(context, resource)) + .withMessageContaining("fail fast property is set") + .withCauseExactlyInstanceOf(IllegalStateException.class) + .havingCause() + .withMessageContaining("config server at http://localhost:8888") + .withMessageContaining("content type text/html") + .withMessageContaining("login page") + .withMessageContaining("spring.cloud.config.username") + .withMessageNotContaining("secret") + .withCauseExactlyInstanceOf(UnknownContentTypeException.class); + } + + @Test + public void failFastPointsAtUriWhenServerRespondsWithOtherContent() { + when(bootstrapContext.get(RestTemplate.class)) + .thenReturn(new RestTemplate(MockConfigServerResponses.respondingWith(MediaType.TEXT_PLAIN, "OK"))); + properties.setFailFast(true); + + assertThatExceptionOfType(ConfigClientFailFastException.class) + .isThrownBy(() -> this.loader.load(context, resource)) + .havingCause() + .withMessageContaining("content type text/plain") + .withMessageContaining("spring.cloud.config.uri") + .withMessageNotContaining("login page"); + } + + @Test + public void warningPointsAtCredentialsWhenServerRespondsWithLoginPage() { + Log log = mock(Log.class); + this.loader = new ConfigServerConfigDataLoader(destination -> log); + when(bootstrapContext.get(RestTemplate.class)) + .thenReturn(new RestTemplate(MockConfigServerResponses.loginPage())); + + assertThat(this.loader.load(context, resource)).isNull(); + + verify(log).warn(ArgumentMatchers.argThat(message -> message.toString().contains("login page"))); + } + @Test public void failFastWhenRequestTimesOut() { mockRequestTimedOut(); diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java index 66793fc9ad..8effa419e7 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java @@ -188,6 +188,22 @@ public void failFast() throws Exception { .hasMessageContaining("fail fast property is set"); } + @Test + public void failFastPointsAtCredentialsWhenServerRespondsWithLoginPage() { + ConfigClientProperties defaults = new ConfigClientProperties(this.environment); + defaults.setFailFast(true); + this.locator = new ConfigServicePropertySourceLocator(defaults); + this.locator.setRestTemplate(new RestTemplate(MockConfigServerResponses.loginPage())); + + Assertions.assertThatThrownBy(() -> this.locator.locateCollection(this.environment)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("fail fast property is set") + .cause() + .isExactlyInstanceOf(IllegalStateException.class) + .hasMessageContaining("config server at http://localhost:8888") + .hasMessageContaining("login page"); + } + @Test public void failFastWhenNotFound() throws Exception { Assertions.assertThatThrownBy(() -> { diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/MockConfigServerResponses.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/MockConfigServerResponses.java new file mode 100644 index 0000000000..50f8d5c9aa --- /dev/null +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/MockConfigServerResponses.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.config.client; + +import java.nio.charset.StandardCharsets; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.mock.http.client.MockClientHttpRequest; +import org.springframework.mock.http.client.MockClientHttpResponse; + +/** + * Request factories that answer every config client request with a fixed response. + * + * @author Sharang Gupta + */ +final class MockConfigServerResponses { + + private MockConfigServerResponses() { + } + + /** + * Answers with the HTML login page that a secured config server serves when the + * credentials of the config client are missing or wrong. + * @return the request factory + */ + static ClientHttpRequestFactory loginPage() { + return respondingWith(MediaType.TEXT_HTML, "Please sign in"); + } + + /** + * Answers with a {@code 200 OK} response that has the given content type and body. + * @param contentType the content type of the response + * @param body the body of the response + * @return the request factory + */ + static ClientHttpRequestFactory respondingWith(MediaType contentType, String body) { + MockClientHttpResponse response = new MockClientHttpResponse(body.getBytes(StandardCharsets.UTF_8), + HttpStatus.OK); + response.getHeaders().setContentType(contentType); + MockClientHttpRequest request = new MockClientHttpRequest(); + request.setResponse(response); + return (uri, httpMethod) -> request; + } + +} From 44a0fb5b76fd5e9c22905382c6b5edf3d52bbd19 Mon Sep 17 00:00:00 2001 From: Sharang Gupta Date: Thu, 15 Oct 2026 22:18:18 +0100 Subject: [PATCH 2/2] Try the next URI after an unreadable response Following the review, drop the message that pointed at the credentials for an HTML response, since a config server can send HTML for other reasons, and that put the URI of the config server in a message that can be logged. Instead, with the ALWAYS multiple URI strategy, a response that is not an Environment now moves on to the next URI, like a 4xx or 5xx response, in both the ConfigData loader and the bootstrap locator. The last URI rethrows the UnknownContentTypeException as before. Signed-off-by: Sharang Gupta --- .../client/ConfigServerConfigDataLoader.java | 27 ++---- .../ConfigServicePropertySourceLocator.java | 8 +- .../ConfigServerConfigDataLoaderTests.java | 84 ++++++++----------- ...nfigServicePropertySourceLocatorTests.java | 56 +++++++++---- .../client/MockConfigServerResponses.java | 61 -------------- 5 files changed, 89 insertions(+), 147 deletions(-) delete mode 100644 spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/MockConfigServerResponses.java diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java index 32eb28e16a..4b7befaaa8 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java @@ -223,25 +223,6 @@ else if (ALL_OPTIONS.size() > 2) { return null; } - /** - * Describes a config server response that is not an {@link Environment}. An HTML - * response is usually the login page of a secured config server, so the message then - * points at the credentials of the config client. - * @param uri the config server URI - * @param ex the exception thrown for the response - * @return the message - */ - static String unreadableResponseMessage(String uri, UnknownContentTypeException ex) { - String message = "Could not read the response of the config server at " + uri + " (status " + ex.getStatusCode() - + ", content type " + ex.getContentType() + "). "; - if (MediaType.TEXT_HTML.includes(ex.getContentType())) { - return message + "A secured config server responds with its login page when the credentials of the " - + "config client are missing or wrong: check them, such as spring.cloud.config.username and " - + "spring.cloud.config.password."; - } - return message + "Check that spring.cloud.config.uri points at a config server."; - } - protected void log(Environment result) { if (logger.isInfoEnabled()) { logger.info(String.format("Located environment: name=%s, profiles=%s, label=%s, version=%s, state=%s", @@ -389,7 +370,13 @@ protected Environment getRemoteEnvironment(ConfigDataLoaderContext context, Conf } } catch (UnknownContentTypeException e) { - throw new IllegalStateException(unreadableResponseMessage(uri, e), e); + if (i < noOfUrls - 1 && properties.getMultipleUriStrategy() == MultipleUriStrategy.ALWAYS) { + logger.info( + "Failed to read the response of a config server. Will try the next url if available. Error : " + + e.getMessage()); + continue; + } + throw e; } if (response == null) { diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java index 1b8ae4a435..4618a7feb5 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java @@ -312,7 +312,13 @@ private Environment getRemoteEnvironment(ConfigClientRequestTemplateFactory requ } } catch (UnknownContentTypeException e) { - throw new IllegalStateException(ConfigServerConfigDataLoader.unreadableResponseMessage(uri, e), e); + if (i < noOfUrls - 1 && defaultProperties.getMultipleUriStrategy() == MultipleUriStrategy.ALWAYS) { + logger.info( + "Failed to read the response of a config server. Will try the next url if available. Error : " + + e.getMessage()); + continue; + } + throw e; } if (response == null) { diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoaderTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoaderTests.java index 8f44f19c1f..549418175f 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoaderTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoaderTests.java @@ -71,7 +71,6 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.cloud.config.client.ConfigClientProperties.AUTHORIZATION; import static org.springframework.cloud.config.environment.EnvironmentMediaType.V2_JSON; @@ -222,52 +221,6 @@ public void failFastWhenNotFound() throws Exception { "fail fast property is set, failing: None of labels [WeSetUpToReturn_NOT_FOUND_ForThisLabel] found"); } - @Test - public void failFastPointsAtCredentialsWhenServerRespondsWithLoginPage() { - when(bootstrapContext.get(RestTemplate.class)) - .thenReturn(new RestTemplate(MockConfigServerResponses.loginPage())); - properties.setUri(new String[] { "http://user:secret@localhost:8888" }); - properties.setFailFast(true); - - assertThatExceptionOfType(ConfigClientFailFastException.class) - .isThrownBy(() -> this.loader.load(context, resource)) - .withMessageContaining("fail fast property is set") - .withCauseExactlyInstanceOf(IllegalStateException.class) - .havingCause() - .withMessageContaining("config server at http://localhost:8888") - .withMessageContaining("content type text/html") - .withMessageContaining("login page") - .withMessageContaining("spring.cloud.config.username") - .withMessageNotContaining("secret") - .withCauseExactlyInstanceOf(UnknownContentTypeException.class); - } - - @Test - public void failFastPointsAtUriWhenServerRespondsWithOtherContent() { - when(bootstrapContext.get(RestTemplate.class)) - .thenReturn(new RestTemplate(MockConfigServerResponses.respondingWith(MediaType.TEXT_PLAIN, "OK"))); - properties.setFailFast(true); - - assertThatExceptionOfType(ConfigClientFailFastException.class) - .isThrownBy(() -> this.loader.load(context, resource)) - .havingCause() - .withMessageContaining("content type text/plain") - .withMessageContaining("spring.cloud.config.uri") - .withMessageNotContaining("login page"); - } - - @Test - public void warningPointsAtCredentialsWhenServerRespondsWithLoginPage() { - Log log = mock(Log.class); - this.loader = new ConfigServerConfigDataLoader(destination -> log); - when(bootstrapContext.get(RestTemplate.class)) - .thenReturn(new RestTemplate(MockConfigServerResponses.loginPage())); - - assertThat(this.loader.load(context, resource)).isNull(); - - verify(log).warn(ArgumentMatchers.argThat(message -> message.toString().contains("login page"))); - } - @Test public void failFastWhenRequestTimesOut() { mockRequestTimedOut(); @@ -349,6 +302,22 @@ public void shouldThrowExceptionWhenNegativeConnectTimeoutSet() { .withMessageContaining("Invalid Value for Connect Timeout set."); } + @Test + public void shouldNotUseNextUriFor_UnreadableResponse_And_CONNECTION_TIMEOUT_ONLY_Strategy() throws Exception { + mockUnreadableResponseFromFirstUri(ConfigClientProperties.MultipleUriStrategy.CONNECTION_TIMEOUT_ONLY); + + assertThatExceptionOfType(ConfigClientFailFastException.class) + .isThrownBy(() -> this.loader.load(context, resource)) + .withCauseInstanceOf(UnknownContentTypeException.class); + } + + @Test + public void shouldUseNextUriFor_UnreadableResponse_And_ALWAYS_Strategy() throws Exception { + mockUnreadableResponseFromFirstUri(ConfigClientProperties.MultipleUriStrategy.ALWAYS); + + assertThat(this.loader.load(context, resource)).isNotNull(); + } + @Test public void shouldNotUseNextUriFor_400_And_CONNECTION_TIMEOUT_ONLY_Strategy() throws Exception { assertNextUriIsNotTriedForClientError(ConfigClientProperties.MultipleUriStrategy.CONNECTION_TIMEOUT_ONLY, @@ -827,8 +796,27 @@ private void assertNextUriIsTriedOnTimeout(ConfigClientProperties.MultipleUriStr assertThat(this.loader.load(context, resource)).isNotNull(); } + private void mockUnreadableResponseFromFirstUri(ConfigClientProperties.MultipleUriStrategy multipleUriStrategy) + throws Exception { + String badURI = "http://baduri"; + String goodURI = "http://localhost:8888"; + properties.setUri(new String[] { badURI, goodURI }); + properties.setFailFast(true); + properties.setMultipleUriStrategy(multipleUriStrategy); + this.loader = new ConfigServerConfigDataLoader(destination -> logger); + ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class); + mockRequestResponse(requestFactory, badURI, HttpStatus.OK, MediaType.TEXT_HTML); + mockRequestResponse(requestFactory, goodURI, HttpStatus.OK); + when(bootstrapContext.get(RestTemplate.class)).thenReturn(new RestTemplate(requestFactory)); + } + private void mockRequestResponse(ClientHttpRequestFactory requestFactory, String baseURI, HttpStatus status) throws Exception { + mockRequestResponse(requestFactory, baseURI, status, MediaType.APPLICATION_JSON); + } + + private void mockRequestResponse(ClientHttpRequestFactory requestFactory, String baseURI, HttpStatus status, + MediaType contentType) throws Exception { ClientHttpRequest request = mock(ClientHttpRequest.class); ClientHttpResponse response = mock(ClientHttpResponse.class); @@ -845,7 +833,7 @@ private void mockRequestResponse(ClientHttpRequestFactory requestFactory, String when(request.execute()).thenReturn(response); HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); + headers.setContentType(contentType); when(response.getHeaders()).thenReturn(headers); when(response.getStatusCode()).thenReturn(status); when(response.getBody()).thenReturn(new ByteArrayInputStream("{}".getBytes())); diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java index 8effa419e7..2def943484 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java @@ -57,6 +57,7 @@ import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.UnknownContentTypeException; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -188,22 +189,6 @@ public void failFast() throws Exception { .hasMessageContaining("fail fast property is set"); } - @Test - public void failFastPointsAtCredentialsWhenServerRespondsWithLoginPage() { - ConfigClientProperties defaults = new ConfigClientProperties(this.environment); - defaults.setFailFast(true); - this.locator = new ConfigServicePropertySourceLocator(defaults); - this.locator.setRestTemplate(new RestTemplate(MockConfigServerResponses.loginPage())); - - Assertions.assertThatThrownBy(() -> this.locator.locateCollection(this.environment)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("fail fast property is set") - .cause() - .isExactlyInstanceOf(IllegalStateException.class) - .hasMessageContaining("config server at http://localhost:8888") - .hasMessageContaining("login page"); - } - @Test public void failFastWhenNotFound() throws Exception { Assertions.assertThatThrownBy(() -> { @@ -316,6 +301,23 @@ public void shouldThrowExceptionWhenNegativeConnectTimeoutSet() { }).isInstanceOf(IllegalStateException.class).hasMessageContaining("Invalid Value for Connect Timeout set."); } + @Test + public void shouldNotUseNextUriFor_UnreadableResponse_And_CONNECTION_TIMEOUT_ONLY_Strategy() throws Exception { + mockUnreadableResponseFromFirstUri(ConfigClientProperties.MultipleUriStrategy.CONNECTION_TIMEOUT_ONLY); + + Assertions.assertThatThrownBy(() -> this.locator.locateCollection(this.environment)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("fail fast property is set") + .hasCauseInstanceOf(UnknownContentTypeException.class); + } + + @Test + public void shouldUseNextUriFor_UnreadableResponse_And_ALWAYS_Strategy() throws Exception { + mockUnreadableResponseFromFirstUri(ConfigClientProperties.MultipleUriStrategy.ALWAYS); + + assertThat(this.locator.locateCollection(this.environment)).isNotNull(); + } + @Test public void shouldNotUseNextUriFor_400_And_CONNECTION_TIMEOUT_ONLY_Strategy() throws Exception { assertNextUriIsNotTriedForClientError(ConfigClientProperties.MultipleUriStrategy.CONNECTION_TIMEOUT_ONLY, @@ -637,8 +639,28 @@ private Map originValue(String value, String origin) { return map; } + private void mockUnreadableResponseFromFirstUri(ConfigClientProperties.MultipleUriStrategy multipleUriStrategy) + throws Exception { + ConfigClientProperties clientProperties = new ConfigClientProperties(this.environment); + String badURI = "http://baduri"; + String goodURI = "http://localhost:8888"; + clientProperties.setUri(new String[] { badURI, goodURI }); + clientProperties.setFailFast(true); + clientProperties.setMultipleUriStrategy(multipleUriStrategy); + this.locator = new ConfigServicePropertySourceLocator(clientProperties); + ClientHttpRequestFactory requestFactory = Mockito.mock(ClientHttpRequestFactory.class); + mockRequestResponse(requestFactory, badURI, HttpStatus.OK, MediaType.TEXT_HTML); + mockRequestResponse(requestFactory, goodURI, HttpStatus.OK); + this.locator.setRestTemplate(new RestTemplate(requestFactory)); + } + private void mockRequestResponse(ClientHttpRequestFactory requestFactory, String baseURI, HttpStatus status) throws Exception { + mockRequestResponse(requestFactory, baseURI, status, MediaType.APPLICATION_JSON); + } + + private void mockRequestResponse(ClientHttpRequestFactory requestFactory, String baseURI, HttpStatus status, + MediaType contentType) throws Exception { ClientHttpRequest request = Mockito.mock(ClientHttpRequest.class); ClientHttpResponse response = Mockito.mock(ClientHttpResponse.class); @@ -657,7 +679,7 @@ private void mockRequestResponse(ClientHttpRequestFactory requestFactory, String Mockito.when(request.execute()).thenReturn(response); HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); + headers.setContentType(contentType); Mockito.when(response.getHeaders()).thenReturn(headers); Mockito.when(response.getStatusCode()).thenReturn(status); Mockito.when(response.getBody()).thenReturn(new ByteArrayInputStream("{}".getBytes())); diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/MockConfigServerResponses.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/MockConfigServerResponses.java deleted file mode 100644 index 50f8d5c9aa..0000000000 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/MockConfigServerResponses.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2013-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.config.client; - -import java.nio.charset.StandardCharsets; - -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.mock.http.client.MockClientHttpRequest; -import org.springframework.mock.http.client.MockClientHttpResponse; - -/** - * Request factories that answer every config client request with a fixed response. - * - * @author Sharang Gupta - */ -final class MockConfigServerResponses { - - private MockConfigServerResponses() { - } - - /** - * Answers with the HTML login page that a secured config server serves when the - * credentials of the config client are missing or wrong. - * @return the request factory - */ - static ClientHttpRequestFactory loginPage() { - return respondingWith(MediaType.TEXT_HTML, "Please sign in"); - } - - /** - * Answers with a {@code 200 OK} response that has the given content type and body. - * @param contentType the content type of the response - * @param body the body of the response - * @return the request factory - */ - static ClientHttpRequestFactory respondingWith(MediaType contentType, String body) { - MockClientHttpResponse response = new MockClientHttpResponse(body.getBytes(StandardCharsets.UTF_8), - HttpStatus.OK); - response.getHeaders().setContentType(contentType); - MockClientHttpRequest request = new MockClientHttpRequest(); - request.setResponse(response); - return (uri, httpMethod) -> request; - } - -}