Skip to content

Commit 671d972

Browse files
committed
Add RestClient.RequestHeadersSpec#exchangeForRequiredValue
This commit adds a variant to RestClient.RequestHeadersSpec#exchange suitable for functions returning non-null values. Closes gh-34692
1 parent d9047d3 commit 671d972

File tree

3 files changed

+117
-1
lines changed

3 files changed

+117
-1
lines changed

spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java

+7
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,13 @@ public <T> T exchange(ExchangeFunction<T> exchangeFunction, boolean close) {
533533
return exchangeInternal(exchangeFunction, close);
534534
}
535535

536+
@Override
537+
public <T> T exchangeForRequiredValue(RequiredValueExchangeFunction<T> exchangeFunction, boolean close) {
538+
T value = exchangeInternal(exchangeFunction, close);
539+
Assert.state(value != null, "The exchanged value must not be null");
540+
return value;
541+
}
542+
536543
@Nullable
537544
private <T> T exchangeInternal(ExchangeFunction<T> exchangeFunction, boolean close) {
538545
Assert.notNull(exchangeFunction, "ExchangeFunction must not be null");

spring-web/src/main/java/org/springframework/web/client/RestClient.java

+75
Original file line numberDiff line numberDiff line change
@@ -671,12 +671,41 @@ interface RequestHeadersSpec<S extends RequestHeadersSpec<S>> {
671671
* @param exchangeFunction the function to handle the response with
672672
* @param <T> the type the response will be transformed to
673673
* @return the value returned from the exchange function, potentially {@code null}
674+
* @see RequestHeadersSpec#exchangeForRequiredValue(RequiredValueExchangeFunction)
674675
*/
675676
@Nullable
676677
default <T> T exchange(ExchangeFunction<T> exchangeFunction) {
677678
return exchange(exchangeFunction, true);
678679
}
679680

681+
/**
682+
* Exchange the {@link ClientHttpResponse} for a value of type {@code T}.
683+
* This can be useful for advanced scenarios, for example to decode the
684+
* response differently depending on the response status:
685+
* <pre class="code">
686+
* Person person = client.get()
687+
* .uri("/people/1")
688+
* .accept(MediaType.APPLICATION_JSON)
689+
* .exchange((request, response) -&gt; {
690+
* if (response.getStatusCode().equals(HttpStatus.OK)) {
691+
* return deserialize(response.getBody());
692+
* }
693+
* else {
694+
* throw new BusinessException();
695+
* }
696+
* });
697+
* </pre>
698+
* <p><strong>Note:</strong> The response is
699+
* {@linkplain ClientHttpResponse#close() closed} after the exchange
700+
* function has been invoked.
701+
* @param exchangeFunction the function to handle the response with
702+
* @param <T> the type the response will be transformed to
703+
* @return the value returned from the exchange function, never {@code null}
704+
*/
705+
default <T> T exchangeForRequiredValue(RequiredValueExchangeFunction<T> exchangeFunction) {
706+
return exchangeForRequiredValue(exchangeFunction, true);
707+
}
708+
680709
/**
681710
* Exchange the {@link ClientHttpResponse} for a value of type {@code T}.
682711
* This can be useful for advanced scenarios, for example to decode the
@@ -703,10 +732,40 @@ default <T> T exchange(ExchangeFunction<T> exchangeFunction) {
703732
* {@code exchangeFunction} is invoked, {@code false} to keep it open
704733
* @param <T> the type the response will be transformed to
705734
* @return the value returned from the exchange function, potentially {@code null}
735+
* @see RequestHeadersSpec#exchangeForRequiredValue(RequiredValueExchangeFunction, boolean)
706736
*/
707737
@Nullable
708738
<T> T exchange(ExchangeFunction<T> exchangeFunction, boolean close);
709739

740+
/**
741+
* Exchange the {@link ClientHttpResponse} for a value of type {@code T}.
742+
* This can be useful for advanced scenarios, for example to decode the
743+
* response differently depending on the response status:
744+
* <pre class="code">
745+
* Person person = client.get()
746+
* .uri("/people/1")
747+
* .accept(MediaType.APPLICATION_JSON)
748+
* .exchange((request, response) -&gt; {
749+
* if (response.getStatusCode().equals(HttpStatus.OK)) {
750+
* return deserialize(response.getBody());
751+
* }
752+
* else {
753+
* throw new BusinessException();
754+
* }
755+
* });
756+
* </pre>
757+
* <p><strong>Note:</strong> If {@code close} is {@code true},
758+
* then the response is {@linkplain ClientHttpResponse#close() closed}
759+
* after the exchange function has been invoked. When set to
760+
* {@code false}, the caller is responsible for closing the response.
761+
* @param exchangeFunction the function to handle the response with
762+
* @param close {@code true} to close the response after
763+
* {@code exchangeFunction} is invoked, {@code false} to keep it open
764+
* @param <T> the type the response will be transformed to
765+
* @return the value returned from the exchange function, never {@code null}
766+
*/
767+
<T> T exchangeForRequiredValue(RequiredValueExchangeFunction<T> exchangeFunction, boolean close);
768+
710769

711770
/**
712771
* Defines the contract for {@link #exchange(ExchangeFunction)}.
@@ -726,6 +785,22 @@ interface ExchangeFunction<T> {
726785
T exchange(HttpRequest clientRequest, ConvertibleClientHttpResponse clientResponse) throws IOException;
727786
}
728787

788+
/**
789+
* Variant of {@link ExchangeFunction} returning a non-null required value.
790+
* @param <T> the type the response will be transformed to
791+
*/
792+
@FunctionalInterface
793+
interface RequiredValueExchangeFunction<T> extends ExchangeFunction<T> {
794+
795+
/**
796+
* Exchange the given response into a value of type {@code T}.
797+
* @param clientRequest the request
798+
* @param clientResponse the response
799+
* @return the exchanged value, never {@code null}
800+
* @throws IOException in case of I/O errors
801+
*/
802+
T exchange(HttpRequest clientRequest, ConvertibleClientHttpResponse clientResponse) throws IOException;
803+
}
729804

730805
/**
731806
* Extension of {@link ClientHttpResponse} that can convert the body.

spring-web/src/test/java/org/springframework/web/client/RestClientIntegrationTests.java

+35-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -58,6 +58,7 @@
5858
import static java.nio.charset.StandardCharsets.UTF_8;
5959
import static org.assertj.core.api.Assertions.assertThat;
6060
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
61+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
6162
import static org.junit.jupiter.api.Assumptions.assumeFalse;
6263
import static org.junit.jupiter.params.provider.Arguments.argumentSet;
6364

@@ -766,6 +767,39 @@ void exchangeFor404(ClientHttpRequestFactory requestFactory) {
766767
expectRequest(request -> assertThat(request.getPath()).isEqualTo("/greeting"));
767768
}
768769

770+
@ParameterizedRestClientTest
771+
void exchangeForRequiredValue(ClientHttpRequestFactory requestFactory) {
772+
startServer(requestFactory);
773+
774+
prepareResponse(response -> response.setBody("Hello Spring!"));
775+
776+
String result = this.restClient.get()
777+
.uri("/greeting")
778+
.header("X-Test-Header", "testvalue")
779+
.exchangeForRequiredValue((request, response) -> new String(RestClientUtils.getBody(response), UTF_8));
780+
781+
assertThat(result).isEqualTo("Hello Spring!");
782+
783+
expectRequestCount(1);
784+
expectRequest(request -> {
785+
assertThat(request.getHeader("X-Test-Header")).isEqualTo("testvalue");
786+
assertThat(request.getPath()).isEqualTo("/greeting");
787+
});
788+
}
789+
790+
@ParameterizedRestClientTest
791+
@SuppressWarnings("DataFlowIssue")
792+
void exchangeForNullRequiredValue(ClientHttpRequestFactory requestFactory) {
793+
startServer(requestFactory);
794+
795+
prepareResponse(response -> response.setBody("Hello Spring!"));
796+
797+
assertThatIllegalStateException().isThrownBy(() -> this.restClient.get()
798+
.uri("/greeting")
799+
.header("X-Test-Header", "testvalue")
800+
.exchangeForRequiredValue((request, response) -> null));
801+
}
802+
769803
@ParameterizedRestClientTest
770804
void requestInitializer(ClientHttpRequestFactory requestFactory) {
771805
startServer(requestFactory);

0 commit comments

Comments
 (0)