Adds gzip response modification handling.

Fixes gh-1492
Fixes gh-1548
This commit is contained in:
Ruslan Akhundov
2020-02-02 23:02:17 +00:00
committed by Spencer Gibb
parent 4bd51d46d2
commit 3d278e93d2
8 changed files with 420 additions and 86 deletions

View File

@@ -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 {

View File

@@ -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);
}
}
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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 {

View File

@@ -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();
}
}
}

View File

@@ -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");
}

View File

@@ -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();
}