Adds gzip response modification handling.
Fixes gh-1492 Fixes gh-1548
This commit is contained in:
committed by
Spencer Gibb
parent
4bd51d46d2
commit
3d278e93d2
@@ -18,6 +18,7 @@ package org.springframework.cloud.gateway.config;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.netflix.hystrix.HystrixObservableCommand;
|
||||
import io.netty.channel.ChannelOption;
|
||||
@@ -92,6 +93,9 @@ import org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayF
|
||||
import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.rewrite.GzipMessageBodyResolver;
|
||||
import org.springframework.cloud.gateway.filter.factory.rewrite.MessageBodyDecoder;
|
||||
import org.springframework.cloud.gateway.filter.factory.rewrite.MessageBodyEncoder;
|
||||
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter;
|
||||
@@ -441,8 +445,10 @@ public class GatewayAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public ModifyResponseBodyGatewayFilterFactory modifyResponseBodyGatewayFilterFactory(
|
||||
ServerCodecConfigurer codecConfigurer) {
|
||||
return new ModifyResponseBodyGatewayFilterFactory(codecConfigurer);
|
||||
ServerCodecConfigurer codecConfigurer, Set<MessageBodyDecoder> bodyDecoders,
|
||||
Set<MessageBodyEncoder> bodyEncoders) {
|
||||
return new ModifyResponseBodyGatewayFilterFactory(codecConfigurer, bodyDecoders,
|
||||
bodyEncoders);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -560,6 +566,11 @@ public class GatewayAutoConfiguration {
|
||||
return new RequestHeaderSizeGatewayFilterFactory();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GzipMessageBodyResolver gzipMessageBodyResolver() {
|
||||
return new GzipMessageBodyResolver();
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(HttpClient.class)
|
||||
protected static class NettyConfiguration {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2013-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.gateway.filter.factory.rewrite;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
public class GzipMessageBodyResolver implements MessageBodyDecoder, MessageBodyEncoder {
|
||||
|
||||
@Override
|
||||
public String encodingType() {
|
||||
return "gzip";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decode(byte[] encoded) {
|
||||
try {
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
|
||||
GZIPInputStream gis = new GZIPInputStream(bis);
|
||||
return FileCopyUtils.copyToByteArray(gis);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalStateException("couldn't decode body from gzip", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encode(DataBuffer original) {
|
||||
try {
|
||||
ByteArrayOutputStream bis = new ByteArrayOutputStream();
|
||||
GZIPOutputStream gos = new GZIPOutputStream(bis);
|
||||
FileCopyUtils.copy(original.asInputStream(), gos);
|
||||
return bis.toByteArray();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalStateException("couldn't encode body to gzip", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2013-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.gateway.filter.factory.rewrite;
|
||||
|
||||
/**
|
||||
* Decoder that is used to decode message body in case it's encoding from Content-Encoding
|
||||
* header matches encoding returned by {@code encodingType()} call.
|
||||
*/
|
||||
public interface MessageBodyDecoder {
|
||||
|
||||
byte[] decode(byte[] encoded);
|
||||
|
||||
String encodingType();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2013-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.gateway.filter.factory.rewrite;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
|
||||
/**
|
||||
* Encoder that is used to encode message body in case it's encoding from Content-Encoding
|
||||
* header matches encoding returned by {@code encodingType()} call.
|
||||
*/
|
||||
public interface MessageBodyEncoder {
|
||||
|
||||
byte[] encode(DataBuffer original);
|
||||
|
||||
String encodingType();
|
||||
|
||||
}
|
||||
@@ -16,11 +16,16 @@
|
||||
|
||||
package org.springframework.cloud.gateway.filter.factory.rewrite;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
@@ -30,6 +35,8 @@ import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.support.BodyInserterContext;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
@@ -44,6 +51,7 @@ import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static java.util.function.Function.identity;
|
||||
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR;
|
||||
|
||||
@@ -56,15 +64,35 @@ public class ModifyResponseBodyGatewayFilterFactory extends
|
||||
@Nullable
|
||||
private final ServerCodecConfigurer codecConfigurer;
|
||||
|
||||
private final Map<String, MessageBodyDecoder> messageBodyDecoders;
|
||||
|
||||
private final Map<String, MessageBodyEncoder> messageBodyEncoders;
|
||||
|
||||
@Deprecated
|
||||
public ModifyResponseBodyGatewayFilterFactory() {
|
||||
super(Config.class);
|
||||
this.codecConfigurer = null;
|
||||
messageBodyDecoders = Collections.emptyMap();
|
||||
messageBodyEncoders = Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public ModifyResponseBodyGatewayFilterFactory(ServerCodecConfigurer codecConfigurer) {
|
||||
super(Config.class);
|
||||
this.codecConfigurer = codecConfigurer;
|
||||
messageBodyDecoders = Collections.emptyMap();
|
||||
messageBodyEncoders = Collections.emptyMap();
|
||||
}
|
||||
|
||||
public ModifyResponseBodyGatewayFilterFactory(ServerCodecConfigurer codecConfigurer,
|
||||
Set<MessageBodyDecoder> messageBodyDecoders,
|
||||
Set<MessageBodyEncoder> messageBodyEncoders) {
|
||||
super(Config.class);
|
||||
this.codecConfigurer = codecConfigurer;
|
||||
this.messageBodyDecoders = messageBodyDecoders.stream()
|
||||
.collect(Collectors.toMap(MessageBodyDecoder::encodingType, identity()));
|
||||
this.messageBodyEncoders = messageBodyEncoders.stream()
|
||||
.collect(Collectors.toMap(MessageBodyEncoder::encodingType, identity()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -175,79 +203,14 @@ public class ModifyResponseBodyGatewayFilterFactory extends
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
return chain.filter(exchange.mutate().response(decorate(exchange)).build());
|
||||
return chain.filter(exchange.mutate()
|
||||
.response(new ModifiedServerHttpResponse(exchange, config)).build());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Deprecated
|
||||
ServerHttpResponse decorate(ServerWebExchange exchange) {
|
||||
return new ServerHttpResponseDecorator(exchange.getResponse()) {
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
|
||||
|
||||
Class inClass = config.getInClass();
|
||||
Class outClass = config.getOutClass();
|
||||
|
||||
String originalResponseContentType = exchange
|
||||
.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
// explicitly add it in this way instead of
|
||||
// 'httpHeaders.setContentType(originalResponseContentType)'
|
||||
// this will prevent exception in case of using non-standard media
|
||||
// types like "Content-Type: image"
|
||||
httpHeaders.add(HttpHeaders.CONTENT_TYPE,
|
||||
originalResponseContentType);
|
||||
|
||||
ClientResponse clientResponse = prepareClientResponse(body,
|
||||
httpHeaders);
|
||||
|
||||
// TODO: flux or mono
|
||||
Mono modifiedBody = clientResponse.bodyToMono(inClass)
|
||||
.flatMap(originalBody -> config.getRewriteFunction()
|
||||
.apply(exchange, originalBody))
|
||||
.switchIfEmpty(Mono.defer(() -> (Mono) config
|
||||
.getRewriteFunction().apply(exchange, null)));
|
||||
|
||||
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody,
|
||||
outClass);
|
||||
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(
|
||||
exchange, exchange.getResponse().getHeaders());
|
||||
return bodyInserter.insert(outputMessage, new BodyInserterContext())
|
||||
.then(Mono.defer(() -> {
|
||||
Flux<DataBuffer> messageBody = outputMessage.getBody();
|
||||
HttpHeaders headers = getDelegate().getHeaders();
|
||||
if (!headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
|
||||
messageBody = messageBody.doOnNext(data -> headers
|
||||
.setContentLength(data.readableByteCount()));
|
||||
}
|
||||
// TODO: fail if isStreamingMediaType?
|
||||
return getDelegate().writeWith(messageBody);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeAndFlushWith(
|
||||
Publisher<? extends Publisher<? extends DataBuffer>> body) {
|
||||
return writeWith(Flux.from(body).flatMapSequential(p -> p));
|
||||
}
|
||||
|
||||
private ClientResponse prepareClientResponse(
|
||||
Publisher<? extends DataBuffer> body, HttpHeaders httpHeaders) {
|
||||
ClientResponse.Builder builder;
|
||||
if (codecConfigurer != null) {
|
||||
builder = ClientResponse.create(
|
||||
exchange.getResponse().getStatusCode(),
|
||||
codecConfigurer.getReaders());
|
||||
}
|
||||
else {
|
||||
builder = ClientResponse
|
||||
.create(exchange.getResponse().getStatusCode());
|
||||
}
|
||||
return builder.headers(headers -> headers.putAll(httpHeaders))
|
||||
.body(Flux.from(body)).build();
|
||||
}
|
||||
|
||||
};
|
||||
return new ModifiedServerHttpResponse(exchange, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -271,6 +234,132 @@ public class ModifyResponseBodyGatewayFilterFactory extends
|
||||
|
||||
}
|
||||
|
||||
protected class ModifiedServerHttpResponse extends ServerHttpResponseDecorator {
|
||||
|
||||
private final ServerWebExchange exchange;
|
||||
|
||||
private final Config config;
|
||||
|
||||
public ModifiedServerHttpResponse(ServerWebExchange exchange, Config config) {
|
||||
super(exchange.getResponse());
|
||||
this.exchange = exchange;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
|
||||
|
||||
Class inClass = config.getInClass();
|
||||
Class outClass = config.getOutClass();
|
||||
|
||||
String originalResponseContentType = exchange
|
||||
.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
// explicitly add it in this way instead of
|
||||
// 'httpHeaders.setContentType(originalResponseContentType)'
|
||||
// this will prevent exception in case of using non-standard media
|
||||
// types like "Content-Type: image"
|
||||
httpHeaders.add(HttpHeaders.CONTENT_TYPE, originalResponseContentType);
|
||||
|
||||
ClientResponse clientResponse = prepareClientResponse(body, httpHeaders);
|
||||
|
||||
// TODO: flux or mono
|
||||
Mono modifiedBody = extractBody(exchange, clientResponse, inClass)
|
||||
.flatMap(originalBody -> config.getRewriteFunction().apply(exchange,
|
||||
originalBody))
|
||||
.switchIfEmpty(Mono.defer(() -> (Mono) config.getRewriteFunction()
|
||||
.apply(exchange, null)));
|
||||
|
||||
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody,
|
||||
outClass);
|
||||
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange,
|
||||
exchange.getResponse().getHeaders());
|
||||
return bodyInserter.insert(outputMessage, new BodyInserterContext())
|
||||
.then(Mono.defer(() -> {
|
||||
Mono<DataBuffer> messageBody = writeBody(getDelegate(),
|
||||
outputMessage, outClass);
|
||||
HttpHeaders headers = getDelegate().getHeaders();
|
||||
if (!headers.containsKey(HttpHeaders.TRANSFER_ENCODING)
|
||||
|| headers.containsKey(HttpHeaders.CONTENT_LENGTH)) {
|
||||
messageBody = messageBody.doOnNext(data -> headers
|
||||
.setContentLength(data.readableByteCount()));
|
||||
}
|
||||
// TODO: fail if isStreamingMediaType?
|
||||
return getDelegate().writeWith(messageBody);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeAndFlushWith(
|
||||
Publisher<? extends Publisher<? extends DataBuffer>> body) {
|
||||
return writeWith(Flux.from(body).flatMapSequential(p -> p));
|
||||
}
|
||||
|
||||
private ClientResponse prepareClientResponse(Publisher<? extends DataBuffer> body,
|
||||
HttpHeaders httpHeaders) {
|
||||
ClientResponse.Builder builder;
|
||||
if (codecConfigurer != null) {
|
||||
builder = ClientResponse.create(exchange.getResponse().getStatusCode(),
|
||||
codecConfigurer.getReaders());
|
||||
}
|
||||
else {
|
||||
builder = ClientResponse.create(exchange.getResponse().getStatusCode());
|
||||
}
|
||||
return builder.headers(headers -> headers.putAll(httpHeaders))
|
||||
.body(Flux.from(body)).build();
|
||||
}
|
||||
|
||||
private <T> Mono<T> extractBody(ServerWebExchange exchange,
|
||||
ClientResponse clientResponse, Class<T> inClass) {
|
||||
// if inClass is byte[] then just return body, otherwise check if
|
||||
// decoding required
|
||||
if (byte[].class.isAssignableFrom(inClass)) {
|
||||
return clientResponse.bodyToMono(inClass);
|
||||
}
|
||||
|
||||
List<String> encodingHeaders = exchange.getResponse().getHeaders()
|
||||
.getOrEmpty(HttpHeaders.CONTENT_ENCODING);
|
||||
for (String encoding : encodingHeaders) {
|
||||
MessageBodyDecoder decoder = messageBodyDecoders.get(encoding);
|
||||
if (decoder != null) {
|
||||
return clientResponse.bodyToMono(byte[].class)
|
||||
.publishOn(Schedulers.parallel()).map(decoder::decode)
|
||||
.map(bytes -> exchange.getResponse().bufferFactory()
|
||||
.wrap(bytes))
|
||||
.map(buffer -> prepareClientResponse(Mono.just(buffer),
|
||||
exchange.getResponse().getHeaders()))
|
||||
.flatMap(response -> response.bodyToMono(inClass));
|
||||
}
|
||||
}
|
||||
|
||||
return clientResponse.bodyToMono(inClass);
|
||||
}
|
||||
|
||||
private Mono<DataBuffer> writeBody(ServerHttpResponse httpResponse,
|
||||
CachedBodyOutputMessage message, Class<?> outClass) {
|
||||
Mono<DataBuffer> response = DataBufferUtils.join(message.getBody());
|
||||
if (byte[].class.isAssignableFrom(outClass)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
List<String> encodingHeaders = httpResponse.getHeaders()
|
||||
.getOrEmpty(HttpHeaders.CONTENT_ENCODING);
|
||||
for (String encoding : encodingHeaders) {
|
||||
MessageBodyEncoder encoder = messageBodyEncoders.get(encoding);
|
||||
if (encoder != null) {
|
||||
DataBufferFactory dataBufferFactory = httpResponse.bufferFactory();
|
||||
response = response.publishOn(Schedulers.parallel())
|
||||
.map(encoder::encode).map(dataBufferFactory::wrap);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@SuppressWarnings("unchecked")
|
||||
public class ResponseAdapter implements ClientHttpResponse {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.gateway.filter.factory.rewrite;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.gateway.route.RouteLocator;
|
||||
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
|
||||
import org.springframework.cloud.gateway.test.BaseWebClientTests;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT)
|
||||
@DirtiesContext
|
||||
public class ModifyResponseBodyGatewayFilterFactoryGzipTests extends BaseWebClientTests {
|
||||
|
||||
@Test
|
||||
public void testModificationOfResponseBody() {
|
||||
URI uri = UriComponentsBuilder.fromUriString(this.baseUri + "/gzip").build(true)
|
||||
.toUri();
|
||||
|
||||
testClient.get().uri(uri).header("Host", "www.modifyresponsebodyjava.org")
|
||||
.accept(MediaType.APPLICATION_JSON).exchange().expectBody()
|
||||
.json("{\"length\":25,\"value\":\"\\\"httpbin compatible home\\\"\"}");
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@SpringBootConfiguration
|
||||
@Import(DefaultTestConfig.class)
|
||||
public static class TestConfig {
|
||||
|
||||
@Value("${test.uri}")
|
||||
String uri;
|
||||
|
||||
@Bean
|
||||
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
|
||||
return builder.routes().route("modify_response_java_test_gzip",
|
||||
r -> r.path("/gzip").and().host("www.modifyresponsebodyjava.org")
|
||||
.filters(f -> f.modifyResponseBody(String.class, Map.class,
|
||||
(webExchange, originalResponse) -> {
|
||||
Map<String, Object> modifiedResponse = new HashMap<>();
|
||||
modifiedResponse.put("value", originalResponse);
|
||||
modifiedResponse.put("length",
|
||||
originalResponse.length());
|
||||
return Mono.just(modifiedResponse);
|
||||
}))
|
||||
.uri(uri))
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory.Config;
|
||||
import org.springframework.http.codec.support.DefaultServerCodecConfigurer;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class ModifyResponseBodyGatewayFilterFactoryUnitTests {
|
||||
@@ -33,7 +34,7 @@ public class ModifyResponseBodyGatewayFilterFactoryUnitTests {
|
||||
config.setOutClass(Integer.class);
|
||||
config.setNewContentType("mycontenttype");
|
||||
GatewayFilter filter = new ModifyResponseBodyGatewayFilterFactory(
|
||||
new DefaultServerCodecConfigurer()).apply(config);
|
||||
new DefaultServerCodecConfigurer(), emptySet(), emptySet()).apply(config);
|
||||
assertThat(filter.toString()).contains("String").contains("Integer")
|
||||
.contains("mycontenttype");
|
||||
}
|
||||
|
||||
@@ -16,22 +16,32 @@
|
||||
|
||||
package org.springframework.cloud.gateway.test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.codec.multipart.FilePart;
|
||||
import org.springframework.http.codec.multipart.Part;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -46,22 +56,23 @@ public class HttpBinCompatibleController {
|
||||
|
||||
private static final Log log = LogFactory.getLog(HttpBinCompatibleController.class);
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
@RequestMapping("/")
|
||||
public String home() {
|
||||
return "httpbin compatible home";
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/headers", method = { RequestMethod.GET, RequestMethod.POST },
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@RequestMapping(path = "/headers", method = { RequestMethod.GET,
|
||||
RequestMethod.POST }, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Map<String, Object> headers(ServerWebExchange exchange) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("headers", getHeaders(exchange));
|
||||
return result;
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/multivalueheaders",
|
||||
method = { RequestMethod.GET, RequestMethod.POST },
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@RequestMapping(path = "/multivalueheaders", method = { RequestMethod.GET,
|
||||
RequestMethod.POST }, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Map<String, Object> multiValueHeaders(ServerWebExchange exchange) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("headers", exchange.getRequest().getHeaders());
|
||||
@@ -75,8 +86,7 @@ public class HttpBinCompatibleController {
|
||||
return Mono.just(get(exchange)).delayElement(Duration.ofSeconds(delay));
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/anything/{anything}",
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@RequestMapping(path = "/anything/{anything}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Map<String, Object> anything(ServerWebExchange exchange,
|
||||
@PathVariable(required = false) String anything) {
|
||||
return get(exchange);
|
||||
@@ -97,8 +107,7 @@ public class HttpBinCompatibleController {
|
||||
return result;
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/post", consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@RequestMapping(value = "/post", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Mono<Map<String, Object>> postFormData(
|
||||
@RequestBody Mono<MultiValueMap<String, Part>> parts) {
|
||||
// StringDecoder decoder = StringDecoder.allMimeTypes(true);
|
||||
@@ -114,16 +123,13 @@ public class HttpBinCompatibleController {
|
||||
}).map(files -> Collections.singletonMap("files", files));
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/post",
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@RequestMapping(path = "/post", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Mono<Map<String, Object>> postUrlEncoded(ServerWebExchange exchange)
|
||||
throws IOException {
|
||||
return post(exchange, null);
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/post", method = RequestMethod.POST,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@RequestMapping(path = "/post", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Mono<Map<String, Object>> post(ServerWebExchange exchange,
|
||||
@RequestBody(required = false) String body) throws IOException {
|
||||
HashMap<String, Object> ret = new HashMap<>();
|
||||
@@ -153,6 +159,29 @@ public class HttpBinCompatibleController {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/gzip", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Mono<Void> gzip(ServerWebExchange exchange) throws IOException {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("httpbin /gzip");
|
||||
}
|
||||
|
||||
String jsonResponse = OBJECT_MAPPER.writeValueAsString("httpbin compatible home");
|
||||
byte[] bytes = jsonResponse.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
response.getHeaders().add(HttpHeaders.CONTENT_ENCODING, "gzip");
|
||||
DataBufferFactory dataBufferFactory = response.bufferFactory();
|
||||
response.setStatusCode(HttpStatus.OK);
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
GZIPOutputStream is = new GZIPOutputStream(bos);
|
||||
FileCopyUtils.copy(bytes, is);
|
||||
|
||||
byte[] gzippedResponse = bos.toByteArray();
|
||||
DataBuffer wrap = dataBufferFactory.wrap(gzippedResponse);
|
||||
return response.writeWith(Flux.just(wrap));
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders(ServerWebExchange exchange) {
|
||||
return exchange.getRequest().getHeaders().toSingleValueMap();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user