From 73eea9d8c09a410cf35ff4563298d91df6695b3a Mon Sep 17 00:00:00 2001
From: Dave Syer
Date: Fri, 30 Jun 2017 14:24:46 +0100
Subject: [PATCH] Migrate MVC module over from spring-cloud-function
---
pom.xml | 1 +
spring-cloud-gateway-dependencies/pom.xml | 5 +
spring-cloud-gateway-mvc/.jdk8 | 0
spring-cloud-gateway-mvc/pom.xml | 33 +
.../cloud/function/gateway/ProxyExchange.java | 644 ++++++++++++++++++
.../config/ProxyExchangeArgumentResolver.java | 83 +++
.../gateway/config/ProxyProperties.java | 70 ++
.../ProxyResponseAutoConfiguration.java | 89 +++
.../main/resources/META-INF/spring.factories | 5 +
.../gateway/ProductionConfigurationTests.java | 445 ++++++++++++
.../src/test/resources/static/test.html | 4 +
11 files changed, 1379 insertions(+)
create mode 100644 spring-cloud-gateway-mvc/.jdk8
create mode 100644 spring-cloud-gateway-mvc/pom.xml
create mode 100644 spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/ProxyExchange.java
create mode 100644 spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyExchangeArgumentResolver.java
create mode 100644 spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyProperties.java
create mode 100644 spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyResponseAutoConfiguration.java
create mode 100644 spring-cloud-gateway-mvc/src/main/resources/META-INF/spring.factories
create mode 100644 spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/function/web/gateway/ProductionConfigurationTests.java
create mode 100644 spring-cloud-gateway-mvc/src/test/resources/static/test.html
diff --git a/pom.xml b/pom.xml
index 6fe0389ce..816ba3a4c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -140,6 +140,7 @@
spring-cloud-gateway-dependencies
+ spring-cloud-gateway-mvc
spring-cloud-gateway-core
spring-cloud-starter-gateway
spring-cloud-gateway-sample
diff --git a/spring-cloud-gateway-dependencies/pom.xml b/spring-cloud-gateway-dependencies/pom.xml
index 0b5de9081..3cad857e4 100644
--- a/spring-cloud-gateway-dependencies/pom.xml
+++ b/spring-cloud-gateway-dependencies/pom.xml
@@ -21,6 +21,11 @@
+
+ org.springframework.cloud
+ spring-cloud-gateway-mvc
+ ${project.version}
+
org.springframework.cloud
spring-cloud-gateway-core
diff --git a/spring-cloud-gateway-mvc/.jdk8 b/spring-cloud-gateway-mvc/.jdk8
new file mode 100644
index 000000000..e69de29bb
diff --git a/spring-cloud-gateway-mvc/pom.xml b/spring-cloud-gateway-mvc/pom.xml
new file mode 100644
index 000000000..35a74ed2c
--- /dev/null
+++ b/spring-cloud-gateway-mvc/pom.xml
@@ -0,0 +1,33 @@
+
+
+ 4.0.0
+
+ spring-cloud-gateway-mvc
+ jar
+ spring-cloud-gateway-mvc
+ Spring Cloud Gateway MVC
+
+
+ org.springframework.cloud
+ spring-cloud-function-parent
+ 1.0.0.BUILD-SNAPSHOT
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
diff --git a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/ProxyExchange.java b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/ProxyExchange.java
new file mode 100644
index 000000000..a8764f7e9
--- /dev/null
+++ b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/ProxyExchange.java
@@ -0,0 +1,644 @@
+/*
+ * Copyright 2016-2017 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
+ *
+ * http://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.function.gateway;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.Vector;
+import java.util.function.Function;
+
+import javax.servlet.ReadListener;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+import org.springframework.core.Conventions;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.RequestEntity.BodyBuilder;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+import org.springframework.util.ClassUtils;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.HttpMediaTypeNotAcceptableException;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.support.WebDataBinderFactory;
+import org.springframework.web.client.RequestCallback;
+import org.springframework.web.client.ResponseExtractor;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.context.request.ServletWebRequest;
+import org.springframework.web.context.request.WebRequest;
+import org.springframework.web.method.support.ModelAndViewContainer;
+import org.springframework.web.servlet.HandlerMapping;
+import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
+import org.springframework.web.util.AbstractUriTemplateHandler;
+
+/**
+ * A @RequestMapping
argument type that can proxy the request to a backend.
+ * Spring will inject one of these into your MVC handler method, and you get return a
+ * ResponseEntity
that you get from one of the HTTP methods {@link #get()},
+ * {@link #post()}, {@link #put()}, {@link #patch()}, {@link #delete()} etc. Example:
+ *
+ *
+ * @GetMapping("/proxy/{id}")
+ * public ResponseEntity<?> proxy(@PathVariable Integer id, ProxyExchange<?> proxy)
+ * throws Exception {
+ * return proxy.uri("http://localhost:9000/foos/" + id).get();
+ * }
+ *
+ *
+ *
+ * By default the incoming request body and headers are sent intact to the downstream
+ * service (with the exception of "sensitive" headers). To manipulate the downstream
+ * request there are "builder" style methods in {@link ProxyExchange}, but only the
+ * {@link #uri(String)} is mandatory. You can change the sensitive headers by calling the
+ * {@link #sensitive(String...)} method (Authorization and Cookie are sensitive by
+ * default).
+ *
+ *
+ * The type parameter T
in ProxyExchange<T>
is the type of
+ * the response body, so it comes out in the {@link ResponseEntity} that you return from
+ * your @RequestMapping
. If you don't care about the type of the request and
+ * response body (e.g. if it's just a passthru) then use a wildcard, or
+ * byte[]
or Object
. Use a concrete type if you want to
+ * transform or manipulate the response, or if you want to assert that it is convertible
+ * to the type you declare.
+ *
+ *
+ * To manipulate the response use the overloaded HTTP methods with a Function
+ * argument and pass in code to transform the response. E.g.
+ *
+ *
+ * @PostMapping("/proxy")
+ * public ResponseEntity<Foo> proxy(ProxyExchange<Foo> proxy) throws Exception {
+ * return proxy.uri("http://localhost:9000/foos/") //
+ * .post(response -> ResponseEntity.status(response.getStatusCode()) //
+ * .headers(response.getHeaders()) //
+ * .header("X-Custom", "MyCustomHeader") //
+ * .body(response.getBody()) //
+ * );
+ * }
+ *
+ *
+ *
+ *
+ *
+ * The full machinery of Spring {@link HttpMessageConverter message converters} is applied
+ * to the incoming request and response and also to the backend request. If you need
+ * additional converters then they need to be added upstream in the MVC configuration and
+ * also to the {@link RestTemplate} that is used in the backend calls (see the
+ * {@link ProxyExchange#ProxyExchange(RestTemplate, NativeWebRequest, ModelAndViewContainer, WebDataBinderFactory, Type)
+ * constructor} for details).
+ *
+ *
+ * As well as the HTTP methods for a backend call you can also use
+ * {@link #forward(String)} for a local in-container dispatch.
+ *
+ *
+ * @author Dave Syer
+ *
+ */
+public class ProxyExchange {
+
+ public static Set DEFAULT_SENSITIVE = new HashSet<>(
+ Arrays.asList("cookie", "authorization"));
+
+ private URI uri;
+
+ private NestedTemplate rest;
+
+ private Object body;
+
+ private RequestResponseBodyMethodProcessor delegate;
+
+ private NativeWebRequest webRequest;
+
+ private ModelAndViewContainer mavContainer;
+
+ private WebDataBinderFactory binderFactory;
+
+ private Set sensitive;
+
+ private HttpHeaders headers = new HttpHeaders();
+
+ private Type responseType;
+
+ public ProxyExchange(RestTemplate rest, NativeWebRequest webRequest,
+ ModelAndViewContainer mavContainer, WebDataBinderFactory binderFactory,
+ Type type) {
+ this.responseType = type;
+ this.rest = createTemplate(rest);
+ this.webRequest = webRequest;
+ this.mavContainer = mavContainer;
+ this.binderFactory = binderFactory;
+ this.delegate = new RequestResponseBodyMethodProcessor(
+ rest.getMessageConverters());
+ }
+
+ /**
+ * Sets the body for the downstream request (if using {@link #post()}, {@link #put()}
+ * or {@link #patch()}). The body can be omitted if you just want to pass the incoming
+ * request downstream without changing it. If you want to transform the incoming
+ * request you can declare it as a @RequestBody
in your
+ * @RequestMapping
in the usual Spring MVC way.
+ *
+ * @param body the request body to send downstream
+ * @return this for convenience
+ */
+ public ProxyExchange body(Object body) {
+ this.body = body;
+ return this;
+ }
+
+ /**
+ * Sets a header for the downstream call.
+ *
+ * @param name
+ * @param value
+ * @return this for convenience
+ */
+ public ProxyExchange header(String name, String... value) {
+ this.headers.put(name, Arrays.asList(value));
+ return this;
+ }
+
+ /**
+ * Additional headers, or overrides of the incoming ones, to be used in the downstream
+ * call.
+ *
+ * @param headers the http headers to use in the downstream call
+ * @return this for convenience
+ */
+ public ProxyExchange headers(HttpHeaders headers) {
+ this.headers.putAll(headers);
+ return this;
+ }
+
+ /**
+ * Sets the names of sensitive headers that are not passed downstream to the backend
+ * service.
+ *
+ * @param names the names of sensitive headers
+ * @return this for convenience
+ */
+ public ProxyExchange sensitive(String... names) {
+ if (this.sensitive == null) {
+ this.sensitive = new HashSet<>();
+ }
+ for (String name : names) {
+ this.sensitive.add(name.toLowerCase());
+ }
+ return this;
+ }
+
+ /**
+ * Sets the uri for the backend call when triggered by the HTTP methods.
+ *
+ * @param uri the backend uri to send the request to
+ * @return this for convenience
+ */
+ public ProxyExchange uri(String uri) {
+ try {
+ this.uri = new URI(uri);
+ }
+ catch (URISyntaxException e) {
+ throw new IllegalStateException("Cannot create URI", e);
+ }
+ return this;
+ }
+
+ public String path() {
+ return (String) this.webRequest.getAttribute(
+ HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE,
+ WebRequest.SCOPE_REQUEST);
+ }
+
+ public String path(String prefix) {
+ String path = path();
+ if (!path.startsWith(prefix)) {
+ throw new IllegalArgumentException(
+ "Path does not start with prefix (" + prefix + "): " + path);
+ }
+ return path.substring(prefix.length());
+ }
+
+ public void forward(String path) {
+ HttpServletRequest request = this.webRequest
+ .getNativeRequest(HttpServletRequest.class);
+ HttpServletResponse response = this.webRequest
+ .getNativeResponse(HttpServletResponse.class);
+ try {
+ request.getRequestDispatcher(path).forward(
+ new BodyForwardingHttpServletRequest(request, response), response);
+ }
+ catch (Exception e) {
+ throw new IllegalStateException("Cannot forward request", e);
+ }
+ }
+
+ public ResponseEntity get() {
+ RequestEntity> requestEntity = headers((BodyBuilder) RequestEntity.get(uri))
+ .build();
+ return exchange(requestEntity);
+ }
+
+ public ResponseEntity get(
+ Function, ResponseEntity> converter) {
+ return converter.apply(get());
+ }
+
+ public ResponseEntity head() {
+ RequestEntity> requestEntity = headers((BodyBuilder) RequestEntity.head(uri))
+ .build();
+ return exchange(requestEntity);
+ }
+
+ public ResponseEntity head(
+ Function, ResponseEntity> converter) {
+ return converter.apply(head());
+ }
+
+ public ResponseEntity options() {
+ RequestEntity> requestEntity = headers((BodyBuilder) RequestEntity.options(uri))
+ .build();
+ return exchange(requestEntity);
+ }
+
+ public ResponseEntity options(
+ Function, ResponseEntity> converter) {
+ return converter.apply(options());
+ }
+
+ public ResponseEntity post() {
+ RequestEntity requestEntity = headers(RequestEntity.post(uri))
+ .body(body());
+ return exchange(requestEntity);
+ }
+
+ public ResponseEntity post(
+ Function, ResponseEntity> converter) {
+ return converter.apply(post());
+ }
+
+ public ResponseEntity delete() {
+ RequestEntity requestEntity = headers(
+ (BodyBuilder) RequestEntity.delete(uri)).build();
+ return exchange(requestEntity);
+ }
+
+ public ResponseEntity delete(
+ Function, ResponseEntity> converter) {
+ return converter.apply(delete());
+ }
+
+ public ResponseEntity put() {
+ RequestEntity requestEntity = headers(RequestEntity.put(uri))
+ .body(body());
+ return exchange(requestEntity);
+ }
+
+ public ResponseEntity put(
+ Function, ResponseEntity> converter) {
+ return converter.apply(put());
+ }
+
+ public ResponseEntity patch() {
+ RequestEntity requestEntity = headers(RequestEntity.patch(uri))
+ .body(body());
+ return exchange(requestEntity);
+ }
+
+ public ResponseEntity patch(
+ Function, ResponseEntity> converter) {
+ return converter.apply(patch());
+ }
+
+ private ResponseEntity exchange(RequestEntity> requestEntity) {
+ Type type = this.responseType;
+ if (type instanceof TypeVariable || type instanceof WildcardType) {
+ type = Object.class;
+ }
+ RequestCallback requestCallback = rest.httpEntityCallback((Object) requestEntity,
+ type);
+ ResponseExtractor> responseExtractor = rest
+ .responseEntityExtractor(type);
+ return rest.execute(requestEntity.getUrl(), requestEntity.getMethod(),
+ requestCallback, responseExtractor);
+ }
+
+ private BodyBuilder headers(BodyBuilder builder) {
+ Set sensitive = this.sensitive;
+ if (sensitive == null) {
+ sensitive = DEFAULT_SENSITIVE;
+ }
+ proxy();
+ for (String name : headers.keySet()) {
+ if (sensitive.contains(name.toLowerCase())) {
+ continue;
+ }
+ builder.header(name, headers.get(name).toArray(new String[0]));
+ }
+ return builder;
+ }
+
+ private void proxy() {
+ try {
+ URI uri = new URI(webRequest.getNativeRequest(HttpServletRequest.class)
+ .getRequestURL().toString());
+ appendForwarded(uri);
+ appendXForwarded(uri);
+ }
+ catch (URISyntaxException e) {
+ throw new IllegalStateException("Cannot create URI for request: " + webRequest
+ .getNativeRequest(HttpServletRequest.class).getRequestURL());
+ }
+ }
+
+ private void appendXForwarded(URI uri) {
+ // Append the legacy headers if they were already added upstream
+ String host = headers.getFirst("x-forwarded-host");
+ if (host == null) {
+ return;
+ }
+ host = host + "," + uri.getHost();
+ headers.set("x-forwarded-host", host);
+ String proto = headers.getFirst("x-forwarded-proto");
+ if (proto == null) {
+ return;
+ }
+ proto = proto + "," + uri.getScheme();
+ headers.set("x-forwarded-proto", proto);
+ }
+
+ private void appendForwarded(URI uri) {
+ String forwarded = headers.getFirst("forwarded");
+ if (forwarded != null) {
+ forwarded = forwarded + ",";
+ } else {
+ forwarded = "";
+ }
+ forwarded = forwarded + forwarded(uri);
+ headers.set("forwarded", forwarded);
+ }
+
+ private String forwarded(URI uri) {
+ if ("http".equals(uri.getScheme())) {
+ return "host=" + uri.getHost();
+ }
+ return String.format("host=%s;proto=%s", uri.getHost(), uri.getScheme());
+ }
+
+ private Object body() {
+ if (body != null) {
+ return body;
+ }
+ body = getRequestBody();
+ return body;
+ }
+
+ /**
+ * Search for the request body if it was already deserialized using
+ * @RequestBody
. If it is not found then deserialize it in the same way
+ * that it would have been for a @RequestBody
.
+ *
+ * @return the request body
+ */
+ private Object getRequestBody() {
+ for (String key : mavContainer.getModel().keySet()) {
+ if (key.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
+ BindingResult result = (BindingResult) mavContainer.getModel().get(key);
+ return result.getTarget();
+ }
+ }
+ MethodParameter input = new MethodParameter(
+ ClassUtils.getMethod(BodyGrabber.class, "body", Object.class), 0);
+ try {
+ delegate.resolveArgument(input, mavContainer, webRequest, binderFactory);
+ }
+ catch (Exception e) {
+ throw new IllegalStateException("Cannot resolve body", e);
+ }
+ String name = Conventions.getVariableNameForParameter(input);
+ BindingResult result = (BindingResult) mavContainer.getModel()
+ .get(BindingResult.MODEL_KEY_PREFIX + name);
+ return result.getTarget();
+ }
+
+ private NestedTemplate createTemplate(RestTemplate input) {
+ NestedTemplate rest = new NestedTemplate();
+ rest.setMessageConverters(input.getMessageConverters());
+ rest.setErrorHandler(input.getErrorHandler());
+ rest.setDefaultUriVariables(
+ ((AbstractUriTemplateHandler) input.getUriTemplateHandler())
+ .getDefaultUriVariables());
+ rest.setRequestFactory(input.getRequestFactory());
+ rest.setInterceptors(input.getInterceptors());
+ return rest;
+ }
+
+ /**
+ * A special {@link RestTemplate} that knows about the {@link Type} of its response
+ * body explicitly (rather than through a {@link ParameterizedTypeReference}, which is
+ * the only way to access this feature in a regular template).
+ *
+ */
+ class NestedTemplate extends RestTemplate {
+ @Override
+ protected RequestCallback httpEntityCallback(Object requestBody,
+ Type responseType) {
+ return super.httpEntityCallback(requestBody, responseType);
+ }
+
+ @Override
+ protected ResponseExtractor> responseEntityExtractor(
+ Type responseType) {
+ return super.responseEntityExtractor(responseType);
+ }
+ }
+
+ /**
+ * A servlet request wrapper that can be safely passed downstream to an internal
+ * forward dispatch, caching its body, and making it available in converted form using
+ * Spring message converters.
+ *
+ */
+ class BodyForwardingHttpServletRequest extends HttpServletRequestWrapper {
+ private HttpServletRequest request;
+ private HttpServletResponse response;
+
+ BodyForwardingHttpServletRequest(HttpServletRequest request,
+ HttpServletResponse response) {
+ super(request);
+ this.request = request;
+ this.response = response;
+ }
+
+ private List header(String name) {
+ List list = headers.get(name);
+ return list;
+ }
+
+ @Override
+ public ServletInputStream getInputStream() throws IOException {
+ Object body = body();
+ MethodParameter output = new MethodParameter(
+ ClassUtils.getMethod(BodySender.class, "body"), -1);
+ ServletOutputToInputConverter response = new ServletOutputToInputConverter(
+ this.response);
+ ServletWebRequest webRequest = new ServletWebRequest(this.request, response);
+ try {
+ delegate.handleReturnValue(body, output, mavContainer, webRequest);
+ }
+ catch (HttpMessageNotWritableException
+ | HttpMediaTypeNotAcceptableException e) {
+ throw new IllegalStateException("Cannot convert body", e);
+ }
+ return response.getInputStream();
+ }
+
+ @Override
+ public Enumeration getHeaderNames() {
+ Set names = headers.keySet();
+ if (names.isEmpty()) {
+ return super.getHeaderNames();
+ }
+ Set result = new LinkedHashSet<>(names);
+ result.addAll(Collections.list(super.getHeaderNames()));
+ return new Vector(result).elements();
+ }
+
+ @Override
+ public Enumeration getHeaders(String name) {
+ List list = header(name);
+ if (list != null) {
+ return new Vector(list).elements();
+ }
+ return super.getHeaders(name);
+ }
+
+ @Override
+ public String getHeader(String name) {
+ List list = header(name);
+ if (list != null && !list.isEmpty()) {
+ return list.iterator().next();
+ }
+ return super.getHeader(name);
+ }
+ }
+
+ protected static class BodyGrabber {
+ public Object body(@RequestBody Object body) {
+ return body;
+ }
+ }
+
+ protected static class BodySender {
+ @ResponseBody
+ public Object body() {
+ return null;
+ }
+ }
+
+}
+
+/**
+ * Convenience class that converts an incoming request input stream into a form that can
+ * be easily deserialized to a Java object using Spring message converters. It is only
+ * used in a local forward dispatch, in which case there is a danger that the request body
+ * will need to be read and analysed more than once. Apart from using the message
+ * converters the other main feature of this class is that the request body is cached and
+ * can be read repeatedly as necessary.
+ *
+ * @author Dave Syer
+ *
+ */
+class ServletOutputToInputConverter extends HttpServletResponseWrapper {
+
+ private StringBuilder builder = new StringBuilder();
+
+ public ServletOutputToInputConverter(HttpServletResponse response) {
+ super(response);
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() throws IOException {
+ return new ServletOutputStream() {
+
+ @Override
+ public void write(int b) throws IOException {
+ builder.append(new Character((char) b));
+ }
+
+ @Override
+ public void setWriteListener(WriteListener listener) {
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+ };
+ }
+
+ public ServletInputStream getInputStream() {
+ ByteArrayInputStream body = new ByteArrayInputStream(
+ builder.toString().getBytes());
+ return new ServletInputStream() {
+
+ @Override
+ public int read() throws IOException {
+ return body.read();
+ }
+
+ @Override
+ public void setReadListener(ReadListener listener) {
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public boolean isFinished() {
+ return body.available() <= 0;
+ }
+ };
+ }
+
+}
diff --git a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyExchangeArgumentResolver.java b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyExchangeArgumentResolver.java
new file mode 100644
index 000000000..bc9e143be
--- /dev/null
+++ b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyExchangeArgumentResolver.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016-2017 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
+ *
+ * http://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.function.gateway.config;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Set;
+
+import org.springframework.cloud.function.gateway.ProxyExchange;
+import org.springframework.core.MethodParameter;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.bind.support.WebDataBinderFactory;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.ModelAndViewContainer;
+
+/**
+ * @author Dave Syer
+ *
+ */
+public class ProxyExchangeArgumentResolver implements HandlerMethodArgumentResolver {
+
+ private RestTemplate rest;
+
+ private HttpHeaders headers;
+
+ private Set sensitive;
+
+ public ProxyExchangeArgumentResolver(RestTemplate builder) {
+ this.rest = builder;
+ }
+
+ public void setHeaders(HttpHeaders headers) {
+ this.headers = headers;
+ }
+
+ public void setSensitive(Set sensitive) {
+ this.sensitive = sensitive;
+ }
+
+ @Override
+ public boolean supportsParameter(MethodParameter parameter) {
+ return ProxyExchange.class.isAssignableFrom(parameter.getParameterType());
+ }
+
+ @Override
+ public Object resolveArgument(MethodParameter parameter,
+ ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
+ WebDataBinderFactory binderFactory) throws Exception {
+ ProxyExchange> proxy = new ProxyExchange<>(rest, webRequest, mavContainer,
+ binderFactory, type(parameter));
+ proxy.headers(headers);
+ if (sensitive != null) {
+ proxy.sensitive(sensitive.toArray(new String[0]));
+ }
+ return proxy;
+ }
+
+ private Type type(MethodParameter parameter) {
+ Type type = parameter.getGenericParameterType();
+ if (type instanceof ParameterizedType) {
+ ParameterizedType param = (ParameterizedType) type;
+ type = param.getActualTypeArguments()[0];
+ }
+ return type;
+ }
+
+}
diff --git a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyProperties.java b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyProperties.java
new file mode 100644
index 000000000..01419f35d
--- /dev/null
+++ b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyProperties.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2016-2017 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
+ *
+ * http://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.function.gateway.config;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.function.gateway.ProxyExchange;
+import org.springframework.http.HttpHeaders;
+
+/**
+ * Configuration properties for the {@link ProxyExchange} argument handler in
+ * @RequestMapping
methods.
+ * @author Dave Syer
+ *
+ */
+@ConfigurationProperties("spring.cloud.gateway.proxy")
+public class ProxyProperties {
+
+ /**
+ * Fixed header values that will be added to all downstream requests.
+ */
+ private Map headers = new LinkedHashMap<>();
+
+ /**
+ * A set of sensitive header names that will not be sent downstream by default.
+ */
+ private Set sensitive = null;
+
+ public Map getHeaders() {
+ return headers;
+ }
+
+ public void setHeaders(Map headers) {
+ this.headers = headers;
+ }
+
+ public Set getSensitive() {
+ return sensitive;
+ }
+
+ public void setSensitive(Set sensitive) {
+ this.sensitive = sensitive;
+ }
+
+ public HttpHeaders convertHeaders() {
+ HttpHeaders headers = new HttpHeaders();
+ for (String key : this.headers.keySet()) {
+ headers.set(key, this.headers.get(key));
+ }
+ return headers;
+ }
+
+}
diff --git a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyResponseAutoConfiguration.java b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyResponseAutoConfiguration.java
new file mode 100644
index 000000000..ddfd07676
--- /dev/null
+++ b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyResponseAutoConfiguration.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016-2017 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
+ *
+ * http://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.function.gateway.config;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.cloud.function.gateway.ProxyExchange;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.http.converter.ByteArrayHttpMessageConverter;
+import org.springframework.web.client.DefaultResponseErrorHandler;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
+
+/**
+ * Autoconfiguration for the {@link ProxyExchange} argument handler in Spring MVC
+ * @RequestMapping
methods.
+ *
+ * @author Dave Syer
+ */
+@Configuration
+@ConditionalOnWebApplication
+@ConditionalOnClass({ HandlerMethodReturnValueHandler.class })
+@EnableConfigurationProperties(ProxyProperties.class)
+public class ProxyResponseAutoConfiguration extends WebMvcConfigurerAdapter {
+
+ @Autowired
+ private ApplicationContext context;
+
+ @Bean
+ @ConditionalOnMissingBean
+ public ProxyExchangeArgumentResolver proxyExchangeArgumentResolver(
+ Optional optional, ProxyProperties proxy) {
+ RestTemplateBuilder builder = optional.orElse(new RestTemplateBuilder());
+ RestTemplate template = builder.build();
+ template.setErrorHandler(new NoOpResponseErrorHandler());
+ template.getMessageConverters().add(new ByteArrayHttpMessageConverter() {
+ @Override
+ public boolean supports(Class> clazz) {
+ return true;
+ }
+ });
+ ProxyExchangeArgumentResolver resolver = new ProxyExchangeArgumentResolver(
+ template);
+ resolver.setHeaders(proxy.convertHeaders());
+ resolver.setSensitive(proxy.getSensitive()); // can be null
+ return resolver;
+ }
+
+ @Override
+ public void addArgumentResolvers(
+ List argumentResolvers) {
+ argumentResolvers.add(context.getBean(ProxyExchangeArgumentResolver.class));
+ }
+
+ private static class NoOpResponseErrorHandler extends DefaultResponseErrorHandler {
+
+ @Override
+ public void handleError(ClientHttpResponse response) throws IOException {
+ }
+
+ }
+}
diff --git a/spring-cloud-gateway-mvc/src/main/resources/META-INF/spring.factories b/spring-cloud-gateway-mvc/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..8a98f9330
--- /dev/null
+++ b/spring-cloud-gateway-mvc/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,5 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.springframework.cloud.function.gateway.config.ProxyResponseAutoConfiguration
+
+org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc=\
+org.springframework.cloud.function.gateway.config.ProxyResponseAutoConfiguration
diff --git a/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/function/web/gateway/ProductionConfigurationTests.java b/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/function/web/gateway/ProductionConfigurationTests.java
new file mode 100644
index 000000000..ec2772920
--- /dev/null
+++ b/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/function/web/gateway/ProductionConfigurationTests.java
@@ -0,0 +1,445 @@
+/*
+ * Copyright 2016-2017 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
+ *
+ * http://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.function.web.gateway;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.embedded.LocalServerPort;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.cloud.function.gateway.ProxyExchange;
+import org.springframework.cloud.function.web.gateway.ProductionConfigurationTests.TestApplication;
+import org.springframework.cloud.function.web.gateway.ProductionConfigurationTests.TestApplication.Bar;
+import org.springframework.cloud.function.web.gateway.ProductionConfigurationTests.TestApplication.Foo;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+@ContextConfiguration(classes = TestApplication.class)
+public class ProductionConfigurationTests {
+
+ @Autowired
+ private TestRestTemplate rest;
+
+ @Autowired
+ private TestApplication application;
+
+ @LocalServerPort
+ private int port;
+
+ @Before
+ public void init() throws Exception {
+ application.setHome(new URI("http://localhost:" + port));
+ }
+
+ @Test
+ public void get() throws Exception {
+ assertThat(rest.getForObject("/proxy/0", Foo.class).getName()).isEqualTo("bye");
+ }
+
+ @Test
+ public void path() throws Exception {
+ assertThat(rest.getForObject("/proxy/path/1", Foo.class).getName())
+ .isEqualTo("foo");
+ }
+
+ @Test
+ public void resource() throws Exception {
+ assertThat(rest.getForObject("/proxy/html/test.html", String.class))
+ .contains("Test");
+ }
+
+ @Test
+ public void resourceWithNoType() throws Exception {
+ assertThat(rest.getForObject("/proxy/typeless/test.html", String.class))
+ .contains("Test");
+ }
+
+ @Test
+ public void missing() throws Exception {
+ assertThat(rest.getForEntity("/proxy/missing/0", Foo.class).getStatusCode())
+ .isEqualTo(HttpStatus.NOT_FOUND);
+ }
+
+ @Test
+ public void uri() throws Exception {
+ assertThat(rest.getForObject("/proxy/0", Foo.class).getName()).isEqualTo("bye");
+ }
+
+ @Test
+ public void post() throws Exception {
+ assertThat(rest.postForObject("/proxy/0", Collections.singletonMap("name", "foo"),
+ Bar.class).getName()).isEqualTo("host=localhost;foo");
+ }
+
+ @Test
+ public void forward() throws Exception {
+ assertThat(rest.getForObject("/forward/foos/0", Foo.class).getName())
+ .isEqualTo("bye");
+ }
+
+ @Test
+ public void forwardHeader() throws Exception {
+ ResponseEntity result = rest.getForEntity("/forward/special/foos/0",
+ Foo.class);
+ assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(result.getBody().getName()).isEqualTo("FOO");
+ }
+
+ @Test
+ public void postForwardHeader() throws Exception {
+ ResponseEntity> result = rest.exchange(
+ RequestEntity
+ .post(rest.getRestTemplate().getUriTemplateHandler().expand(
+ "/forward/special/bars"))
+ .body(Collections.singletonList(Collections.singletonMap("name", "foo"))),
+ new ParameterizedTypeReference>() {
+ });
+ assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(result.getBody().iterator().next().getName()).isEqualTo("FOOfoo");
+ }
+
+ @Test
+ public void postForwardBody() throws Exception {
+ ResponseEntity result = rest.exchange(
+ RequestEntity
+ .post(rest.getRestTemplate().getUriTemplateHandler().expand(
+ "/forward/body/bars"))
+ .body(Collections.singletonList(Collections.singletonMap("name", "foo"))),
+ String.class);
+ assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(result.getBody()).contains("foo");
+ }
+
+ @Test
+ public void postForwardForgetBody() throws Exception {
+ ResponseEntity result = rest.exchange(
+ RequestEntity
+ .post(rest.getRestTemplate().getUriTemplateHandler().expand(
+ "/forward/forget/bars"))
+ .body(Collections.singletonList(Collections.singletonMap("name", "foo"))),
+ String.class);
+ assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(result.getBody()).contains("foo");
+ }
+
+ @Test
+ public void postForwardBodyFoo() throws Exception {
+ ResponseEntity> result = rest.exchange(
+ RequestEntity
+ .post(rest.getRestTemplate().getUriTemplateHandler().expand(
+ "/forward/body/bars"))
+ .body(Collections.singletonList(Collections.singletonMap("name", "foo"))),
+ new ParameterizedTypeReference>() {
+ });
+ assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(result.getBody().iterator().next().getName()).isEqualTo("foo");
+ }
+
+ @Test
+ public void list() throws Exception {
+ assertThat(rest.exchange(
+ RequestEntity
+ .post(rest.getRestTemplate().getUriTemplateHandler().expand(
+ "/proxy"))
+ .body(Collections.singletonList(Collections.singletonMap("name", "foo"))),
+ new ParameterizedTypeReference>() {
+ }).getBody().iterator().next().getName()).isEqualTo("host=localhost;foo");
+ }
+
+ @Test
+ public void bodyless() throws Exception {
+ assertThat(rest.postForObject("/proxy/0", Collections.singletonMap("name", "foo"),
+ Bar.class).getName()).isEqualTo("host=localhost;foo");
+ }
+
+ @Test
+ public void entity() throws Exception {
+ assertThat(rest.exchange(
+ RequestEntity
+ .post(rest.getRestTemplate().getUriTemplateHandler()
+ .expand("/proxy/entity"))
+ .body(Collections.singletonMap("name", "foo")),
+ new ParameterizedTypeReference>() {
+ }).getBody().iterator().next().getName()).isEqualTo("host=localhost;foo");
+ }
+
+ @Test
+ public void entityWithType() throws Exception {
+ assertThat(rest.exchange(
+ RequestEntity
+ .post(rest.getRestTemplate().getUriTemplateHandler()
+ .expand("/proxy/type"))
+ .body(Collections.singletonMap("name", "foo")),
+ new ParameterizedTypeReference>() {
+ }).getBody().iterator().next().getName()).isEqualTo("host=localhost;foo");
+ }
+
+ @Test
+ public void single() throws Exception {
+ assertThat(rest.postForObject("/proxy/single",
+ Collections.singletonMap("name", "foobar"), Bar.class).getName())
+ .isEqualTo("host=localhost;foobar");
+ }
+
+ @Test
+ public void converter() throws Exception {
+ assertThat(rest.postForObject("/proxy/converter",
+ Collections.singletonMap("name", "foobar"), Bar.class).getName())
+ .isEqualTo("host=localhost;foobar");
+ }
+
+ @SpringBootApplication
+ static class TestApplication {
+
+ @RestController
+ static class ProxyController {
+
+ private URI home;
+
+ public void setHome(URI home) {
+ this.home = home;
+ }
+
+ @GetMapping("/proxy/{id}")
+ public ResponseEntity> proxyFoos(@PathVariable Integer id,
+ ProxyExchange> proxy) throws Exception {
+ return proxy.uri(home.toString() + "/foos/" + id).get();
+ }
+
+ @GetMapping("/proxy/path/**")
+ public ResponseEntity> proxyPath(ProxyExchange> proxy,
+ UriComponentsBuilder uri) throws Exception {
+ String path = proxy.path("/proxy/path/");
+ return proxy.uri(home.toString() + "/foos/" + path).get();
+ }
+
+ @GetMapping("/proxy/html/**")
+ public ResponseEntity proxyHtml(ProxyExchange proxy,
+ UriComponentsBuilder uri) throws Exception {
+ String path = proxy.path("/proxy/html");
+ return proxy.uri(home.toString() + path).get();
+ }
+
+ @GetMapping("/proxy/typeless/**")
+ public ResponseEntity> proxyTypeless(ProxyExchange> proxy,
+ UriComponentsBuilder uri) throws Exception {
+ String path = proxy.path("/proxy/typeless");
+ return proxy.uri(home.toString() + path).get();
+ }
+
+ @GetMapping("/proxy/missing/{id}")
+ public ResponseEntity> proxyMissing(@PathVariable Integer id,
+ ProxyExchange> proxy) throws Exception {
+ return proxy.uri(home.toString() + "/missing/" + id).get();
+ }
+
+ @GetMapping("/proxy")
+ public ResponseEntity> proxyUri(ProxyExchange> proxy) throws Exception {
+ return proxy.uri(home.toString() + "/foos").get();
+ }
+
+ @PostMapping("/proxy/{id}")
+ public ResponseEntity> proxyBars(@PathVariable Integer id,
+ @RequestBody Map body,
+ ProxyExchange> proxy) throws Exception {
+ body.put("id", id);
+ return proxy.uri(home.toString() + "/bars").body(Arrays.asList(body))
+ .post(this::first);
+ }
+
+ @PostMapping("/proxy")
+ public ResponseEntity> barsWithNoBody(ProxyExchange> proxy)
+ throws Exception {
+ return proxy.uri(home.toString() + "/bars").post();
+ }
+
+ @PostMapping("/proxy/entity")
+ public ResponseEntity> explicitEntity(@RequestBody Foo foo,
+ ProxyExchange> proxy) throws Exception {
+ return proxy.uri(home.toString() + "/bars").body(Arrays.asList(foo))
+ .post();
+ }
+
+ @PostMapping("/proxy/type")
+ public ResponseEntity> explicitEntityWithType(@RequestBody Foo foo,
+ ProxyExchange> proxy) throws Exception {
+ return proxy.uri(home.toString() + "/bars").body(Arrays.asList(foo))
+ .post();
+ }
+
+ @PostMapping("/proxy/single")
+ public ResponseEntity> implicitEntity(@RequestBody Foo foo,
+ ProxyExchange> proxy) throws Exception {
+ return proxy.uri(home.toString() + "/bars").body(Arrays.asList(foo))
+ .post(this::first);
+ }
+
+ @PostMapping("/proxy/converter")
+ public ResponseEntity implicitEntityWithConverter(@RequestBody Foo foo,
+ ProxyExchange> proxy) throws Exception {
+ return proxy.uri(home.toString() + "/bars").body(Arrays.asList(foo))
+ .post(response -> ResponseEntity.status(response.getStatusCode())
+ .headers(response.getHeaders())
+ .body(response.getBody().iterator().next()));
+ }
+
+ @GetMapping("/forward/**")
+ public void forward(ProxyExchange> proxy) throws Exception {
+ String path = proxy.path("/forward");
+ if (path.startsWith("/special")) {
+ proxy.header("X-Custom", "FOO");
+ path = proxy.path("/forward/special");
+ }
+ proxy.forward(path);
+ }
+
+ @PostMapping("/forward/**")
+ public void postForward(ProxyExchange> proxy) throws Exception {
+ String path = proxy.path("/forward");
+ if (path.startsWith("/special")) {
+ proxy.header("X-Custom", "FOO");
+ path = proxy.path("/forward/special");
+ }
+ proxy.forward(path);
+ }
+
+ @PostMapping("/forward/body/**")
+ public void postForwardBody(@RequestBody byte[] body, ProxyExchange> proxy)
+ throws Exception {
+ String path = proxy.path("/forward/body");
+ proxy.body(body).forward(path);
+ }
+
+ @PostMapping("/forward/forget/**")
+ public void postForwardForgetBody(@RequestBody byte[] body,
+ ProxyExchange> proxy) throws Exception {
+ String path = proxy.path("/forward/forget");
+ proxy.forward(path);
+ }
+
+ private ResponseEntity first(ResponseEntity> response) {
+ return ResponseEntity.status(response.getStatusCode())
+ .headers(response.getHeaders())
+ .body(response.getBody().iterator().next());
+ }
+
+ }
+
+ @Autowired
+ private ProxyController controller;
+
+ public void setHome(URI home) {
+ controller.setHome(home);
+ }
+
+ @RestController
+ static class TestController {
+
+ @GetMapping("/foos")
+ public List foos() {
+ return Arrays.asList(new Foo("hello"));
+ }
+
+ @GetMapping("/foos/{id}")
+ public Foo foo(@PathVariable Integer id, @RequestHeader HttpHeaders headers) {
+ String custom = headers.getFirst("X-Custom");
+ return new Foo(id == 1 ? "foo" : custom != null ? custom : "bye");
+ }
+
+ @PostMapping("/bars")
+ public List bars(@RequestBody List foos,
+ @RequestHeader HttpHeaders headers) {
+ String custom = headers.getFirst("X-Custom");
+ custom = custom == null ? "" : custom;
+ custom = headers.getFirst("forwarded")==null ? custom : headers.getFirst("forwarded") + ";" + custom;
+ return Arrays.asList(new Bar(custom + foos.iterator().next().getName()));
+ }
+
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ static class Foo {
+ private String name;
+
+ public Foo() {
+ }
+
+ public Foo(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ static class Bar {
+ private String name;
+
+ public Bar() {
+ }
+
+ public Bar(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/spring-cloud-gateway-mvc/src/test/resources/static/test.html b/spring-cloud-gateway-mvc/src/test/resources/static/test.html
new file mode 100644
index 000000000..0cfed139f
--- /dev/null
+++ b/spring-cloud-gateway-mvc/src/test/resources/static/test.html
@@ -0,0 +1,4 @@
+
+Test
+
+
\ No newline at end of file