Improve support for MultiPart in jetty-core

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2024-06-11 19:01:55 +10:00
parent 1f78946a5c
commit 76cf685763
5 changed files with 755 additions and 11 deletions

View File

@ -98,8 +98,7 @@ public class MultiPartFormData
*/
public static CompletableFuture<Parts> from(Attributes attributes, MultiPartCompliance compliance, ComplianceViolation.Listener listener, String boundary, Function<Parser, CompletableFuture<Parts>> parse)
{
@SuppressWarnings("unchecked")
CompletableFuture<Parts> futureParts = (CompletableFuture<Parts>)attributes.getAttribute(MultiPartFormData.class.getName());
CompletableFuture<Parts> futureParts = get(attributes);
if (futureParts == null)
{
futureParts = parse.apply(new Parser(boundary, compliance, listener));
@ -108,6 +107,18 @@ public class MultiPartFormData
return futureParts;
}
/**
* Returns {@code multipart/form-data} parts if they have already been created.
*
* @param attributes the attributes where the futureParts are tracked
* @return the future parts
*/
@SuppressWarnings("unchecked")
public static CompletableFuture<Parts> get(Attributes attributes)
{
return (CompletableFuture<Parts>)attributes.getAttribute(MultiPartFormData.class.getName());
}
/**
* <p>An ordered list of {@link MultiPart.Part}s that can
* be accessed by index or by name, or iterated over.</p>
@ -221,7 +232,7 @@ public class MultiPartFormData
private final MultiPart.Parser parser;
private final MultiPartCompliance compliance;
private final ComplianceViolation.Listener complianceListener;
private boolean useFilesForPartsWithoutFileName;
private boolean useFilesForPartsWithoutFileName = true;
private Path filesDirectory;
private long maxFileSize = -1;
private long maxMemoryFileSize;
@ -450,17 +461,17 @@ public class MultiPartFormData
public void onPartContent(Content.Chunk chunk)
{
ByteBuffer buffer = chunk.getByteBuffer();
long maxFileSize = getMaxFileSize();
fileSize += buffer.remaining();
if (maxFileSize >= 0 && fileSize > maxFileSize)
{
onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxFileSize)));
return;
}
String fileName = getFileName();
if (fileName != null || isUseFilesForPartsWithoutFileName())
{
long maxFileSize = getMaxFileSize();
fileSize += buffer.remaining();
if (maxFileSize >= 0 && fileSize > maxFileSize)
{
onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxFileSize)));
return;
}
long maxMemoryFileSize = getMaxMemoryFileSize();
if (maxMemoryFileSize >= 0)
{

View File

@ -0,0 +1,107 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.nio.file.Path;
import org.eclipse.jetty.http.ComplianceViolation;
import org.eclipse.jetty.http.MultiPartCompliance;
import static org.eclipse.jetty.http.ComplianceViolation.Listener.NOOP;
public class MultiPartConfig
{
public static MultiPartConfig from(Request request, Path location, int maxFormKeys, long maxRequestSize, long maxFileSize, long fileSizeThreshold)
{
HttpChannel httpChannel = HttpChannel.from(request);
HttpConfiguration httpConfiguration = request.getConnectionMetaData().getHttpConfiguration();
MultiPartCompliance multiPartCompliance = httpConfiguration.getMultiPartCompliance();
ComplianceViolation.Listener complianceViolationListener = httpChannel.getComplianceViolationListener();
int maxHeaderSize = httpConfiguration.getRequestHeaderSize();
if (location == null)
location = request.getContext().getTempDirectory().toPath();
return new MultiPartConfig(location, maxFormKeys, maxRequestSize,
maxFileSize, fileSizeThreshold, maxHeaderSize, multiPartCompliance, complianceViolationListener);
}
private final Path location;
private final long fileSizeThreshold;
private final long maxFileSize;
private final long maxRequestSize;
private final int maxFormKeys;
private final int maxHeadersSize;
private final MultiPartCompliance compliance;
private final ComplianceViolation.Listener listener;
public MultiPartConfig(Path location, int maxFormKeys, long maxRequestSize, long maxFileSize, long fileSizeThreshold)
{
this(location, maxFormKeys, maxRequestSize, maxFileSize, fileSizeThreshold, 8 * 1024, MultiPartCompliance.RFC7578, null);
}
public MultiPartConfig(Path location, int maxFormKeys, long maxRequestSize, long maxFileSize, long fileSizeThreshold,
int maxHeadersSize, MultiPartCompliance compliance, ComplianceViolation.Listener listener)
{
this.location = location;
this.maxFormKeys = maxFormKeys;
this.maxRequestSize = maxRequestSize;
this.maxFileSize = maxFileSize;
this.fileSizeThreshold = fileSizeThreshold;
this.maxHeadersSize = maxHeadersSize;
this.compliance = compliance;
this.listener = listener == null ? NOOP : listener;
}
public Path getLocation()
{
return location;
}
public long getFileSizeThreshold()
{
return fileSizeThreshold;
}
public long getMaxFileSize()
{
return maxFileSize;
}
public long getMaxRequestSize()
{
return maxRequestSize;
}
public int getMaxFormKeys()
{
return maxFormKeys;
}
public int getMaxHeadersSize()
{
return maxHeadersSize;
}
public MultiPartCompliance getMultiPartCompliance()
{
return compliance;
}
public ComplianceViolation.Listener getViolationListener()
{
return listener;
}
}

View File

@ -0,0 +1,82 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.util.Attributes;
public class MultiPartFormFields
{
public static CompletableFuture<MultiPartFormData.Parts> from(Request request, MultiPartConfig config)
{
return from(request, request, config);
}
public static CompletableFuture<MultiPartFormData.Parts> from(Request request, Content.Source source, MultiPartConfig config)
{
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
return from(source, request, contentType, config);
}
public static CompletableFuture<MultiPartFormData.Parts> from(Content.Source content, Attributes attributes, String contentType, MultiPartConfig config)
{
// Look for an existing future (we use the future here rather than the parts as it can remember any failure).
CompletableFuture<MultiPartFormData.Parts> futureParts = MultiPartFormData.get(attributes);
if (futureParts == null)
{
// No existing parts, so we need to try to read them ourselves
// Are we the right content type to produce our own parts?
if (contentType == null || !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpField.getValueParameters(contentType, null)))
return CompletableFuture.failedFuture(new IllegalStateException("Not multipart Content-Type"));
// Do we have a boundary?
String boundary = MultiPart.extractBoundary(contentType);
if (boundary == null)
return CompletableFuture.failedFuture(new IllegalStateException("No multipart boundary parameter in Content-Type"));
// Look for an existing future MultiPartFormData.Parts
futureParts = MultiPartFormData.from(attributes, config.getMultiPartCompliance(), config.getViolationListener(), boundary, parser ->
{
try
{
// No existing core parts, so we need to configure the parser.
parser.setMaxParts(config.getMaxFormKeys());
parser.setMaxMemoryFileSize(config.getFileSizeThreshold());
parser.setMaxFileSize(config.getMaxFileSize());
parser.setMaxLength(config.getMaxRequestSize());
parser.setPartHeadersMaxLength(config.getMaxHeadersSize());
if (config.getLocation() != null)
parser.setFilesDirectory(config.getLocation());
// parse the core parts.
return parser.parse(content);
}
catch (Throwable failure)
{
return CompletableFuture.failedFuture(failure);
}
});
}
return futureParts;
}
}

View File

@ -472,6 +472,7 @@ public class MultiPartServletTest
InputStream inputStream = new GZIPInputStream(responseStream.getInputStream());
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
formData.setMaxParts(1);
formData.setMaxMemoryFileSize(-1);
MultiPartFormData.Parts parts = formData.parse(new InputStreamContentSource(inputStream)).join();
assertThat(parts.size(), is(1));

View File

@ -0,0 +1,543 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.test;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.zip.GZIPInputStream;
import org.eclipse.jetty.client.BytesRequestContent;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.InputStreamResponseListener;
import org.eclipse.jetty.client.MultiPartRequestContent;
import org.eclipse.jetty.client.OutputStreamRequestContent;
import org.eclipse.jetty.client.StringRequestContent;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.io.content.InputStreamContentSource;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.MultiPartConfig;
import org.eclipse.jetty.server.MultiPartFormFields;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.component.LifeCycle;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
public class CoreMultiPartTest
{
private static final int MAX_FILE_SIZE = 512 * 1024;
private Server server;
private ServerConnector connector;
private HttpClient client;
private Path tmpDir;
private MultiPartConfig config;
@BeforeEach
public void before() throws Exception
{
tmpDir = Files.createTempDirectory(CoreMultiPartTest.class.getSimpleName());
}
private void start(Handler handler, MultiPartConfig config) throws Exception
{
this.config = config == null ? new MultiPartConfig(tmpDir, -1, -1, MAX_FILE_SIZE, 0) : config;
server = new Server(null, null, null);
connector = new ServerConnector(server);
server.addConnector(connector);
server.setHandler(handler);
GzipHandler gzipHandler = new GzipHandler();
gzipHandler.addIncludedMimeTypes("multipart/form-data");
gzipHandler.setMinGzipSize(32);
server.insertHandler(gzipHandler);
server.start();
client = new HttpClient();
client.start();
}
@AfterEach
public void stop() throws Exception
{
LifeCycle.stop(client);
LifeCycle.stop(server);
IO.delete(tmpDir.toFile());
}
private MultiPartFormData.Parts getParts(Request request, MultiPartConfig config)
{
try
{
return MultiPartFormFields.from(request, config).get();
}
catch (Throwable t)
{
throw new BadMessageException("bad multipart", t.getCause());
}
}
@Test
public void testLargePart() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
getParts(request, config);
callback.succeeded();
return true;
}
}, new MultiPartConfig(null, -1, -1, 1024 * 1024, -1));
OutputStreamRequestContent content = new OutputStreamRequestContent();
MultiPartRequestContent multiPart = new MultiPartRequestContent();
multiPart.addPart(new MultiPart.ContentSourcePart("param", null, null, content));
multiPart.close();
InputStreamResponseListener listener = new InputStreamResponseListener();
client.newRequest("localhost", connector.getLocalPort())
.path("/defaultConfig")
.scheme(HttpScheme.HTTP.asString())
.method(HttpMethod.POST)
.body(multiPart)
.send(listener);
// The write side will eventually throw because connection is closed.
assertThrows(Throwable.class, () ->
{
// Write large amount of content to the part.
byte[] byteArray = new byte[1024 * 1024];
Arrays.fill(byteArray, (byte)1);
for (int i = 0; i < 1024 * 2; i++)
{
content.getOutputStream().write(byteArray);
}
content.close();
});
assert400orEof(listener, responseContent -> assertThat(responseContent, containsString("400")));
}
@Test
public void testManyParts() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
getParts(request, config);
callback.succeeded();
return true;
}
}, new MultiPartConfig(null, 1024, -1, -1, -1));
byte[] byteArray = new byte[1024];
Arrays.fill(byteArray, (byte)1);
MultiPartRequestContent multiPart = new MultiPartRequestContent();
for (int i = 0; i < 1024 * 1024; i++)
{
BytesRequestContent content = new BytesRequestContent(byteArray);
multiPart.addPart(new MultiPart.ContentSourcePart("part" + i, null, null, content));
}
multiPart.close();
InputStreamResponseListener listener = new InputStreamResponseListener();
client.newRequest("localhost", connector.getLocalPort())
.path("/defaultConfig")
.scheme(HttpScheme.HTTP.asString())
.method(HttpMethod.POST)
.body(multiPart)
.send(listener);
assert400orEof(listener, responseContent -> assertThat(responseContent, containsString("400")));
}
@Test
public void testMaxRequestSize() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
getParts(request, config);
callback.succeeded();
return true;
}
}, new MultiPartConfig(tmpDir, -1, 1024, -1, 1024 * 1024 * 8));
OutputStreamRequestContent content = new OutputStreamRequestContent();
MultiPartRequestContent multiPart = new MultiPartRequestContent();
multiPart.addPart(new MultiPart.ContentSourcePart("param", null, null, content));
multiPart.close();
InputStreamResponseListener listener = new InputStreamResponseListener();
client.newRequest("localhost", connector.getLocalPort())
.path("/requestSizeLimit")
.scheme(HttpScheme.HTTP.asString())
.method(HttpMethod.POST)
.body(multiPart)
.send(listener);
Throwable writeError = null;
try
{
// Write large amount of content to the part.
byte[] byteArray = new byte[1024 * 1024];
Arrays.fill(byteArray, (byte)1);
for (int i = 0; i < 1024 * 1024; i++)
{
content.getOutputStream().write(byteArray);
}
fail("We should never be able to write all the content.");
}
catch (Exception e)
{
writeError = e;
}
assertThat(writeError, instanceOf(EofException.class));
assert400orEof(listener, null);
}
private static void assert400orEof(InputStreamResponseListener listener, Consumer<String> checkbody) throws InterruptedException, TimeoutException
{
// There is a race here, either we fail trying to write some more content OR
// we get 400 response, for some reason reading the content throws EofException.
String responseContent = null;
try
{
org.eclipse.jetty.client.Response response = listener.get(60, TimeUnit.SECONDS);
assertThat(response.getStatus(), equalTo(HttpStatus.BAD_REQUEST_400));
responseContent = IO.toString(listener.getInputStream());
}
catch (ExecutionException | IOException e)
{
Throwable cause = e.getCause();
assertThat(cause, instanceOf(EofException.class));
return;
}
if (checkbody != null)
checkbody.accept(responseContent);
}
@Test
public void testSimpleMultiPart() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
MultiPartFormData.Parts parts = getParts(request, config);
assertNotNull(parts);
assertEquals(1, parts.size());
MultiPart.Part part = parts.iterator().next();
assertEquals("part1", part.getName());
HttpFields fields = part.getHeaders();
assertNotNull(fields);
assertEquals(2, fields.size());
InputStream inputStream = Content.Source.asInputStream(part.getContentSource());
String content1 = IO.toString(inputStream, UTF_8);
assertEquals("content1", content1);
callback.succeeded();
return true;
}
}, null);
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
{
OutputStream output = socket.getOutputStream();
String content = """
--A1B2C3
Content-Disposition: form-data; name="part1"
Content-Type: text/plain; charset="UTF-8"
content1
--A1B2C3--
""";
String header = """
POST / HTTP/1.1
Host: localhost
Content-Type: multipart/form-data; boundary="A1B2C3"
Content-Length: $L
""".replace("$L", String.valueOf(content.length()));
output.write(header.getBytes(UTF_8));
output.write(content.getBytes(UTF_8));
output.flush();
HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream());
assertNotNull(response);
assertEquals(HttpStatus.OK_200, response.getStatus());
}
}
@Test
public void testTempFilesDeletedOnError() throws Exception
{
byte[] bytes = new byte[2 * MAX_FILE_SIZE];
Arrays.fill(bytes, (byte)1);
// Should throw as the max file size is exceeded.
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
getParts(request, config);
callback.succeeded();
return true;
}
}, null);
MultiPartRequestContent multiPart = new MultiPartRequestContent();
multiPart.addPart(new MultiPart.ContentSourcePart("largePart", "largeFile.bin", HttpFields.EMPTY, new BytesRequestContent(bytes)));
multiPart.close();
try (StacklessLogging ignored = new StacklessLogging(HttpChannel.class))
{
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.scheme(HttpScheme.HTTP.asString())
.method(HttpMethod.POST)
.body(multiPart)
.send();
assertEquals(400, response.getStatus());
}
String[] fileList = tmpDir.toFile().list();
assertNotNull(fileList);
assertThat(fileList.length, is(0));
}
@Test
public void testDefaultTempDirectory() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
MultiPartConfig conf = MultiPartConfig.from(request, config.getLocation(),
config.getMaxFormKeys(), config.getMaxRequestSize(),
config.getMaxFileSize(), config.getFileSizeThreshold());
MultiPartFormData.Parts parts = getParts(request, conf);
assertNotNull(parts);
assertEquals(1, parts.size());
MultiPart.Part part = parts.iterator().next();
assertEquals("part1", part.getName());
HttpFields headers = part.getHeaders();
assertNotNull(headers);
assertEquals(2, headers.size());
InputStream inputStream = Content.Source.asInputStream(part.getContentSource());
String content1 = IO.toString(inputStream, UTF_8);
assertEquals("content1", content1);
callback.succeeded();
return true;
}
}, new MultiPartConfig(null, -1, MAX_FILE_SIZE, -1, 0));
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
{
OutputStream output = socket.getOutputStream();
String content = """
--A1B2C3
Content-Disposition: form-data; name="part1"
Content-Type: text/plain; charset="UTF-8"
content1
--A1B2C3--
""";
String header = """
POST / HTTP/1.1
Host: localhost
Content-Type: multipart/form-data; boundary="A1B2C3"
Content-Length: $L
""".replace("$L", String.valueOf(content.length()));
output.write(header.getBytes(UTF_8));
output.write(content.getBytes(UTF_8));
output.flush();
HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream());
assertNotNull(response);
assertEquals(HttpStatus.OK_200, response.getStatus());
}
}
@Test
public void testMultiPartGzip() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
response.getHeaders().put(HttpHeader.CONTENT_TYPE, contentType);
MultiPartRequestContent echoParts = new MultiPartRequestContent(MultiPart.extractBoundary(contentType));
MultiPartFormData.Parts servletParts = getParts(request, config);
for (MultiPart.Part part : servletParts)
{
HttpFields.Mutable partHeaders = HttpFields.build();
for (HttpField field : part.getHeaders())
partHeaders.add(field);
echoParts.addPart(new MultiPart.ContentSourcePart(part.getName(), part.getFileName(), partHeaders, part.getContentSource()));
}
echoParts.close();
IO.copy(Content.Source.asInputStream(echoParts), Content.Sink.asOutputStream(response));
callback.succeeded();
return true;
}
}, null);
// Do not automatically handle gzip.
client.getContentDecoderFactories().clear();
String contentString = "the quick brown fox jumps over the lazy dog, " +
"the quick brown fox jumps over the lazy dog";
StringRequestContent content = new StringRequestContent(contentString);
MultiPartRequestContent multiPartContent = new MultiPartRequestContent();
multiPartContent.addPart(new MultiPart.ContentSourcePart("stringPart", null, HttpFields.EMPTY, content));
multiPartContent.close();
InputStreamResponseListener responseStream = new InputStreamResponseListener();
client.newRequest("localhost", connector.getLocalPort())
.path("/echo")
.scheme(HttpScheme.HTTP.asString())
.method(HttpMethod.POST)
.headers(h -> h.add(HttpHeader.ACCEPT_ENCODING, "gzip"))
.body(multiPartContent)
.send(responseStream);
org.eclipse.jetty.client.Response response = responseStream.get(5, TimeUnit.SECONDS);
HttpFields headers = response.getHeaders();
assertThat(headers.get(HttpHeader.CONTENT_TYPE), startsWith("multipart/form-data"));
assertThat(headers.get(HttpHeader.CONTENT_ENCODING), is("gzip"));
String contentType = headers.get(HttpHeader.CONTENT_TYPE);
String boundary = MultiPart.extractBoundary(contentType);
InputStream inputStream = new GZIPInputStream(responseStream.getInputStream());
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
formData.setMaxParts(1);
formData.setMaxMemoryFileSize(-1);
MultiPartFormData.Parts parts = formData.parse(new InputStreamContentSource(inputStream)).join();
assertThat(parts.size(), is(1));
assertThat(parts.get(0).getContentAsString(UTF_8), is(contentString));
}
@Test
public void testDoubleReadFromPart() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain");
PrintWriter writer = new PrintWriter(Content.Sink.asOutputStream(response));
for (MultiPart.Part part : getParts(request, config))
{
String partContent = IO.toString(Content.Source.asInputStream(part.getContentSource()));
writer.println("Part: name=" + part.getName() + ", size=" + part.getLength() + ", content=" + partContent);
// We can only consume the getContentSource() once so we must use newContentSource().
partContent = IO.toString(Content.Source.asInputStream(part.newContentSource()));
writer.println("Part: name=" + part.getName() + ", size=" + part.getLength() + ", content=" + partContent);
}
writer.close();
callback.succeeded();
return true;
}
}, null);
String contentString = "the quick brown fox jumps over the lazy dog, " +
"the quick brown fox jumps over the lazy dog";
StringRequestContent content = new StringRequestContent(contentString);
MultiPartRequestContent multiPart = new MultiPartRequestContent();
multiPart.addPart(new MultiPart.ContentSourcePart("myPart", null, HttpFields.EMPTY, content));
multiPart.close();
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.scheme(HttpScheme.HTTP.asString())
.method(HttpMethod.POST)
.body(multiPart)
.send();
assertEquals(200, response.getStatus());
assertThat(response.getContentAsString(), containsString("Part: name=myPart, size=88, content=the quick brown fox jumps over the lazy dog, the quick brown fox jumps over the lazy dog\n" +
"Part: name=myPart, size=88, content=the quick brown fox jumps over the lazy dog, the quick brown fox jumps over the lazy dog"));
}
}