diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java index 434916de5f46..b95a0f91cc84 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java @@ -116,6 +116,8 @@ final class DefaultRestClient implements RestClient { private final List defaultStatusHandlers; + private final boolean defaultStatusHandlerEnabled; + private final DefaultRestClientBuilder builder; private final List> messageConverters; @@ -134,7 +136,7 @@ final class DefaultRestClient implements RestClient { @Nullable MultiValueMap defaultCookies, @Nullable Object defaultApiVersion, @Nullable ApiVersionInserter apiVersionInserter, @Nullable Consumer> defaultRequest, - @Nullable List statusHandlers, + @Nullable List statusHandlers, boolean defaultStatusHandlerEnabled, List> messageConverters, ObservationRegistry observationRegistry, @Nullable ClientRequestObservationConvention observationConvention, @@ -151,6 +153,7 @@ final class DefaultRestClient implements RestClient { this.apiVersionInserter = apiVersionInserter; this.defaultRequest = defaultRequest; this.defaultStatusHandlers = (statusHandlers != null ? new ArrayList<>(statusHandlers) : new ArrayList<>()); + this.defaultStatusHandlerEnabled = defaultStatusHandlerEnabled; this.messageConverters = messageConverters; this.observationRegistry = observationRegistry; this.observationConvention = observationConvention; @@ -785,7 +788,9 @@ private class DefaultResponseSpec implements ResponseSpec { DefaultResponseSpec(RequestHeadersSpec requestHeadersSpec) { this.requestHeadersSpec = requestHeadersSpec; this.statusHandlers.addAll(DefaultRestClient.this.defaultStatusHandlers); - this.statusHandlers.add(StatusHandler.createDefaultStatusHandler(DefaultRestClient.this.messageConverters)); + if (DefaultRestClient.this.defaultStatusHandlerEnabled) { + this.statusHandlers.add(StatusHandler.createDefaultStatusHandler(DefaultRestClient.this.messageConverters)); + } this.defaultStatusHandlerCount = this.statusHandlers.size(); } diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java index 9df03025e8bd..c69922cc5bcd 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java @@ -100,6 +100,8 @@ final class DefaultRestClientBuilder implements RestClient.Builder { private @Nullable List statusHandlers; + private boolean defaultStatusHandlerEnabled = true; + private @Nullable List interceptors; private @Nullable BiPredicate bufferingPredicate; @@ -138,6 +140,7 @@ public DefaultRestClientBuilder(DefaultRestClientBuilder other) { this.apiVersionInserter = other.apiVersionInserter; this.defaultRequest = other.defaultRequest; this.statusHandlers = (other.statusHandlers != null ? new ArrayList<>(other.statusHandlers) : null); + this.defaultStatusHandlerEnabled = other.defaultStatusHandlerEnabled; this.interceptors = (other.interceptors != null) ? new ArrayList<>(other.interceptors) : null; this.bufferingPredicate = other.bufferingPredicate; this.initializers = (other.initializers != null) ? new ArrayList<>(other.initializers) : null; @@ -301,6 +304,12 @@ public RestClient.Builder defaultStatusHandler(ResponseErrorHandler errorHandler return defaultStatusHandlerInternal(StatusHandler.fromErrorHandler(errorHandler)); } + @Override + public RestClient.Builder disableDefaultStatusHandler() { + this.defaultStatusHandlerEnabled = false; + return this; + } + private RestClient.Builder defaultStatusHandlerInternal(StatusHandler statusHandler) { if (this.statusHandlers == null) { this.statusHandlers = new ArrayList<>(); @@ -436,7 +445,7 @@ public RestClient build() { requestFactory, this.interceptors, this.bufferingPredicate, this.initializers, uriBuilderFactory, defaultHeaders, defaultCookies, this.defaultApiVersion, this.apiVersionInserter, this.defaultRequest, - this.statusHandlers, converters, + this.statusHandlers, this.defaultStatusHandlerEnabled, converters, this.observationRegistry, this.observationConvention, new DefaultRestClientBuilder(this)); } diff --git a/spring-web/src/main/java/org/springframework/web/client/NoOpResponseErrorHandler.java b/spring-web/src/main/java/org/springframework/web/client/NoOpResponseErrorHandler.java index 3864923d33e3..363818f37456 100644 --- a/spring-web/src/main/java/org/springframework/web/client/NoOpResponseErrorHandler.java +++ b/spring-web/src/main/java/org/springframework/web/client/NoOpResponseErrorHandler.java @@ -29,6 +29,8 @@ *

This implementation is not suitable with the {@link RestClient} as it uses * a list of candidates where the first matching is invoked. If you want to * disable default status handlers with the {@code RestClient}, consider + * disabling the built-in default status handler via + * {@link RestClient.Builder#disableDefaultStatusHandler()} or * registering a noop {@link ResponseSpec.ErrorHandler ErrorHandler} with a * predicate that matches all status code, see * {@link RestClient.Builder#defaultStatusHandler(Predicate, ErrorHandler)}. diff --git a/spring-web/src/main/java/org/springframework/web/client/RestClient.java b/spring-web/src/main/java/org/springframework/web/client/RestClient.java index facd6111c0fd..0289ebe1f6d7 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestClient.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestClient.java @@ -386,12 +386,25 @@ Builder defaultStatusHandler(Predicate statusPredicate, * error is invoked. If you want to disable other defaults, consider * using {@link #defaultStatusHandler(Predicate, ResponseSpec.ErrorHandler)} * with a predicate that matches all status codes. + *

To disable the built-in default status handler entirely, use + * {@link #disableDefaultStatusHandler()}. * @param errorHandler handler that typically, though not necessarily, * throws an exception * @return this builder */ Builder defaultStatusHandler(ResponseErrorHandler errorHandler); + /** + * Disable the default status handler that maps error status + * codes (4xx/5xx) to {@link RestClientException} variants. + *

By default, the default status handler is enabled. Disabling it allows + * full control over status handling via custom handlers or per-response + * {@code onStatus} registrations. + * @return this builder + * @since 7.0.4 + */ + Builder disableDefaultStatusHandler(); + /** * Add the given request interceptor to the end of the interceptor chain. * @param interceptor the interceptor to be added to the chain diff --git a/spring-web/src/test/java/org/springframework/web/client/DefaultRestClientTests.java b/spring-web/src/test/java/org/springframework/web/client/DefaultRestClientTests.java index 45847f586f9e..9421a22b8d0a 100644 --- a/spring-web/src/test/java/org/springframework/web/client/DefaultRestClientTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/DefaultRestClientTests.java @@ -34,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -117,6 +118,38 @@ void requiredBodyWithParameterizedTypeReferenceAndNullBody() throws IOException ); } + @Test + void defaultStatusHandlerThrowsOnErrorStatus() throws IOException { + mockSentRequest(HttpMethod.GET, "https://example.org"); + mockResponseStatus(HttpStatus.BAD_REQUEST); + mockResponseBody("Error", MediaType.TEXT_PLAIN); + + assertThatThrownBy(() -> this.client.get() + .uri("https://example.org") + .retrieve() + .body(String.class)) + .isInstanceOf(HttpClientErrorException.class); + } + + @Test + void disableDefaultStatusHandlerAllowsErrorBody() throws IOException { + this.client = RestClient.builder() + .requestFactory(this.requestFactory) + .disableDefaultStatusHandler() + .build(); + + mockSentRequest(HttpMethod.GET, "https://example.org"); + mockResponseStatus(HttpStatus.BAD_REQUEST); + mockResponseBody("Error", MediaType.TEXT_PLAIN); + + String result = this.client.get() + .uri("https://example.org") + .retrieve() + .body(String.class); + + assertThat(result).isEqualTo("Error"); + } + private void mockSentRequest(HttpMethod method, String uri) throws IOException { given(this.requestFactory.createRequest(URI.create(uri), method)).willReturn(this.request); diff --git a/spring-web/src/test/java/org/springframework/web/client/RestClientBuilderTests.java b/spring-web/src/test/java/org/springframework/web/client/RestClientBuilderTests.java index 7dda5d1a7463..aa3075691669 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestClientBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestClientBuilderTests.java @@ -274,6 +274,15 @@ void buildCopiesDefaultCookiesImmutable() { ); } + @Test + void disableDefaultStatusHandler() { + RestClient restClient = RestClient.builder() + .disableDefaultStatusHandler() + .build(); + + assertThat(fieldValue("defaultStatusHandlerEnabled", restClient)).isEqualTo(false); + } + private static @Nullable Object fieldValue(String name, DefaultRestClientBuilder instance) { try { Field field = DefaultRestClientBuilder.class.getDeclaredField(name);