Commit 043732ae by Dave Syer

More careful handling of multipart data in proxy

To write file data the message converter needs the file content to be provided in the form of an HttpEntity per file. Fixes gh-197 again
parent 2d8085cc
...@@ -25,6 +25,7 @@ import java.util.Map.Entry; ...@@ -25,6 +25,7 @@ import java.util.Map.Entry;
import javax.servlet.ServletInputStream; import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage; import org.springframework.http.HttpOutputMessage;
import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.InvalidMediaTypeException;
...@@ -34,6 +35,8 @@ import org.springframework.util.Assert; ...@@ -34,6 +35,8 @@ import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartRequest;
import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.context.RequestContext;
...@@ -152,22 +155,36 @@ public class FormBodyWrapperFilter extends ZuulFilter { ...@@ -152,22 +155,36 @@ public class FormBodyWrapperFilter extends ZuulFilter {
} }
private synchronized void buildContentData() { private synchronized void buildContentData() {
MultiValueMap<String, String> builder = new LinkedMultiValueMap<String, String>(); try {
MultiValueMap<String, Object> builder = new LinkedMultiValueMap<String, Object>();
for (Entry<String, String[]> entry : this.request.getParameterMap() for (Entry<String, String[]> entry : this.request.getParameterMap()
.entrySet()) { .entrySet()) {
for (String value : entry.getValue()) { for (String value : entry.getValue()) {
builder.add(entry.getKey(), value); builder.add(entry.getKey(), value);
} }
} }
if (this.request instanceof MultipartRequest) {
MultipartRequest multi = (MultipartRequest) this.request;
for (Entry<String, MultipartFile> part : multi.getFileMap()
.entrySet()) {
MultipartFile file = part.getValue();
HttpHeaders headers = new HttpHeaders();
headers.setContentDispositionFormData(file.getName(),
file.getOriginalFilename());
headers.setContentType(MediaType.valueOf(file.getContentType()));
HttpEntity<byte[]> entity = new HttpEntity<byte[]>(
file.getBytes(), headers);
builder.set(part.getKey(), entity);
}
}
FormHttpOutputMessage data = new FormHttpOutputMessage(); FormHttpOutputMessage data = new FormHttpOutputMessage();
data.getHeaders().setContentType( this.contentType = MediaType.valueOf(this.request.getContentType());
MediaType.valueOf(this.request.getContentType())); data.getHeaders().setContentType(this.contentType);
try {
this.converter.write(builder, this.contentType, data); this.converter.write(builder, this.contentType, data);
// copy new content type including multipart boundary
this.contentType = data.getHeaders().getContentType(); this.contentType = data.getHeaders().getContentType();
this.contentLength = new Long(data.getHeaders().getContentLength())
.intValue();
this.contentData = data.getInput(); this.contentData = data.getInput();
this.contentLength = this.contentData.length;
} }
catch (Exception e) { catch (Exception e) {
throw new IllegalStateException("Cannot convert form data", e); throw new IllegalStateException("Cannot convert form data", e);
...@@ -190,6 +207,7 @@ public class FormBodyWrapperFilter extends ZuulFilter { ...@@ -190,6 +207,7 @@ public class FormBodyWrapperFilter extends ZuulFilter {
} }
public byte[] getInput() throws IOException { public byte[] getInput() throws IOException {
this.output.flush();
return this.output.toByteArray(); return this.output.toByteArray();
} }
......
...@@ -136,13 +136,23 @@ public class Servlet30WrapperFilter extends ZuulFilter { ...@@ -136,13 +136,23 @@ public class Servlet30WrapperFilter extends ZuulFilter {
@Override @Override
public boolean isAsyncStarted() { public boolean isAsyncStarted() {
try {
return this.request.isAsyncStarted(); return this.request.isAsyncStarted();
} }
catch (Throwable e) {
return false;
}
}
@Override @Override
public boolean isAsyncSupported() { public boolean isAsyncSupported() {
try {
return this.request.isAsyncSupported(); return this.request.isAsyncSupported();
} }
catch (Throwable e) {
return false;
}
}
@Override @Override
public AsyncContext getAsyncContext() { public AsyncContext getAsyncContext() {
......
...@@ -16,11 +16,11 @@ ...@@ -16,11 +16,11 @@
package org.springframework.cloud.netflix.zuul; package org.springframework.cloud.netflix.zuul;
import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
...@@ -28,7 +28,6 @@ import org.springframework.boot.test.IntegrationTest; ...@@ -28,7 +28,6 @@ import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate; import org.springframework.boot.test.TestRestTemplate;
import org.springframework.cloud.netflix.ribbon.RibbonClient; import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
...@@ -42,10 +41,11 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; ...@@ -42,10 +41,11 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.netflix.appinfo.EurekaInstanceConfig; import com.netflix.appinfo.EurekaInstanceConfig;
import com.netflix.loadbalancer.BaseLoadBalancer; import com.netflix.loadbalancer.BaseLoadBalancer;
...@@ -65,12 +65,6 @@ public class FormZuulProxyApplicationTests { ...@@ -65,12 +65,6 @@ public class FormZuulProxyApplicationTests {
@Value("${local.server.port}") @Value("${local.server.port}")
private int port; private int port;
@Autowired
private ProxyRouteLocator routes;
@Autowired
private RoutesEndpoint endpoint;
@Test @Test
public void postWithForm() { public void postWithForm() {
MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>(); MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
...@@ -100,6 +94,23 @@ public class FormZuulProxyApplicationTests { ...@@ -100,6 +94,23 @@ public class FormZuulProxyApplicationTests {
} }
@Test @Test
public void postWithMultipartFile() {
MultiValueMap<String, Object> form = new LinkedMultiValueMap<String, Object>();
HttpHeaders part = new HttpHeaders();
part.setContentType(MediaType.TEXT_PLAIN);
part.setContentDispositionFormData("file", "foo.txt");
form.set("foo", new HttpEntity<byte[]>("bar".getBytes(), part));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/simple/file", HttpMethod.POST,
new HttpEntity<MultiValueMap<String, Object>>(form, headers),
String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Posted! bar", result.getBody());
}
@Test
public void postWithUTF8Form() { public void postWithUTF8Form() {
MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>(); MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
form.set("foo", "bar"); form.set("foo", "bar");
...@@ -124,10 +135,18 @@ public class FormZuulProxyApplicationTests { ...@@ -124,10 +135,18 @@ public class FormZuulProxyApplicationTests {
class FormZuulProxyApplication { class FormZuulProxyApplication {
@RequestMapping(value = "/", method = RequestMethod.POST) @RequestMapping(value = "/", method = RequestMethod.POST)
public String delete(@RequestBody MultiValueMap<String, String> form) { public String accept(@RequestParam MultiValueMap<String, String> form)
throws IOException {
return "Posted! " + form; return "Posted! " + form;
} }
// TODO: Why does this not work if you add @RequestParam as above?
@RequestMapping(value = "/file", method = RequestMethod.POST)
public String file(@RequestParam(required = false) MultipartFile file)
throws IOException {
return "Posted! " + (file == null ? "" : new String(file.getBytes()));
}
@Bean @Bean
public ZuulFilter sampleFilter() { public ZuulFilter sampleFilter() {
return new ZuulFilter() { return new ZuulFilter() {
...@@ -156,7 +175,7 @@ class FormZuulProxyApplication { ...@@ -156,7 +175,7 @@ class FormZuulProxyApplication {
} }
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(SampleZuulProxyApplication.class, args); SpringApplication.run(FormZuulProxyApplication.class, args);
} }
} }
...@@ -170,6 +189,7 @@ class FormRibbonClientConfiguration { ...@@ -170,6 +189,7 @@ class FormRibbonClientConfiguration {
BaseLoadBalancer balancer = new BaseLoadBalancer(); BaseLoadBalancer balancer = new BaseLoadBalancer();
balancer.setServersList(Arrays.asList(new Server("localhost", instance balancer.setServersList(Arrays.asList(new Server("localhost", instance
.getNonSecurePort()))); .getNonSecurePort())));
// balancer.setServersList(Arrays.asList(new Server("localhost", 8000)));
return balancer; return balancer;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment