From b8cf89fb45a412abe040c2c3658d9b4c2f47ea05 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Thu, 5 Feb 2015 16:32:33 +0100 Subject: [PATCH] Use an HttpMessageConverter for form body Instead of building a String, if we use an existing HttpMessageConverter from Spring web, we can support multipart form data. Fixes gh-197 (hopefully) --- spring-cloud-netflix-core/pom.xml | 8 -- .../filters/pre/FormBodyWrapperFilter.java | 90 +++++++++++++++---- .../zuul/FormZuulProxyApplicationTests.java | 14 +++ 3 files changed, 87 insertions(+), 25 deletions(-) diff --git a/spring-cloud-netflix-core/pom.xml b/spring-cloud-netflix-core/pom.xml index 6243aca0..85ee6f09 100644 --- a/spring-cloud-netflix-core/pom.xml +++ b/spring-cloud-netflix-core/pom.xml @@ -149,13 +149,5 @@ spring-boot-starter-test test - - javax.inject - javax.inject - - - com.google.guava - guava - diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/FormBodyWrapperFilter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/FormBodyWrapperFilter.java index 492ad7f1..96816a91 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/FormBodyWrapperFilter.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/FormBodyWrapperFilter.java @@ -16,17 +16,23 @@ package org.springframework.cloud.netflix.zuul.filters.pre; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.lang.reflect.Field; import java.util.Map.Entry; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; -import org.apache.commons.io.Charsets; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpOutputMessage; import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; +import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.ReflectionUtils; import com.netflix.zuul.ZuulFilter; @@ -67,10 +73,11 @@ public class FormBodyWrapperFilter extends ZuulFilter { if (contentType == null) { return false; } - // Only use this filter for MediaType : application/x-www-form-urlencoded + // Only use this filter for form data try { - return MediaType.APPLICATION_FORM_URLENCODED.includes(MediaType - .valueOf(contentType)); + MediaType mediaType = MediaType.valueOf(contentType); + return MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType) + || MediaType.MULTIPART_FORM_DATA.includes(mediaType); } catch (InvalidMediaTypeException ex) { return false; @@ -81,14 +88,19 @@ public class FormBodyWrapperFilter extends ZuulFilter { public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); + FormBodyRequestWrapper wrapper = null; if (request instanceof HttpServletRequestWrapper) { HttpServletRequest wrapped = (HttpServletRequest) ReflectionUtils.getField( this.requestField, request); - ReflectionUtils.setField(this.requestField, request, - new FormBodyRequestWrapper(wrapped)); + wrapper = new FormBodyRequestWrapper(wrapped); + ReflectionUtils.setField(this.requestField, request, wrapper); } else { - ctx.setRequest(new FormBodyRequestWrapper(request)); + wrapper = new FormBodyRequestWrapper(request); + ctx.setRequest(wrapper); + } + if (wrapper != null) { + ctx.getZuulRequestHeaders().put("content-type", wrapper.getContentType()); } return null; } @@ -99,17 +111,31 @@ public class FormBodyWrapperFilter extends ZuulFilter { private byte[] contentData; + private MediaType contentType; + + private int contentLength; + + private AllEncompassingFormHttpMessageConverter converter = new AllEncompassingFormHttpMessageConverter(); + public FormBodyRequestWrapper(HttpServletRequest request) { super(request); this.request = request; } + @Override + public String getContentType() { + if (this.contentData == null) { + buildContentData(); + } + return this.contentType.toString(); + } + @Override public int getContentLength() { if (this.contentData == null) { - this.contentData = buildContentData(); + buildContentData(); } - return this.contentData.length; + return this.contentLength; } @Override @@ -119,24 +145,54 @@ public class FormBodyWrapperFilter extends ZuulFilter { } else { if (this.contentData == null) { - this.contentData = buildContentData(); + buildContentData(); } return new ServletInputStreamWrapper(this.contentData); } } - private byte[] buildContentData() { - StringBuilder builder = new StringBuilder(); + private synchronized void buildContentData() { + MultiValueMap builder = new LinkedMultiValueMap(); for (Entry entry : this.request.getParameterMap() .entrySet()) { for (String value : entry.getValue()) { - if (builder.length() != 0) { - builder.append("&"); - } - builder.append(entry.getKey()).append("=").append(value); + builder.add(entry.getKey(), value); } } - return builder.toString().getBytes(Charsets.UTF_8); + FormHttpOutputMessage data = new FormHttpOutputMessage(); + data.getHeaders().setContentType( + MediaType.valueOf(this.request.getContentType())); + try { + this.converter.write(builder, this.contentType, data); + this.contentType = data.getHeaders().getContentType(); + this.contentLength = new Long(data.getHeaders().getContentLength()) + .intValue(); + this.contentData = data.getInput(); + } + catch (Exception e) { + throw new IllegalStateException("Cannot convert form data", e); + } + } + + private class FormHttpOutputMessage implements HttpOutputMessage { + + private HttpHeaders headers = new HttpHeaders(); + private ByteArrayOutputStream output = new ByteArrayOutputStream(); + + @Override + public HttpHeaders getHeaders() { + return this.headers; + } + + @Override + public OutputStream getBody() throws IOException { + return this.output; + } + + public byte[] getInput() throws IOException { + return this.output.toByteArray(); + } + } } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/FormZuulProxyApplicationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/FormZuulProxyApplicationTests.java index 1b0fa0e6..1d5a4875 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/FormZuulProxyApplicationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/FormZuulProxyApplicationTests.java @@ -85,6 +85,20 @@ public class FormZuulProxyApplicationTests { assertEquals("Posted! {foo=[bar]}", result.getBody()); } + @Test + public void postWithMultipartForm() { + MultiValueMap form = new LinkedMultiValueMap(); + form.set("foo", "bar"); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/simple", HttpMethod.POST, + new HttpEntity>(form, headers), + String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("Posted! {foo=[bar]}", result.getBody()); + } + @Test public void postWithUTF8Form() { MultiValueMap form = new LinkedMultiValueMap();