Skip to content

Commit a54297b

Browse files
committed
Puts client response input stream in a request attribute.
Fixes gh-3405
1 parent 58c8441 commit a54297b

File tree

4 files changed

+64
-2
lines changed

4 files changed

+64
-2
lines changed

spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ public abstract class MvcUtils {
5757
*/
5858
public static final String CACHED_REQUEST_BODY_ATTR = qualify("cachedRequestBody");
5959

60+
/**
61+
* Client response input stream key.
62+
*/
63+
public static final String CLIENT_RESPONSE_INPUT_STREAM_ATTR = qualify("cachedClientResponseBody");
64+
6065
/**
6166
* CircuitBreaker execution exception attribute name.
6267
*/

spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/ClientHttpRequestFactoryProxyExchange.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
package org.springframework.cloud.gateway.server.mvc.handler;
1818

1919
import java.io.IOException;
20+
import java.io.InputStream;
2021
import java.io.UncheckedIOException;
2122

23+
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
2224
import org.springframework.http.client.ClientHttpRequest;
2325
import org.springframework.http.client.ClientHttpRequestFactory;
2426
import org.springframework.http.client.ClientHttpResponse;
@@ -41,10 +43,18 @@ public ServerResponse exchange(Request request) {
4143
// copy body from request to clientHttpRequest
4244
StreamUtils.copy(request.getServerRequest().servletRequest().getInputStream(), clientHttpRequest.getBody());
4345
ClientHttpResponse clientHttpResponse = clientHttpRequest.execute();
46+
InputStream body = clientHttpResponse.getBody();
47+
// put the body input stream in a request attribute so filters can read it.
48+
MvcUtils.putAttribute(request.getServerRequest(), MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR, body);
4449
ServerResponse serverResponse = GatewayServerResponse.status(clientHttpResponse.getStatusCode())
4550
.build((req, httpServletResponse) -> {
4651
try (clientHttpResponse) {
47-
StreamUtils.copy(clientHttpResponse.getBody(), httpServletResponse.getOutputStream());
52+
// get input stream from request attribute in case it was
53+
// modified.
54+
InputStream inputStream = MvcUtils.getAttribute(request.getServerRequest(),
55+
MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR);
56+
// copy body from request to clientHttpRequest
57+
StreamUtils.copy(inputStream, httpServletResponse.getOutputStream());
4858
}
4959
return null;
5060
});

spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/RestClientProxyExchange.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
package org.springframework.cloud.gateway.server.mvc.handler;
1818

1919
import java.io.IOException;
20+
import java.io.InputStream;
2021
import java.io.OutputStream;
2122

23+
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
2224
import org.springframework.http.client.ClientHttpResponse;
2325
import org.springframework.util.StreamUtils;
2426
import org.springframework.web.client.RestClient;
@@ -45,11 +47,18 @@ private static int copyBody(Request request, OutputStream outputStream) throws I
4547
}
4648

4749
private static ServerResponse doExchange(Request request, ClientHttpResponse clientResponse) throws IOException {
50+
InputStream body = clientResponse.getBody();
51+
// put the body input stream in a request attribute so filters can read it.
52+
MvcUtils.putAttribute(request.getServerRequest(), MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR, body);
4853
ServerResponse serverResponse = GatewayServerResponse.status(clientResponse.getStatusCode())
4954
.build((req, httpServletResponse) -> {
5055
try (clientResponse) {
56+
// get input stream from request attribute in case it was
57+
// modified.
58+
InputStream inputStream = MvcUtils.getAttribute(request.getServerRequest(),
59+
MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR);
5160
// copy body from request to clientHttpRequest
52-
StreamUtils.copy(clientResponse.getBody(), httpServletResponse.getOutputStream());
61+
StreamUtils.copy(inputStream, httpServletResponse.getOutputStream());
5362
}
5463
return null;
5564
});

spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package org.springframework.cloud.gateway.server.mvc;
1818

19+
import java.io.ByteArrayInputStream;
1920
import java.io.IOException;
21+
import java.io.InputStream;
2022
import java.net.URI;
2123
import java.nio.charset.StandardCharsets;
2224
import java.time.Duration;
@@ -75,6 +77,7 @@
7577
import org.springframework.test.context.ContextConfiguration;
7678
import org.springframework.util.LinkedMultiValueMap;
7779
import org.springframework.util.MultiValueMap;
80+
import org.springframework.util.StreamUtils;
7881
import org.springframework.web.bind.annotation.GetMapping;
7982
import org.springframework.web.bind.annotation.PostMapping;
8083
import org.springframework.web.bind.annotation.RequestBody;
@@ -696,6 +699,15 @@ public void queryParamWithSpecialCharactersWorks() {
696699
});
697700
}
698701

702+
@Test
703+
public void clientResponseBodyAttributeWorks() {
704+
restClient.get().uri("/anything/readresponsebody").header("X-Foo", "fooval").exchange().expectStatus().isOk()
705+
.expectBody(Map.class).consumeWith(res -> {
706+
Map<String, Object> headers = getMap(res.getResponseBody(), "headers");
707+
assertThat(headers).containsEntry("X-Foo", "FOOVAL");
708+
});
709+
}
710+
699711
@SpringBootConfiguration
700712
@EnableAutoConfiguration
701713
@LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class)
@@ -1289,6 +1301,32 @@ public RouterFunction<ServerResponse> gatewayRouterFunctionsQuery() {
12891301
// @formatter:on
12901302
}
12911303

1304+
@Bean
1305+
public RouterFunction<ServerResponse> gatewayRouterFunctionsReadResponseBody() {
1306+
// @formatter:off
1307+
return route("testClientResponseBodyAttribute")
1308+
.GET("/anything/readresponsebody", http())
1309+
.before(new HttpbinUriResolver())
1310+
.after((request, response) -> {
1311+
Object o = request.attributes().get(MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR);
1312+
if (o instanceof InputStream) {
1313+
try {
1314+
byte[] bytes = StreamUtils.copyToByteArray((InputStream) o);
1315+
String s = new String(bytes, StandardCharsets.UTF_8);
1316+
String replace = s.replace("fooval", "FOOVAL");
1317+
ByteArrayInputStream bais = new ByteArrayInputStream(replace.getBytes());
1318+
request.attributes().put(MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTR, bais);
1319+
}
1320+
catch (IOException e) {
1321+
throw new RuntimeException(e);
1322+
}
1323+
}
1324+
return response;
1325+
})
1326+
.build();
1327+
// @formatter:on
1328+
}
1329+
12921330
@Bean
12931331
public FilterRegistrationBean myFilter() {
12941332
FilterRegistrationBean<MyFilter> reg = new FilterRegistrationBean<>(new MyFilter());

0 commit comments

Comments
 (0)