diff --git a/VERSION.txt b/VERSION.txt
index 259865a698a..0c63546b804 100644
--- a/VERSION.txt
+++ b/VERSION.txt
@@ -414,8 +414,6 @@ jetty-10.0.18 - 26 October 2023
+ 10537 HTTP/3: Incomplete Data Transfer When Used with Spring Boot WebFlux
+ 10696 jetty.sh doesn't work with JETTY_USER in Jetty 10.0.17 thru Jetty
12.0.2
- + 10669 Provide ability to defer initial deployment of webapps until after
- Server has started
+ 10705 Creating a `HTTP3ServerConnector` with a `SslContextFactory` that has
a non-null `SSLContext` makes the server fail to start with an unclear error
message
diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpDestination.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpDestination.java
index 8ba6035f2e5..3758bd2c597 100644
--- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpDestination.java
+++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpDestination.java
@@ -188,7 +188,7 @@ public class HttpDestination extends ContainerLifeCycle implements Destination,
protected void doStop() throws Exception
{
requestTimeouts.destroy();
- abort(new AsynchronousCloseException());
+ abortExchanges(new AsynchronousCloseException());
Sweeper connectionPoolSweeper = client.getBean(Sweeper.class);
if (connectionPoolSweeper != null && connectionPool instanceof Sweeper.Sweepable)
connectionPoolSweeper.remove((Sweeper.Sweepable)connectionPool);
@@ -294,7 +294,7 @@ public class HttpDestination extends ContainerLifeCycle implements Destination,
@Override
public void failed(Throwable x)
{
- abort(x);
+ abortExchanges(x);
}
@Override
@@ -513,7 +513,7 @@ public class HttpDestination extends ContainerLifeCycle implements Destination,
*
* @param cause the abort cause
*/
- public void abort(Throwable cause)
+ private void abortExchanges(Throwable cause)
{
// Copy the queue of exchanges and fail only those that are queued at this moment.
// The application may queue another request from the failure/complete listener
diff --git a/jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/ContextProviderStartupTest.java b/jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/ContextProviderStartupTest.java
index c9072312e46..aa8acefca23 100644
--- a/jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/ContextProviderStartupTest.java
+++ b/jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/ContextProviderStartupTest.java
@@ -32,10 +32,8 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
-import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/EtagUtils.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/EtagUtils.java
index 2effc40960c..9221cef3999 100644
--- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/EtagUtils.java
+++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/EtagUtils.java
@@ -66,13 +66,9 @@ public final class EtagUtils
*/
public static HttpField createWeakEtagField(Resource resource, String etagSuffix)
{
- Path path = resource.getPath();
- if (path == null)
- return null;
-
- String etagValue = EtagUtils.computeWeakEtag(path, etagSuffix);
- if (etagValue != null)
- return new PreEncodedHttpField(HttpHeader.ETAG, etagValue);
+ String etag = EtagUtils.computeWeakEtag(resource, etagSuffix);
+ if (etag != null)
+ return new PreEncodedHttpField(HttpHeader.ETAG, etag);
return null;
}
@@ -213,6 +209,9 @@ public final class EtagUtils
*/
public static String rewriteWithSuffix(String etag, String newSuffix)
{
+ if (etag == null)
+ return null;
+
StringBuilder ret = new StringBuilder();
boolean weak = etag.startsWith("W/");
int start = 0;
diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartConfig.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartConfig.java
new file mode 100644
index 00000000000..7373b3fba0e
--- /dev/null
+++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartConfig.java
@@ -0,0 +1,220 @@
+//
+// ========================================================================
+// 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.http;
+
+import java.nio.file.Path;
+
+import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.util.Attributes;
+
+import static org.eclipse.jetty.http.ComplianceViolation.Listener.NOOP;
+
+/**
+ * The Configuration needed to parse multipart/form-data.
+ * @see MultiPartFormData#from(Content.Source, Attributes, String, MultiPartConfig)
+ */
+public class MultiPartConfig
+{
+ private static final int DEFAULT_MAX_PARTS = 100;
+ private static final int DEFAULT_MAX_SIZE = 50 * 1024 * 1024;
+ private static final int DEFAULT_MAX_PART_SIZE = 10 * 1024 * 1024;
+ private static final int DEFAULT_MAX_MEMORY_PART_SIZE = 1024;
+ private static final int DEFAULT_MAX_HEADERS_SIZE = 8 * 1024;
+ private static final boolean DEFAULT_USE_FILES_FOR_PARTS_WITHOUT_FILE_NAME = true;
+
+ public static class Builder
+ {
+ private Path _location;
+ private Integer _maxParts;
+ private Long _maxSize;
+ private Long _maxPartSize;
+ private Long _maxMemoryPartSize;
+ private Integer _maxHeadersSize;
+ private Boolean _useFilesForPartsWithoutFileName;
+ private MultiPartCompliance _complianceMode;
+ private ComplianceViolation.Listener _violationListener;
+
+ public Builder()
+ {
+ }
+
+ /**
+ * @param location the directory where parts will be saved as files.
+ */
+ public Builder location(Path location)
+ {
+ _location = location;
+ return this;
+ }
+
+ /**
+ * @param maxParts the maximum number of parts that can be parsed from the multipart content, or -1 for unlimited.
+ */
+ public Builder maxParts(int maxParts)
+ {
+ _maxParts = maxParts;
+ return this;
+ }
+
+ /**
+ * @return the maximum size in bytes of the whole multipart content, or -1 for unlimited.
+ */
+ public Builder maxSize(long maxSize)
+ {
+ _maxSize = maxSize;
+ return this;
+ }
+
+ /**
+ * @return the maximum part size in bytes, or -1 for unlimited.
+ */
+ public Builder maxPartSize(long maxPartSize)
+ {
+ _maxPartSize = maxPartSize;
+ return this;
+ }
+
+ /**
+ *
Sets the maximum size of a part in memory, after which it will be written as a file.
+ * Use value {@code 0} to always write the part to disk.
+ * Use value {@code -1} to never write the part to disk.
+ *
+ * @param maxMemoryPartSize the maximum part size which can be held in memory.
+ */
+ public Builder maxMemoryPartSize(long maxMemoryPartSize)
+ {
+ _maxMemoryPartSize = maxMemoryPartSize;
+ return this;
+ }
+
+ /**
+ * @param maxHeadersSize the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length.
+ */
+ public Builder maxHeadersSize(int maxHeadersSize)
+ {
+ _maxHeadersSize = maxHeadersSize;
+ return this;
+ }
+
+ /**
+ * @param useFilesForPartsWithoutFileName whether parts without a fileName may be stored as files.
+ */
+ public Builder useFilesForPartsWithoutFileName(Boolean useFilesForPartsWithoutFileName)
+ {
+ _useFilesForPartsWithoutFileName = useFilesForPartsWithoutFileName;
+ return this;
+ }
+
+ /**
+ * @param complianceMode the compliance mode.
+ */
+ public Builder complianceMode(MultiPartCompliance complianceMode)
+ {
+ _complianceMode = complianceMode;
+ return this;
+ }
+
+ /**
+ * @param violationListener the compliance violation listener.
+ */
+ public Builder violationListener(ComplianceViolation.Listener violationListener)
+ {
+ _violationListener = violationListener;
+ return this;
+ }
+
+ public MultiPartConfig build()
+ {
+ return new MultiPartConfig(_location,
+ _maxParts == null ? DEFAULT_MAX_PARTS : _maxParts,
+ _maxSize == null ? DEFAULT_MAX_SIZE : _maxSize,
+ _maxPartSize == null ? DEFAULT_MAX_PART_SIZE : _maxPartSize,
+ _maxMemoryPartSize == null ? DEFAULT_MAX_MEMORY_PART_SIZE : _maxMemoryPartSize,
+ _maxHeadersSize == null ? DEFAULT_MAX_HEADERS_SIZE : _maxHeadersSize,
+ _useFilesForPartsWithoutFileName == null ? DEFAULT_USE_FILES_FOR_PARTS_WITHOUT_FILE_NAME : _useFilesForPartsWithoutFileName,
+ _complianceMode == null ? MultiPartCompliance.RFC7578 : _complianceMode,
+ _violationListener == null ? NOOP : _violationListener);
+ }
+ }
+
+ private final Path _location;
+ private final long _maxMemoryPartSize;
+ private final long _maxPartSize;
+ private final long _maxSize;
+ private final int _maxParts;
+ private final int _maxHeadersSize;
+ private final boolean _useFilesForPartsWithoutFileName;
+ private final MultiPartCompliance _compliance;
+ private final ComplianceViolation.Listener _listener;
+
+ private MultiPartConfig(Path location, int maxParts, long maxSize, long maxPartSize, long maxMemoryPartSize,
+ int maxHeadersSize, boolean useFilesForPartsWithoutFileName,
+ MultiPartCompliance compliance, ComplianceViolation.Listener listener)
+ {
+ this._location = location;
+ this._maxParts = maxParts;
+ this._maxSize = maxSize;
+ this._maxPartSize = maxPartSize;
+ this._maxMemoryPartSize = maxMemoryPartSize;
+ this._maxHeadersSize = maxHeadersSize;
+ this._useFilesForPartsWithoutFileName = useFilesForPartsWithoutFileName;
+ this._compliance = compliance;
+ this._listener = listener;
+ }
+
+ public Path getLocation()
+ {
+ return _location;
+ }
+
+ public int getMaxParts()
+ {
+ return _maxParts;
+ }
+
+ public long getMaxSize()
+ {
+ return _maxSize;
+ }
+
+ public long getMaxPartSize()
+ {
+ return _maxPartSize;
+ }
+
+ public long getMaxMemoryPartSize()
+ {
+ return _maxMemoryPartSize;
+ }
+
+ public int getMaxHeadersSize()
+ {
+ return _maxHeadersSize;
+ }
+
+ public boolean isUseFilesForPartsWithoutFileName()
+ {
+ return _useFilesForPartsWithoutFileName;
+ }
+
+ public MultiPartCompliance getMultiPartCompliance()
+ {
+ return _compliance;
+ }
+
+ public ComplianceViolation.Listener getViolationListener()
+ {
+ return _listener;
+ }
+}
diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java
index e1cae2531fa..69293958022 100644
--- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java
+++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java
@@ -79,8 +79,45 @@ public class MultiPartFormData
}
/**
- * Returns {@code multipart/form-data} parts using {@link MultiPartCompliance#RFC7578}.
+ * Returns {@code multipart/form-data} parts using the given {@link Content.Source} and {@link MultiPartConfig}.
+ *
+ * @param content the source of the multipart content.
+ * @param attributes the attributes where the futureParts are tracked.
+ * @param contentType the value of the {@link HttpHeader#CONTENT_TYPE} header.
+ * @param config the multipart configuration.
+ * @return the future parts
*/
+ public static CompletableFuture 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 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"));
+
+ Parser parser = new Parser(boundary);
+ parser.configure(config);
+ futureParts = parser.parse(content);
+ attributes.setAttribute(MultiPartFormData.class.getName(), futureParts);
+ return futureParts;
+ }
+ return futureParts;
+ }
+
+ /**
+ * Returns {@code multipart/form-data} parts using {@link MultiPartCompliance#RFC7578}.
+ * @deprecated use {@link #from(Content.Source, Attributes, String, MultiPartConfig)}.
+ */
+ @Deprecated
public static CompletableFuture from(Attributes attributes, String boundary, Function> parse)
{
return from(attributes, MultiPartCompliance.RFC7578, ComplianceViolation.Listener.NOOP, boundary, parse);
@@ -95,11 +132,12 @@ public class MultiPartFormData
* @param boundary the boundary for the {@code multipart/form-data} parts
* @param parse the parser completable future
* @return the future parts
+ * @deprecated use {@link #from(Content.Source, Attributes, String, MultiPartConfig)}.
*/
+ @Deprecated
public static CompletableFuture from(Attributes attributes, MultiPartCompliance compliance, ComplianceViolation.Listener listener, String boundary, Function> parse)
{
- @SuppressWarnings("unchecked")
- CompletableFuture futureParts = (CompletableFuture)attributes.getAttribute(MultiPartFormData.class.getName());
+ CompletableFuture futureParts = get(attributes);
if (futureParts == null)
{
futureParts = parse.apply(new Parser(boundary, compliance, listener));
@@ -108,6 +146,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 get(Attributes attributes)
+ {
+ return (CompletableFuture)attributes.getAttribute(MultiPartFormData.class.getName());
+ }
+
/**
* An ordered list of {@link MultiPart.Part}s that can
* be accessed by index or by name, or iterated over.
@@ -219,9 +269,9 @@ public class MultiPartFormData
{
private final PartsListener listener = new PartsListener();
private final MultiPart.Parser parser;
- private final MultiPartCompliance compliance;
- private final ComplianceViolation.Listener complianceListener;
- private boolean useFilesForPartsWithoutFileName;
+ private MultiPartCompliance compliance;
+ private ComplianceViolation.Listener complianceListener;
+ private boolean useFilesForPartsWithoutFileName = true;
private Path filesDirectory;
private long maxFileSize = -1;
private long maxMemoryFileSize;
@@ -234,6 +284,10 @@ public class MultiPartFormData
this(boundary, MultiPartCompliance.RFC7578, ComplianceViolation.Listener.NOOP);
}
+ /**
+ * @deprecated use {@link Parser#Parser(String)} with {@link #configure(MultiPartConfig)}.
+ */
+ @Deprecated
public Parser(String boundary, MultiPartCompliance multiPartCompliance, ComplianceViolation.Listener complianceViolationListener)
{
compliance = Objects.requireNonNull(multiPartCompliance);
@@ -429,6 +483,23 @@ public class MultiPartFormData
parser.setMaxParts(maxParts);
}
+ /**
+ * Configure the Parser given a {@link MultiPartConfig} instance.
+ * @param config the configuration.
+ */
+ public void configure(MultiPartConfig config)
+ {
+ parser.setMaxParts(config.getMaxParts());
+ maxMemoryFileSize = config.getMaxMemoryPartSize();
+ maxFileSize = config.getMaxPartSize();
+ maxLength = config.getMaxSize();
+ parser.setPartHeadersMaxLength(config.getMaxHeadersSize());
+ useFilesForPartsWithoutFileName = config.isUseFilesForPartsWithoutFileName();
+ filesDirectory = config.getLocation();
+ complianceListener = config.getViolationListener();
+ compliance = config.getMultiPartCompliance();
+ }
+
// Only used for testing.
int getPartsSize()
{
@@ -440,8 +511,7 @@ public class MultiPartFormData
private final AutoLock lock = new AutoLock();
private final List parts = new ArrayList<>();
private final List partChunks = new ArrayList<>();
- private long fileSize;
- private long memoryFileSize;
+ private long size;
private Path filePath;
private SeekableByteChannel fileChannel;
private Throwable failure;
@@ -450,22 +520,21 @@ public class MultiPartFormData
public void onPartContent(Content.Chunk chunk)
{
ByteBuffer buffer = chunk.getByteBuffer();
+ long maxPartSize = getMaxFileSize();
+ size += buffer.remaining();
+ if (maxPartSize >= 0 && size > maxPartSize)
+ {
+ onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxPartSize)));
+ return;
+ }
+
String fileName = getFileName();
if (fileName != null || isUseFilesForPartsWithoutFileName())
{
- long maxFileSize = getMaxFileSize();
- fileSize += buffer.remaining();
- if (maxFileSize >= 0 && fileSize > maxFileSize)
+ long maxMemoryPartSize = getMaxMemoryFileSize();
+ if (maxMemoryPartSize >= 0)
{
- onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxFileSize)));
- return;
- }
-
- long maxMemoryFileSize = getMaxMemoryFileSize();
- if (maxMemoryFileSize >= 0)
- {
- memoryFileSize += buffer.remaining();
- if (memoryFileSize > maxMemoryFileSize)
+ if (size > maxMemoryPartSize)
{
try
{
@@ -482,6 +551,11 @@ public class MultiPartFormData
{
write(c.getByteBuffer());
}
+ try (AutoLock ignored = lock.lock())
+ {
+ this.partChunks.forEach(Content.Chunk::release);
+ this.partChunks.clear();
+ }
}
write(buffer);
if (chunk.isLast())
@@ -491,16 +565,23 @@ public class MultiPartFormData
{
onFailure(x);
}
-
- try (AutoLock ignored = lock.lock())
- {
- partChunks.forEach(Content.Chunk::release);
- partChunks.clear();
- }
return;
}
}
}
+ else
+ {
+ long maxMemoryPartSize = getMaxMemoryFileSize();
+ if (maxMemoryPartSize >= 0)
+ {
+ if (size > maxMemoryPartSize)
+ {
+ onFailure(new IllegalStateException("max memory file size exceeded: %d".formatted(maxMemoryPartSize)));
+ return;
+ }
+ }
+ }
+
// Retain the chunk because it is stored for later use.
chunk.retain();
try (AutoLock ignored = lock.lock())
@@ -541,8 +622,7 @@ public class MultiPartFormData
@Override
public void onPart(String name, String fileName, HttpFields headers)
{
- fileSize = 0;
- memoryFileSize = 0;
+ size = 0;
try (AutoLock ignored = lock.lock())
{
// Content-Transfer-Encoding is not a multi-valued field.
diff --git a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartCaptureTest.java b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartCaptureTest.java
index 7469cb2af51..171b9831261 100644
--- a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartCaptureTest.java
+++ b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartCaptureTest.java
@@ -58,7 +58,7 @@ public class MultiPartCaptureTest
FS.ensureDirExists(tempDir);
MultiPartFormData.Parser parser = new MultiPartFormData.Parser(boundary);
- parser.setUseFilesForPartsWithoutFileName(false);
+ parser.setUseFilesForPartsWithoutFileName(true);
parser.setFilesDirectory(tempDir);
ByteBufferContentSource contentSource = new ByteBufferContentSource(formRequest.asByteBuffer());
MultiPartFormData.Parts parts = parser.parse(contentSource).get();
diff --git a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/internal/HTTP2Flusher.java b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/internal/HTTP2Flusher.java
index ddff64b2cec..73e95969371 100644
--- a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/internal/HTTP2Flusher.java
+++ b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/internal/HTTP2Flusher.java
@@ -113,7 +113,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
{
entries.offer(entry);
if (LOG.isDebugEnabled())
- LOG.debug("Appended {}, entries={}", entry, entries.size());
+ LOG.debug("Appended {}, entries={}, {}", entry, entries.size(), this);
}
}
if (closed == null)
@@ -132,7 +132,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
{
list.forEach(entries::offer);
if (LOG.isDebugEnabled())
- LOG.debug("Appended {}, entries={}", list, entries.size());
+ LOG.debug("Appended {}, entries={} {}", list, entries.size(), this);
}
}
if (closed == null)
@@ -161,7 +161,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
protected Action process() throws Throwable
{
if (LOG.isDebugEnabled())
- LOG.debug("Flushing {}", session);
+ LOG.debug("process {} {}", session, this);
try (AutoLock ignored = lock.lock())
{
@@ -184,7 +184,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
if (pendingEntries.isEmpty())
{
if (LOG.isDebugEnabled())
- LOG.debug("Flushed {}", session);
+ LOG.debug("Flushed {} {}", session, this);
return Action.IDLE;
}
@@ -257,7 +257,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
if (LOG.isDebugEnabled())
LOG.debug("Failure generating {}", entry, failure);
failed(failure);
- return Action.SUCCEEDED;
+ return Action.SCHEDULED;
}
}
diff --git a/jetty-core/jetty-maven/src/main/java/org/eclipse/jetty/maven/MavenResource.java b/jetty-core/jetty-maven/src/main/java/org/eclipse/jetty/maven/MavenResource.java
index 42475de50fd..aefe9e1d134 100644
--- a/jetty-core/jetty-maven/src/main/java/org/eclipse/jetty/maven/MavenResource.java
+++ b/jetty-core/jetty-maven/src/main/java/org/eclipse/jetty/maven/MavenResource.java
@@ -217,4 +217,10 @@ public class MavenResource extends Resource
return null;
return _resource.resolve(subUriPath);
}
+
+ @Override
+ public String toString()
+ {
+ return "(Maven) " + _resource.toString();
+ }
}
diff --git a/jetty-core/jetty-maven/src/main/java/org/eclipse/jetty/maven/SelectiveJarResource.java b/jetty-core/jetty-maven/src/main/java/org/eclipse/jetty/maven/SelectiveJarResource.java
index 8e8c44f983e..22079cc47b1 100644
--- a/jetty-core/jetty-maven/src/main/java/org/eclipse/jetty/maven/SelectiveJarResource.java
+++ b/jetty-core/jetty-maven/src/main/java/org/eclipse/jetty/maven/SelectiveJarResource.java
@@ -277,4 +277,10 @@ public class SelectiveJarResource extends Resource
}
}
}
+
+ @Override
+ public String toString()
+ {
+ return "(Selective Jar/Maven) " + _delegate.toString();
+ }
}
diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
index c3ee715df20..00904781c0f 100644
--- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
+++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
@@ -21,6 +21,7 @@ import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
+import java.nio.file.Path;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
@@ -34,6 +35,7 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
+import org.eclipse.jetty.http.ComplianceViolation;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
@@ -41,6 +43,8 @@ import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.http.MultiPartCompliance;
+import org.eclipse.jetty.http.MultiPartConfig;
import org.eclipse.jetty.http.Trailers;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.internal.CompletionStreamWrapper;
@@ -581,6 +585,36 @@ public interface Request extends Attributes, Content.Source
return CookieCache.getCookies(request);
}
+ /**
+ * Get a {@link MultiPartConfig.Builder} given a {@link Request} and a location.
+ *
+ * If the location is null this will extract the {@link Context} temp directory from the request.
+ * The {@code maxHeaderSize}, {@link MultiPartCompliance}, {@link ComplianceViolation.Listener}
+ * are also extracted from the request. Additional settings can be configured through the
+ * {@link MultiPartConfig.Builder} which is returned.
+ *
+ * @param request the request.
+ * @param location the temp directory location, or null to use the context default.
+ * @return a {@link MultiPartConfig} with settings extracted from the request.
+ */
+ static MultiPartConfig.Builder getMultiPartConfig(Request request, Path location)
+ {
+ 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.Builder()
+ .location(location)
+ .maxHeadersSize(maxHeaderSize)
+ .complianceMode(multiPartCompliance)
+ .violationListener(complianceViolationListener);
+ }
+
/**
* Generate a proper "Location" header for redirects.
*
diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
index 84ef89a15e3..1a237c236ad 100644
--- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
+++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
@@ -715,7 +715,14 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Alias
if (!Resources.isReadable(baseResource))
throw new IllegalArgumentException("Base Resource is not valid: " + baseResource);
if (baseResource.isAlias())
- LOG.warn("Base Resource should not be an alias");
+ {
+ URI realUri = baseResource.getRealURI();
+ if (realUri == null)
+ LOG.warn("Base Resource should not be an alias (100% of requests to context are subject to Security/Alias Checks): {}", baseResource);
+ else
+ LOG.warn("Base Resource should not be an alias (100% of requests to context are subject to Security/Alias Checks): {} points to {}",
+ baseResource, realUri.toASCIIString());
+ }
}
_availability.set(Availability.STARTING);
diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ReadWriteFailuresTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ReadWriteFailuresTest.java
index d10405241fd..f5650926ef6 100644
--- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ReadWriteFailuresTest.java
+++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ReadWriteFailuresTest.java
@@ -202,7 +202,7 @@ public class ReadWriteFailuresTest
POST / HTTP/1.1
Host: localhost
Content-Length: 1
-
+
""";
try (LocalConnector.LocalEndPoint endPoint = connector.executeRequest(request))
{
@@ -234,7 +234,7 @@ public class ReadWriteFailuresTest
POST / HTTP/1.1
Host: localhost
Content-Length: %d
-
+
%s
""".formatted(content.length(), content);
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request, 5, TimeUnit.SECONDS));
diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/EventsHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/EventsHandlerTest.java
index f61ef281838..0ecb573c398 100644
--- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/EventsHandlerTest.java
+++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/EventsHandlerTest.java
@@ -17,6 +17,7 @@ import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@@ -39,6 +40,7 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertTrue;
public class EventsHandlerTest
{
@@ -117,6 +119,7 @@ public class EventsHandlerTest
{
AtomicReference beginNanoTime = new AtomicReference<>();
AtomicReference readyNanoTime = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
EventsHandler eventsHandler = new EventsHandler(new EchoHandler())
{
@Override
@@ -124,6 +127,7 @@ public class EventsHandlerTest
{
beginNanoTime.set(request.getBeginNanoTime());
readyNanoTime.set(request.getHeadersNanoTime());
+ latch.countDown();
}
};
startServer(eventsHandler);
@@ -148,6 +152,7 @@ public class EventsHandlerTest
String response = endPoint.getResponse();
assertThat(response, containsString("HTTP/1.1 200 OK"));
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
assertThat(NanoTime.millisSince(beginNanoTime.get()), greaterThan(900L));
assertThat(NanoTime.millisSince(readyNanoTime.get()), greaterThan(450L));
}
diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/MultiPartFormDataHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/MultiPartFormDataHandlerTest.java
index 375642169d8..7233ff0ff2f 100644
--- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/MultiPartFormDataHandlerTest.java
+++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/MultiPartFormDataHandlerTest.java
@@ -78,8 +78,9 @@ public class MultiPartFormDataHandlerTest
public boolean handle(Request request, Response response, Callback callback)
{
String boundary = MultiPart.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE));
- new MultiPartFormData.Parser(boundary)
- .parse(request)
+ MultiPartFormData.Parser parser = new MultiPartFormData.Parser(boundary);
+ parser.setMaxMemoryFileSize(-1);
+ parser.parse(request)
.whenComplete((parts, failure) ->
{
if (parts != null)
@@ -128,8 +129,9 @@ public class MultiPartFormDataHandlerTest
{
String boundary = MultiPart.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE));
- new MultiPartFormData.Parser(boundary)
- .parse(request)
+ MultiPartFormData.Parser parser = new MultiPartFormData.Parser(boundary);
+ parser.setMaxMemoryFileSize(-1);
+ parser.parse(request)
.whenComplete((parts, failure) ->
{
if (parts != null)
diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ExceptionUtil.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ExceptionUtil.java
index e677da1c6d2..6de5827e71c 100644
--- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ExceptionUtil.java
+++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ExceptionUtil.java
@@ -303,58 +303,28 @@ public class ExceptionUtil
return t1;
}
- public static void callAndThen(Throwable cause, Consumer first, Consumer second)
+ /** Call a method that handles a {@link Throwable}, catching and associating any exception that it throws.
+ * @param cause The {@link Throwable} to pass to the consumer
+ * @param consumer The handler of a {@link Throwable}
+ */
+ public static void call(Throwable cause, Consumer consumer)
{
try
{
- first.accept(cause);
+ consumer.accept(cause);
}
catch (Throwable t)
{
- addSuppressedIfNotAssociated(cause, t);
- }
- finally
- {
- second.accept(cause);
- }
- }
-
- public static void callAndThen(Throwable cause, Consumer first, Runnable second)
- {
- try
- {
- first.accept(cause);
- }
- catch (Throwable t)
- {
- addSuppressedIfNotAssociated(cause, t);
- }
- finally
- {
- second.run();
- }
- }
-
- public static void callAndThen(Runnable first, Runnable second)
- {
- try
- {
- first.run();
- }
- catch (Throwable t)
- {
- // ignored
- }
- finally
- {
- second.run();
+ ExceptionUtil.addSuppressedIfNotAssociated(t, cause);
+ throw t;
}
}
/**
- * Call a {@link Invocable.Callable} and handle failures
- * @param callable The runnable to call
- * @param failure The handling of failures
+ * Call a {@link Invocable.Callable} and handle any resulting failures
+ * @param callable The {@link org.eclipse.jetty.util.thread.Invocable.Callable} to call
+ * @param failure A handler of failures from the call
+ * @see #run(Runnable, Consumer)
*/
public static void call(Invocable.Callable callable, Consumer failure)
{
@@ -380,6 +350,7 @@ public class ExceptionUtil
* Call a {@link Runnable} and handle failures
* @param runnable The runnable to call
* @param failure The handling of failures
+ * @see #call(Throwable, Consumer)
*/
public static void run(Runnable runnable, Consumer failure)
{
@@ -401,6 +372,71 @@ public class ExceptionUtil
}
}
+ /**
+ * Call a handler of {@link Throwable} and then always call another, suppressing any exceptions thrown.
+ * @param cause The {@link Throwable} to be passed to both consumers.
+ * @param call The first {@link Consumer} of {@link Throwable} to call.
+ * @param then The second {@link Consumer} of {@link Throwable} to call.
+ */
+ public static void callAndThen(Throwable cause, Consumer call, Consumer then)
+ {
+ try
+ {
+ call.accept(cause);
+ }
+ catch (Throwable t)
+ {
+ addSuppressedIfNotAssociated(cause, t);
+ }
+ finally
+ {
+ then.accept(cause);
+ }
+ }
+
+ /**
+ * Call a handler of {@link Throwable} and then always call a {@link Runnable}, suppressing any exceptions thrown.
+ * @param cause The {@link Throwable} to be passed to both consumers.
+ * @param call The {@link Consumer} of {@link Throwable} to call.
+ * @param then The {@link Runnable} to call.
+ */
+ public static void callAndThen(Throwable cause, Consumer call, Runnable then)
+ {
+ try
+ {
+ call.accept(cause);
+ }
+ catch (Throwable t)
+ {
+ addSuppressedIfNotAssociated(cause, t);
+ }
+ finally
+ {
+ then.run();
+ }
+ }
+
+ /**
+ * Call a {@link Runnable} and then always call another, ignoring any exceptions thrown.
+ * @param call The first {@link Runnable} to call.
+ * @param then The second {@link Runnable} to call.
+ */
+ public static void callAndThen(Runnable call, Runnable then)
+ {
+ try
+ {
+ call.run();
+ }
+ catch (Throwable t)
+ {
+ // ignored
+ }
+ finally
+ {
+ then.run();
+ }
+ }
+
/**
* Get from a {@link CompletableFuture} and convert any uncheck exceptions to {@link RuntimeException}.
* @param completableFuture The future to get from.
diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/MemoryResource.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/MemoryResource.java
index 31970e17cf1..5215218021c 100644
--- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/MemoryResource.java
+++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/MemoryResource.java
@@ -155,6 +155,6 @@ public class MemoryResource extends Resource
@Override
public String toString()
{
- return getName();
+ return "(Memory) " + _uri.toASCIIString();
}
}
diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java
index d88930e1d6c..b5fc9657b26 100644
--- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java
+++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java
@@ -460,4 +460,13 @@ public abstract class Resource implements Iterable
}
return false;
}
+
+ public String toString()
+ {
+ String str = getName();
+ URI uri = getURI();
+ if (uri != null)
+ str = getURI().toASCIIString();
+ return "(" + this.getClass().getSimpleName() + ") " + str;
+ }
}
diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java
index f186bb23c72..61db423d822 100644
--- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java
+++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java
@@ -308,7 +308,7 @@ public class URLResourceFactory implements ResourceFactory
@Override
public String toString()
{
- return String.format("URLResource@%X(%s)", this.uri.hashCode(), this.uri.toASCIIString());
+ return "(URL) " + this.uri.toASCIIString();
}
private static class InputStreamReference extends AtomicReference implements Runnable
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletMultiPartFormData.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletMultiPartFormData.java
index 31ef0e5f1b8..b27b355b2cb 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletMultiPartFormData.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletMultiPartFormData.java
@@ -26,12 +26,11 @@ import java.util.concurrent.CompletableFuture;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.Part;
-import org.eclipse.jetty.http.ComplianceViolation;
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.MultiPartCompliance;
+import org.eclipse.jetty.http.MultiPartConfig;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.ByteBufferPool;
@@ -39,7 +38,7 @@ import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.InputStreamContentSource;
import org.eclipse.jetty.server.ConnectionMetaData;
-import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.StringUtil;
/**
@@ -99,16 +98,13 @@ public class ServletMultiPartFormData
return CompletableFuture.failedFuture(new IllegalStateException("No core request"));
// Get a temporary directory for larger parts.
- File filesDirectory = StringUtil.isBlank(config.getLocation())
- ? servletContextRequest.getContext().getTempDirectory()
- : new File(config.getLocation());
-
- HttpChannel httpChannel = HttpChannel.from(servletContextRequest);
- ComplianceViolation.Listener complianceViolationListener = httpChannel.getComplianceViolationListener();
- MultiPartCompliance compliance = servletContextRequest.getConnectionMetaData().getHttpConfiguration().getMultiPartCompliance();
+ Path filesDirectory = StringUtil.isBlank(config.getLocation())
+ ? servletContextRequest.getContext().getTempDirectory().toPath()
+ : new File(config.getLocation()).toPath();
// Look for an existing future MultiPartFormData.Parts
- CompletableFuture futureFormData = MultiPartFormData.from(servletContextRequest, compliance, complianceViolationListener, boundary, parser ->
+ CompletableFuture futureFormData = MultiPartFormData.get(servletContextRequest);
+ if (futureFormData == null)
{
try
{
@@ -131,24 +127,24 @@ public class ServletMultiPartFormData
source = iscs;
}
- parser.setMaxParts(contextHandler.getMaxFormKeys());
- parser.setFilesDirectory(filesDirectory.toPath());
- parser.setMaxMemoryFileSize(config.getFileSizeThreshold());
- parser.setMaxFileSize(config.getMaxFileSize());
- parser.setMaxLength(config.getMaxRequestSize());
- parser.setPartHeadersMaxLength(connectionMetaData.getHttpConfiguration().getRequestHeaderSize());
+ MultiPartConfig multiPartConfig = Request.getMultiPartConfig(servletContextRequest, filesDirectory)
+ .location(filesDirectory)
+ .maxParts(contextHandler.getMaxFormKeys())
+ .maxMemoryPartSize(config.getFileSizeThreshold())
+ .maxPartSize(config.getMaxFileSize())
+ .maxSize(config.getMaxRequestSize())
+ .build();
- // parse the core parts.
- return parser.parse(source);
+ futureFormData = MultiPartFormData.from(source, servletContextRequest, contentType, multiPartConfig);
}
catch (Throwable failure)
{
return CompletableFuture.failedFuture(failure);
}
- });
+ }
// When available, convert the core parts to servlet parts
- futureServletParts = futureFormData.thenApply(formDataParts -> new Parts(filesDirectory.toPath(), formDataParts));
+ futureServletParts = futureFormData.thenApply(formDataParts -> new Parts(filesDirectory, formDataParts));
// cache the result in attributes.
servletRequest.setAttribute(ServletMultiPartFormData.class.getName(), futureServletParts);
diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java
index c47a171e89a..0e712e87ed2 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/MultiPartServletTest.java
@@ -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));
diff --git a/jetty-ee8/jetty-ee8-servlet/pom.xml b/jetty-ee8/jetty-ee8-servlet/pom.xml
index 7e2da26e363..c18edff7624 100644
--- a/jetty-ee8/jetty-ee8-servlet/pom.xml
+++ b/jetty-ee8/jetty-ee8-servlet/pom.xml
@@ -74,6 +74,8 @@
org.apache.maven.plugins
maven-surefire-plugin
+
+ false
@{argLine} ${jetty.surefire.argLine}
--add-modules org.eclipse.jetty.util.ajax
--add-reads org.eclipse.jetty.ee8.servlet=org.eclipse.jetty.logging
diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SessionHandler.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SessionHandler.java
index 5a1e9872a42..ef1f7579261 100644
--- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SessionHandler.java
+++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SessionHandler.java
@@ -47,7 +47,6 @@ import org.eclipse.jetty.server.HttpStream;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.session.AbstractSessionManager;
-import org.eclipse.jetty.session.DefaultSessionIdManager;
import org.eclipse.jetty.session.ManagedSession;
import org.eclipse.jetty.session.SessionCache;
import org.eclipse.jetty.session.SessionConfig;
diff --git a/jetty-ee9/jetty-ee9-servlet/pom.xml b/jetty-ee9/jetty-ee9-servlet/pom.xml
index 66bd2f795f8..3f00330ba1e 100644
--- a/jetty-ee9/jetty-ee9-servlet/pom.xml
+++ b/jetty-ee9/jetty-ee9-servlet/pom.xml
@@ -72,6 +72,8 @@
maven-surefire-plugin
+
+ false
@{argLine} ${jetty.surefire.argLine}
--add-modules org.eclipse.jetty.util.ajax
--add-reads org.eclipse.jetty.ee9.servlet=org.eclipse.jetty.logging
diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/CrossContextDispatcherTest.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/CrossContextDispatcherTest.java
index ad527a24364..50379d42850 100644
--- a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/CrossContextDispatcherTest.java
+++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/CrossContextDispatcherTest.java
@@ -18,7 +18,6 @@ import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -70,10 +69,8 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/DefaultServletTest.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/DefaultServletTest.java
index 7c157f58f26..f6eb2bbb39e 100644
--- a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/DefaultServletTest.java
+++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/DefaultServletTest.java
@@ -13,6 +13,8 @@
package org.eclipse.jetty.ee9.servlet;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
@@ -28,6 +30,8 @@ import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
@@ -59,10 +63,12 @@ import org.eclipse.jetty.toolchain.test.MavenPaths;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.resource.FileSystemPool;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
+import org.eclipse.jetty.util.resource.URLResourceFactory;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -2560,6 +2566,49 @@ public class DefaultServletTest
}
}
+ @Test
+ public void testGetPrecompressedSuffixMapping() throws Exception
+ {
+ Path docRoot = workDir.getEmptyPathDir().resolve("docroot");
+ FS.ensureDirExists(docRoot);
+
+ startServer((context) ->
+ {
+ ResourceFactory.registerResourceFactory("file", new URLResourceFactory());
+ Resource resource = ResourceFactory.of(context).newResource(docRoot);
+ assertThat("Expecting URLResource", resource.getClass().getName(), endsWith("URLResource"));
+ context.setBaseResource(resource);
+
+ ServletHolder defholder = context.addServlet(DefaultServlet.class, "*.js");
+ defholder.setInitParameter("cacheControl", "no-store");
+ defholder.setInitParameter("dirAllowed", "false");
+ defholder.setInitParameter("gzip", "false");
+ defholder.setInitParameter("precompressed", "gzip=.gz");
+ });
+
+
+ FS.ensureDirExists(docRoot.resolve("scripts"));
+
+ String scriptText = "This is a script";
+ Files.writeString(docRoot.resolve("scripts/script.js"), scriptText, UTF_8);
+
+ byte[] compressedBytes = compressGzip(scriptText);
+ Files.write(docRoot.resolve("scripts/script.js.gz"), compressedBytes);
+
+ String rawResponse = connector.getResponse("""
+ GET /context/scripts/script.js HTTP/1.1
+ Host: test
+ Accept-Encoding: gzip
+ Connection: close
+
+ """);
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat("Suffix url-pattern mapping not used", response.get(HttpHeader.CACHE_CONTROL), is("no-store"));
+ String responseDecompressed = decompressGzip(response.getContentBytes());
+ assertThat(responseDecompressed, is("This is a script"));
+ }
+
@Test
public void testHead() throws Exception
{
@@ -2950,6 +2999,31 @@ public class DefaultServletTest
return ret;
}
+ private static byte[] compressGzip(String textToCompress) throws IOException
+ {
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ GZIPOutputStream gzipOut = new GZIPOutputStream(baos);
+ ByteArrayInputStream input = new ByteArrayInputStream(textToCompress.getBytes(UTF_8)))
+ {
+ IO.copy(input, gzipOut);
+ gzipOut.flush();
+ gzipOut.finish();
+ return baos.toByteArray();
+ }
+ }
+
+ private static String decompressGzip(byte[] compressedContent) throws IOException
+ {
+ try (ByteArrayInputStream input = new ByteArrayInputStream(compressedContent);
+ GZIPInputStream gzipInput = new GZIPInputStream(input);
+ ByteArrayOutputStream output = new ByteArrayOutputStream())
+ {
+ IO.copy(gzipInput, output);
+ output.flush();
+ return output.toString(UTF_8);
+ }
+ }
+
public static class Scenarios extends ArrayList
{
public void addScenario(String description, String rawRequest, int expectedStatus)
diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/DispatcherTest.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/DispatcherTest.java
index 2b1abfdc82a..03bccebb041 100644
--- a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/DispatcherTest.java
+++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/DispatcherTest.java
@@ -66,8 +66,10 @@ import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.resource.FileSystemPool;
import org.hamcrest.Matcher;
import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
@@ -75,6 +77,7 @@ import org.slf4j.LoggerFactory;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -124,6 +127,12 @@ public class DispatcherTest
return _contextCollection;
}
+ @BeforeEach
+ public void ensureFileSystemPoolIsSane()
+ {
+ assertThat(FileSystemPool.INSTANCE.mounts(), empty());
+ }
+
@AfterEach
public void destroy() throws Exception
{
diff --git a/pom.xml b/pom.xml
index 8f6561c5a2f..bfe81f3b3fe 100644
--- a/pom.xml
+++ b/pom.xml
@@ -201,7 +201,7 @@
4.0.2
4.0.1
3.1.5
- 10.1.16
+ 10.1.25
2.0.1
5.1.2.Final
@@ -226,7 +226,7 @@
4.0.2
4.0.1
4.0.0-M1
- 11.0.0-M20
+ 11.0.0-M21
2.0.1
6.0.0.Beta1
@@ -240,7 +240,7 @@
1.0.0.v201108011116
1.2.5
4.0.6
- 9.0.83.1
+ 9.0.90
3.1.9.Final
2.0.1
@@ -285,9 +285,9 @@
5.4.0
7.1.1.Final
infinispan/server
- 15.0.1.Final-1
- 5.0.2.Final
- 15.0.1.Final
+ 15.0.5.Final
+ 5.0.4.Final
+ 15.0.5.Final
1.2
false
false
@@ -367,7 +367,7 @@
2.2.3
3.2.20
3.12.14
- 4.1.107.Final
+ 4.1.109.Final
0.9.1
8.1.0
8.0.0
@@ -407,7 +407,7 @@
3.0.0
2.16.2
1.7.0.Final
- 2.3.1.Final
+ 2.4.2.Final
2.4.8
@@ -1305,37 +1305,37 @@
org.wildfly.security
wildfly-elytron-sasl-digest
- 2.3.1.Final
+ ${wildfly.elytron.version}
org.wildfly.security
wildfly-elytron-sasl-external
- 2.3.1.Final
+ ${wildfly.elytron.version}
org.wildfly.security
wildfly-elytron-sasl-gs2
- 2.3.1.Final
+ ${wildfly.elytron.version}
org.wildfly.security
wildfly-elytron-sasl-gssapi
- 2.3.1.Final
+ ${wildfly.elytron.version}
org.wildfly.security
wildfly-elytron-sasl-oauth2
- 2.3.1.Final
+ ${wildfly.elytron.version}
org.wildfly.security
wildfly-elytron-sasl-plain
- 2.3.1.Final
+ ${wildfly.elytron.version}
org.wildfly.security
wildfly-elytron-sasl-scram
- 2.3.1.Final
+ ${wildfly.elytron.version}
diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/CoreMultiPartTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/CoreMultiPartTest.java
new file mode 100644
index 00000000000..844425ff5b9
--- /dev/null
+++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/CoreMultiPartTest.java
@@ -0,0 +1,559 @@
+//
+// ========================================================================
+// 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.MultiPartConfig;
+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.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 MultiPartConfig multiPartConfig(Path location, int maxFormKeys, long maxRequestSize, long maxFileSize, long fileSizeThreshold)
+ {
+ return new MultiPartConfig.Builder()
+ .location(location)
+ .maxParts(maxFormKeys)
+ .maxSize(maxRequestSize)
+ .maxPartSize(maxFileSize)
+ .maxMemoryPartSize(fileSizeThreshold)
+ .useFilesForPartsWithoutFileName(true)
+ .build();
+ }
+
+ private void start(Handler handler, MultiPartConfig config) throws Exception
+ {
+ this.config = config == null ? 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
+ {
+ String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
+ return MultiPartFormData.from(request, request, contentType, 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;
+ }
+ }, 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;
+ }
+ }, 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;
+ }
+ }, 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 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 = Request.getMultiPartConfig(request, config.getLocation())
+ .maxParts(config.getMaxParts())
+ .maxSize(config.getMaxSize())
+ .maxPartSize(config.getMaxPartSize())
+ .maxMemoryPartSize(config.getMaxMemoryPartSize())
+ .useFilesForPartsWithoutFileName(config.isUseFilesForPartsWithoutFileName())
+ .build();
+ 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;
+ }
+ }, 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"));
+ }
+}