Skip to content

Commit 693ec82

Browse files
committed
Introduce RestClient.ResponseSpec#requiredBody
Closes spring-projectsgh-36173
1 parent 9b95482 commit 693ec82

3 files changed

Lines changed: 193 additions & 2 deletions

File tree

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -813,19 +813,31 @@ private ResponseSpec onStatusInternal(StatusHandler statusHandler) {
813813
}
814814

815815
@Override
816-
@SuppressWarnings("NullAway") // See https://github.com/uber/NullAway/issues/1290
817816
public <T> @Nullable T body(Class<T> bodyType) {
818817
return executeAndExtract((request, response) -> readBody(request, response, bodyType, bodyType, this.hints));
819818
}
820819

821820
@Override
822-
@SuppressWarnings("NullAway") // See https://github.com/uber/NullAway/issues/1290
821+
public <T> T requiredBody(Class<T> bodyType) {
822+
T body = body(bodyType);
823+
Assert.state(body != null, "The body must not be null");
824+
return body;
825+
}
826+
827+
@Override
823828
public <T> @Nullable T body(ParameterizedTypeReference<T> bodyType) {
824829
Type type = bodyType.getType();
825830
Class<T> bodyClass = bodyClass(type);
826831
return executeAndExtract((request, response) -> readBody(request, response, type, bodyClass, this.hints));
827832
}
828833

834+
@Override
835+
public <T> T requiredBody(ParameterizedTypeReference<T> bodyType) {
836+
T body = body(bodyType);
837+
Assert.state(body != null, "The body must not be null");
838+
return body;
839+
}
840+
829841
@Override
830842
public <T> ResponseEntity<T> toEntity(Class<T> bodyType) {
831843
return toEntityInternal(bodyType, bodyType);

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,9 +1027,24 @@ ResponseSpec onStatus(Predicate<HttpStatusCode> statusPredicate,
10271027
* response with a status code of 4xx or 5xx. Use
10281028
* {@link #onStatus(Predicate, ErrorHandler)} to customize error response
10291029
* handling.
1030+
* @see #requiredBody(Class)
10301031
*/
10311032
<T> @Nullable T body(Class<T> bodyType);
10321033

1034+
/**
1035+
* Extract the body as an object of the given type.
1036+
* @param bodyType the type of return value
1037+
* @param <T> the body type
1038+
* @return the body
1039+
* @throws IllegalStateException if no response body was available
1040+
* @throws RestClientResponseException by default when receiving a
1041+
* response with a status code of 4xx or 5xx. Use
1042+
* {@link #onStatus(Predicate, ErrorHandler)} to customize error response
1043+
* handling.
1044+
* @since 7.0.4
1045+
*/
1046+
<T> T requiredBody(Class<T> bodyType);
1047+
10331048
/**
10341049
* Extract the body as an object of the given type.
10351050
* @param bodyType the type of return value
@@ -1039,9 +1054,24 @@ ResponseSpec onStatus(Predicate<HttpStatusCode> statusPredicate,
10391054
* response with a status code of 4xx or 5xx. Use
10401055
* {@link #onStatus(Predicate, ErrorHandler)} to customize error response
10411056
* handling.
1057+
* @see #requiredBody(ParameterizedTypeReference)
10421058
*/
10431059
<T> @Nullable T body(ParameterizedTypeReference<T> bodyType);
10441060

1061+
/**
1062+
* Extract the body as an object of the given type.
1063+
* @param bodyType the type of return value
1064+
* @param <T> the body type
1065+
* @return the body
1066+
* @throws IllegalStateException if no response body was available
1067+
* @throws RestClientResponseException by default when receiving a
1068+
* response with a status code of 4xx or 5xx. Use
1069+
* {@link #onStatus(Predicate, ErrorHandler)} to customize error response
1070+
* handling.
1071+
* @since 7.0.4
1072+
*/
1073+
<T> T requiredBody(ParameterizedTypeReference<T> bodyType);
1074+
10451075
/**
10461076
* Return a {@code ResponseEntity} with the body decoded to an Object of
10471077
* the given type.
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.client;
18+
19+
import java.io.ByteArrayInputStream;
20+
import java.io.IOException;
21+
import java.net.URI;
22+
23+
import org.junit.jupiter.api.BeforeEach;
24+
import org.junit.jupiter.api.Test;
25+
26+
import org.springframework.core.ParameterizedTypeReference;
27+
import org.springframework.http.HttpHeaders;
28+
import org.springframework.http.HttpMethod;
29+
import org.springframework.http.HttpStatus;
30+
import org.springframework.http.MediaType;
31+
import org.springframework.http.client.ClientHttpRequest;
32+
import org.springframework.http.client.ClientHttpRequestFactory;
33+
import org.springframework.http.client.ClientHttpResponse;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
37+
import static org.mockito.BDDMockito.given;
38+
import static org.mockito.Mockito.mock;
39+
40+
/**
41+
* Tests for {@link DefaultRestClient}.
42+
*
43+
* @author Sebastien Deleuze
44+
*/
45+
class DefaultRestClientTests {
46+
47+
private final ClientHttpRequestFactory requestFactory = mock();
48+
49+
private final ClientHttpRequest request = mock();
50+
51+
private final ClientHttpResponse response = mock();
52+
53+
private RestClient client;
54+
55+
56+
@BeforeEach
57+
void setup() {
58+
this.client = RestClient.builder()
59+
.requestFactory(this.requestFactory)
60+
.build();
61+
}
62+
63+
64+
@Test
65+
void requiredBodyWithClass() throws IOException {
66+
mockSentRequest(HttpMethod.GET, "https://example.org");
67+
mockResponseStatus(HttpStatus.OK);
68+
mockResponseBody("Hello World", MediaType.TEXT_PLAIN);
69+
70+
String result = this.client.get()
71+
.uri("https://example.org")
72+
.retrieve()
73+
.requiredBody(String.class);
74+
75+
assertThat(result).isEqualTo("Hello World");
76+
}
77+
78+
@Test
79+
void requiredBodyWithClassAndNullBody() throws IOException {
80+
mockSentRequest(HttpMethod.GET, "https://example.org");
81+
mockResponseStatus(HttpStatus.OK);
82+
mockEmptyResponseBody();
83+
84+
assertThatIllegalStateException().isThrownBy(() ->
85+
this.client.get()
86+
.uri("https://example.org")
87+
.retrieve()
88+
.requiredBody(String.class)
89+
);
90+
}
91+
92+
@Test
93+
void requiredBodyWithParameterizedTypeReference() throws IOException {
94+
mockSentRequest(HttpMethod.GET, "https://example.org");
95+
mockResponseStatus(HttpStatus.OK);
96+
mockResponseBody("Hello World", MediaType.TEXT_PLAIN);
97+
98+
String result = this.client.get()
99+
.uri("https://example.org")
100+
.retrieve()
101+
.requiredBody(new ParameterizedTypeReference<>() {});
102+
103+
assertThat(result).isEqualTo("Hello World");
104+
}
105+
106+
@Test
107+
void requiredBodyWithParameterizedTypeReferenceAndNullBody() throws IOException {
108+
mockSentRequest(HttpMethod.GET, "https://example.org");
109+
mockResponseStatus(HttpStatus.OK);
110+
mockEmptyResponseBody();
111+
112+
assertThatIllegalStateException().isThrownBy(() ->
113+
this.client.get()
114+
.uri("https://example.org")
115+
.retrieve()
116+
.requiredBody(new ParameterizedTypeReference<String>() {})
117+
);
118+
}
119+
120+
121+
private void mockSentRequest(HttpMethod method, String uri) throws IOException {
122+
given(this.requestFactory.createRequest(URI.create(uri), method)).willReturn(this.request);
123+
given(this.request.getHeaders()).willReturn(new HttpHeaders());
124+
given(this.request.getMethod()).willReturn(method);
125+
given(this.request.getURI()).willReturn(URI.create(uri));
126+
}
127+
128+
private void mockResponseStatus(HttpStatus responseStatus) throws IOException {
129+
given(this.request.execute()).willReturn(this.response);
130+
given(this.response.getStatusCode()).willReturn(responseStatus);
131+
given(this.response.getStatusText()).willReturn(responseStatus.getReasonPhrase());
132+
}
133+
134+
private void mockResponseBody(String expectedBody, MediaType mediaType) throws IOException {
135+
HttpHeaders responseHeaders = new HttpHeaders();
136+
responseHeaders.setContentType(mediaType);
137+
responseHeaders.setContentLength(expectedBody.length());
138+
given(this.response.getHeaders()).willReturn(responseHeaders);
139+
given(this.response.getBody()).willReturn(new ByteArrayInputStream(expectedBody.getBytes()));
140+
}
141+
142+
private void mockEmptyResponseBody() throws IOException {
143+
HttpHeaders responseHeaders = new HttpHeaders();
144+
responseHeaders.setContentLength(0);
145+
given(this.response.getHeaders()).willReturn(responseHeaders);
146+
given(this.response.getBody()).willReturn(new ByteArrayInputStream(new byte[0]));
147+
}
148+
149+
}

0 commit comments

Comments
 (0)