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
This commit is contained in:
Dave Syer
2015-02-12 18:37:54 +00:00
parent 2d8085cc24
commit 043732aef3
3 changed files with 73 additions and 25 deletions

View File

@@ -25,6 +25,7 @@ import java.util.Map.Entry;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.InvalidMediaTypeException;
@@ -34,6 +35,8 @@ import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
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.context.RequestContext;
@@ -152,22 +155,36 @@ public class FormBodyWrapperFilter extends ZuulFilter {
}
private synchronized void buildContentData() {
MultiValueMap<String, String> builder = new LinkedMultiValueMap<String, String>();
for (Entry<String, String[]> entry : this.request.getParameterMap()
.entrySet()) {
for (String value : entry.getValue()) {
builder.add(entry.getKey(), value);
}
}
FormHttpOutputMessage data = new FormHttpOutputMessage();
data.getHeaders().setContentType(
MediaType.valueOf(this.request.getContentType()));
try {
MultiValueMap<String, Object> builder = new LinkedMultiValueMap<String, Object>();
for (Entry<String, String[]> entry : this.request.getParameterMap()
.entrySet()) {
for (String value : entry.getValue()) {
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();
this.contentType = MediaType.valueOf(this.request.getContentType());
data.getHeaders().setContentType(this.contentType);
this.converter.write(builder, this.contentType, data);
// copy new content type including multipart boundary
this.contentType = data.getHeaders().getContentType();
this.contentLength = new Long(data.getHeaders().getContentLength())
.intValue();
this.contentData = data.getInput();
this.contentLength = this.contentData.length;
}
catch (Exception e) {
throw new IllegalStateException("Cannot convert form data", e);
@@ -190,6 +207,7 @@ public class FormBodyWrapperFilter extends ZuulFilter {
}
public byte[] getInput() throws IOException {
this.output.flush();
return this.output.toByteArray();
}

View File

@@ -136,12 +136,22 @@ public class Servlet30WrapperFilter extends ZuulFilter {
@Override
public boolean isAsyncStarted() {
return this.request.isAsyncStarted();
try {
return this.request.isAsyncStarted();
}
catch (Throwable e) {
return false;
}
}
@Override
public boolean isAsyncSupported() {
return this.request.isAsyncSupported();
try {
return this.request.isAsyncSupported();
}
catch (Throwable e) {
return false;
}
}
@Override

View File

@@ -16,11 +16,11 @@
package org.springframework.cloud.netflix.zuul;
import java.io.IOException;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -28,7 +28,6 @@ import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
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.Configuration;
import org.springframework.http.HttpEntity;
@@ -42,10 +41,11 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.util.LinkedMultiValueMap;
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.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.netflix.appinfo.EurekaInstanceConfig;
import com.netflix.loadbalancer.BaseLoadBalancer;
@@ -65,12 +65,6 @@ public class FormZuulProxyApplicationTests {
@Value("${local.server.port}")
private int port;
@Autowired
private ProxyRouteLocator routes;
@Autowired
private RoutesEndpoint endpoint;
@Test
public void postWithForm() {
MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
@@ -99,6 +93,23 @@ public class FormZuulProxyApplicationTests {
assertEquals("Posted! {foo=[bar]}", result.getBody());
}
@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() {
MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
@@ -124,10 +135,18 @@ public class FormZuulProxyApplicationTests {
class FormZuulProxyApplication {
@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;
}
// 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
public ZuulFilter sampleFilter() {
return new ZuulFilter() {
@@ -156,7 +175,7 @@ class FormZuulProxyApplication {
}
public static void main(String[] args) {
SpringApplication.run(SampleZuulProxyApplication.class, args);
SpringApplication.run(FormZuulProxyApplication.class, args);
}
}
@@ -170,6 +189,7 @@ class FormRibbonClientConfiguration {
BaseLoadBalancer balancer = new BaseLoadBalancer();
balancer.setServersList(Arrays.asList(new Server("localhost", instance
.getNonSecurePort())));
// balancer.setServersList(Arrays.asList(new Server("localhost", 8000)));
return balancer;
}