Skip to content

Commit e6401b2

Browse files
committed
Access to request and response byte[] in WebTestClient
The WiretapConnector now decorated the ClientHttpRequest & Response in order to intercept and save the actual content written and read. The saved content is now incorporated in the diagnostic output but may be used for other purposes as well (e.g. REST Docs). Diagnostic information about an exchange has also been refactored similar to command line output from curl.
1 parent 71b021c commit e6401b2

File tree

10 files changed

+318
-125
lines changed

10 files changed

+318
-125
lines changed

spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class DefaultWebTestClient implements WebTestClient {
6161

6262
private final WebClient webClient;
6363

64-
private final WebTestClientConnector webTestClientConnector;
64+
private final WiretapConnector wiretapConnector;
6565

6666
private final Duration timeout;
6767

@@ -71,15 +71,15 @@ class DefaultWebTestClient implements WebTestClient {
7171
DefaultWebTestClient(WebClient.Builder webClientBuilder, ClientHttpConnector connector, Duration timeout) {
7272
Assert.notNull(webClientBuilder, "WebClient.Builder is required");
7373

74-
this.webTestClientConnector = new WebTestClientConnector(connector);
75-
this.webClient = webClientBuilder.clientConnector(this.webTestClientConnector).build();
74+
this.wiretapConnector = new WiretapConnector(connector);
75+
this.webClient = webClientBuilder.clientConnector(this.wiretapConnector).build();
7676
this.timeout = (timeout != null ? timeout : Duration.ofSeconds(5));
7777
}
7878

7979
private DefaultWebTestClient(DefaultWebTestClient webTestClient, ExchangeFilterFunction filter) {
8080
this.webClient = webTestClient.webClient.filter(filter);
8181
this.timeout = webTestClient.timeout;
82-
this.webTestClientConnector = webTestClient.webTestClientConnector;
82+
this.wiretapConnector = webTestClient.wiretapConnector;
8383
}
8484

8585

@@ -174,7 +174,7 @@ private class DefaultHeaderSpec implements WebTestClient.HeaderSpec {
174174
DefaultHeaderSpec(WebClient.HeaderSpec spec) {
175175
this.headerSpec = spec;
176176
this.requestId = String.valueOf(requestIndex.incrementAndGet());
177-
this.headerSpec.header(WebTestClientConnector.REQUEST_ID_HEADER_NAME, this.requestId);
177+
this.headerSpec.header(WiretapConnector.REQUEST_ID_HEADER_NAME, this.requestId);
178178
}
179179

180180

@@ -254,9 +254,9 @@ public <T, S extends Publisher<T>> ResponseSpec exchange(S publisher, Class<T> e
254254
}
255255

256256
private DefaultResponseSpec toResponseSpec(Mono<ClientResponse> mono) {
257-
ClientResponse response = mono.block(getTimeout());
258-
ClientHttpRequest httpRequest = webTestClientConnector.claimRequest(this.requestId);
259-
return new DefaultResponseSpec(httpRequest, response);
257+
ClientResponse clientResponse = mono.block(getTimeout());
258+
ExchangeResult exchangeResult = wiretapConnector.claimRequest(this.requestId);
259+
return new DefaultResponseSpec(exchangeResult, clientResponse);
260260
}
261261
}
262262

@@ -268,8 +268,8 @@ private class UndecodedExchangeResult extends ExchangeResult {
268268
private final ClientResponse response;
269269

270270

271-
public UndecodedExchangeResult(ClientHttpRequest httpRequest, ClientResponse response) {
272-
super(httpRequest, response);
271+
public UndecodedExchangeResult(ExchangeResult result, ClientResponse response) {
272+
super(result);
273273
this.response = response;
274274
}
275275

@@ -290,7 +290,7 @@ public EntityExchangeResult<List<?>> consumeList(ResolvableType elementType, int
290290

291291
public <T> FluxExchangeResult<T> decodeBody(ResolvableType elementType) {
292292
Flux<T> body = this.response.body(toFlux(elementType));
293-
return new FluxExchangeResult<>(this, body, elementType);
293+
return new FluxExchangeResult<>(this, body);
294294
}
295295

296296
@SuppressWarnings("unchecked")
@@ -313,8 +313,8 @@ private class DefaultResponseSpec implements ResponseSpec {
313313
private final UndecodedExchangeResult result;
314314

315315

316-
public DefaultResponseSpec(ClientHttpRequest httpRequest, ClientResponse response) {
317-
this.result = new UndecodedExchangeResult(httpRequest, response);
316+
public DefaultResponseSpec(ExchangeResult result, ClientResponse response) {
317+
this.result = new UndecodedExchangeResult(result, response);
318318
}
319319

320320
@Override

spring-test/src/main/java/org/springframework/test/web/reactive/server/EntityExchangeResult.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,4 @@ public T getResponseBody() {
4343
return this.body;
4444
}
4545

46-
@Override
47-
protected String formatResponseBody() {
48-
return this.body.toString();
49-
}
50-
5146
}

spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java

Lines changed: 86 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -16,114 +16,123 @@
1616
package org.springframework.test.web.reactive.server;
1717

1818
import java.net.URI;
19+
import java.nio.charset.Charset;
20+
import java.nio.charset.StandardCharsets;
21+
import java.util.Arrays;
22+
import java.util.List;
1923
import java.util.function.Supplier;
2024
import java.util.stream.Collectors;
2125

26+
import reactor.core.publisher.MonoProcessor;
27+
2228
import org.springframework.http.HttpHeaders;
2329
import org.springframework.http.HttpMethod;
2430
import org.springframework.http.HttpStatus;
31+
import org.springframework.http.MediaType;
2532
import org.springframework.http.ResponseCookie;
26-
import org.springframework.http.client.reactive.ClientHttpRequest;
2733
import org.springframework.util.MultiValueMap;
28-
import org.springframework.web.reactive.function.client.ClientResponse;
2934

3035
/**
3136
* Simple container for request and response details from an exchange performed
3237
* through the {@link WebTestClient}.
3338
*
34-
* <p>An {@code ExchangeResult} only exposes the status and the headers from
35-
* the response which is all that's available when a {@link ClientResponse} is
36-
* first created.
39+
* <p>When an {@code ExchangeResult} is first created it has only the status and
40+
* headers of the response available. When the response body is extracted, the
41+
* {@code ExchangeResult} is re-created as either {@link EntityExchangeResult}
42+
* or {@link FluxExchangeResult} that further expose extracted entities.
3743
*
38-
* <p>Sub-types {@link EntityExchangeResult} and {@link FluxExchangeResult}
39-
* further expose the response body either as a fully extracted representation
40-
* or as a {@code Flux} of representations to be consumed.
44+
* <p>Raw request and response content may also be accessed once complete via
45+
* {@link #getRequestContent()} or {@link #getResponseContent()}.
4146
*
4247
* @author Rossen Stoyanchev
4348
* @since 5.0
49+
*
4450
* @see EntityExchangeResult
4551
* @see FluxExchangeResult
4652
*/
4753
public class ExchangeResult {
4854

49-
private final HttpMethod method;
50-
51-
private final URI url;
52-
53-
private final HttpHeaders requestHeaders;
55+
private static final List<MediaType> PRINTABLE_MEDIA_TYPES = Arrays.asList(
56+
MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.parseMediaType("text/*"),
57+
MediaType.APPLICATION_FORM_URLENCODED);
5458

55-
private final HttpStatus status;
5659

57-
private final HttpHeaders responseHeaders;
60+
private final WiretapClientHttpRequest request;
5861

59-
private final MultiValueMap<String, ResponseCookie> responseCookies;
62+
private final WiretapClientHttpResponse response;
6063

6164

6265
/**
63-
* Constructor used when a {@code ClientResponse} is first created.
66+
* Constructor used when the {@code ClientHttpResponse} becomes available.
6467
*/
65-
protected ExchangeResult(ClientHttpRequest request, ClientResponse response) {
66-
this.method = request.getMethod();
67-
this.url = request.getURI();
68-
this.requestHeaders = request.getHeaders();
69-
this.status = response.statusCode();
70-
this.responseHeaders = response.headers().asHttpHeaders();
71-
this.responseCookies = response.cookies();
68+
protected ExchangeResult(WiretapClientHttpRequest request, WiretapClientHttpResponse response) {
69+
this.request = request;
70+
this.response = response;
7271
}
7372

7473
/**
7574
* Copy constructor used when the body is decoded or consumed.
7675
*/
7776
protected ExchangeResult(ExchangeResult other) {
78-
this.method = other.getMethod();
79-
this.url = other.getUrl();
80-
this.requestHeaders = other.getRequestHeaders();
81-
this.status = other.getStatus();
82-
this.responseHeaders = other.getResponseHeaders();
83-
this.responseCookies = other.getResponseCookies();
77+
this.request = other.request;
78+
this.response = other.response;
8479
}
8580

8681

8782
/**
8883
* Return the method of the request.
8984
*/
9085
public HttpMethod getMethod() {
91-
return this.method;
86+
return this.request.getMethod();
9287
}
9388

9489
/**
9590
* Return the request headers that were sent to the server.
9691
*/
9792
public URI getUrl() {
98-
return this.url;
93+
return this.request.getURI();
9994
}
10095

10196
/**
10297
* Return the request headers sent to the server.
10398
*/
10499
public HttpHeaders getRequestHeaders() {
105-
return this.requestHeaders;
100+
return this.request.getHeaders();
101+
}
102+
103+
/**
104+
* Return a "promise" for the raw request body content once completed.
105+
*/
106+
public MonoProcessor<byte[]> getRequestContent() {
107+
return this.request.getBodyContent();
106108
}
107109

108110
/**
109111
* Return the status of the executed request.
110112
*/
111113
public HttpStatus getStatus() {
112-
return this.status;
114+
return this.response.getStatusCode();
113115
}
114116

115117
/**
116118
* Return the response headers received from the server.
117119
*/
118120
public HttpHeaders getResponseHeaders() {
119-
return this.responseHeaders;
121+
return this.response.getHeaders();
120122
}
121123

122124
/**
123125
* Return response cookies received from the server.
124126
*/
125127
public MultiValueMap<String, ResponseCookie> getResponseCookies() {
126-
return this.responseCookies;
128+
return this.getResponseCookies();
129+
}
130+
131+
/**
132+
* Return a "promise" for the raw response body content once completed.
133+
*/
134+
public MonoProcessor<byte[]> getResponseContent() {
135+
return this.response.getBodyContent();
127136
}
128137

129138

@@ -156,39 +165,60 @@ public <T> T assertWithDiagnosticsAndReturn(Supplier<T> assertion) {
156165

157166
@Override
158167
public String toString() {
159-
return "\n\n" +
160-
formatValue("Request", this.method + " " + getUrl()) +
161-
formatValue("Status", this.status + " " + getStatusReason()) +
162-
formatHeading("Response Headers") + formatHeaders(this.responseHeaders) +
163-
formatHeading("Request Headers") + formatHeaders(this.requestHeaders) +
168+
return "\n" +
169+
"> " + getMethod() + " " + getUrl() + "\n" +
170+
"> " + formatHeaders(getRequestHeaders()) + "\n" +
171+
"\n" +
172+
formatContent(getRequestHeaders().getContentType(), getRequestContent()) + "\n" +
164173
"\n" +
165-
formatValue("Response Body", formatResponseBody());
174+
"> " + getStatus() + " " + getStatusReason() + "\n" +
175+
"> " + formatHeaders(getResponseHeaders()) + "\n" +
176+
"\n" +
177+
formatContent(getResponseHeaders().getContentType(), getResponseContent()) + "\n\n";
166178
}
167179

168180
private String getStatusReason() {
169181
String reason = "";
170-
if (this.status != null && this.status.getReasonPhrase() != null) {
171-
reason = this.status.getReasonPhrase();
182+
if (getStatus() != null && getStatus().getReasonPhrase() != null) {
183+
reason = getStatus().getReasonPhrase();
172184
}
173185
return reason;
174186
}
175187

176-
private String formatHeading(String heading) {
177-
return "\n" + String.format("%s", heading) + "\n";
178-
}
179-
180-
private String formatValue(String label, Object value) {
181-
return String.format("%18s: %s", label, value) + "\n";
182-
}
183-
184188
private String formatHeaders(HttpHeaders headers) {
185189
return headers.entrySet().stream()
186-
.map(entry -> formatValue(entry.getKey(), entry.getValue()))
187-
.collect(Collectors.joining());
190+
.map(entry -> entry.getKey() + ": " + entry.getValue())
191+
.collect(Collectors.joining("\n> "));
188192
}
189193

190-
protected String formatResponseBody() {
191-
return "Not read yet";
194+
private String formatContent(MediaType contentType, MonoProcessor<byte[]> body) {
195+
if (body.isSuccess()) {
196+
byte[] bytes = body.blockMillis(0);
197+
if (bytes.length == 0) {
198+
return "No content";
199+
}
200+
201+
if (contentType == null) {
202+
return "Unknown content type (" + bytes.length + " bytes)";
203+
}
204+
205+
Charset charset = contentType.getCharset();
206+
if (charset != null) {
207+
return new String(bytes, charset);
208+
}
209+
210+
if (PRINTABLE_MEDIA_TYPES.stream().anyMatch(contentType::isCompatibleWith)) {
211+
return new String(bytes, StandardCharsets.UTF_8);
212+
}
213+
214+
return "Unknown charset (" + bytes.length + " bytes)";
215+
}
216+
else if (body.isError()) {
217+
return "I/O failure: " + body.getError().getMessage();
218+
}
219+
else {
220+
return "Content not available yet";
221+
}
192222
}
193223

194224
}

spring-test/src/main/java/org/springframework/test/web/reactive/server/FluxExchangeResult.java

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717

1818
import reactor.core.publisher.Flux;
1919

20-
import org.springframework.core.ResolvableType;
21-
2220
/**
2321
* {@code ExchangeResult} variant with the response body as a {@code Flux<T>}.
2422
*
@@ -32,13 +30,10 @@ public class FluxExchangeResult<T> extends ExchangeResult {
3230

3331
private final Flux<T> body;
3432

35-
private final ResolvableType elementType;
36-
3733

38-
FluxExchangeResult(ExchangeResult result, Flux<T> body, ResolvableType elementType) {
34+
FluxExchangeResult(ExchangeResult result, Flux<T> body) {
3935
super(result);
4036
this.body = body;
41-
this.elementType = elementType;
4237
}
4338

4439

@@ -49,9 +44,4 @@ public Flux<T> getResponseBody() {
4944
return this.body;
5045
}
5146

52-
@Override
53-
protected String formatResponseBody() {
54-
return "Flux<" + this.elementType.toString() + ">";
55-
}
56-
5747
}

0 commit comments

Comments
 (0)