violations)
+ {
+ return new UriCompliance("CUSTOM" + __custom.getAndIncrement(), violations);
+ }
+
/**
* Create compliance set from string.
*
@@ -151,22 +179,23 @@ public final class UriCompliance implements ComplianceViolation.Mode
*/
public static UriCompliance from(String spec)
{
- Set sections;
+ Set violations;
String[] elements = spec.split("\\s*,\\s*");
switch (elements[0])
{
case "0":
- sections = noneOf(Violation.class);
+ violations = noneOf(Violation.class);
break;
case "*":
- sections = allOf(Violation.class);
+ violations = allOf(Violation.class);
break;
default:
{
UriCompliance mode = UriCompliance.valueOf(elements[0]);
- sections = (mode == null) ? noneOf(Violation.class) : copyOf(mode.getAllowed());
+ violations = (mode == null) ? noneOf(Violation.class) : copyOf(mode.getAllowed());
+ break;
}
}
@@ -178,12 +207,12 @@ public final class UriCompliance implements ComplianceViolation.Mode
element = element.substring(1);
Violation section = Violation.valueOf(element);
if (exclude)
- sections.remove(section);
+ violations.remove(section);
else
- sections.add(section);
+ violations.add(section);
}
- UriCompliance compliance = new UriCompliance("CUSTOM" + __custom.getAndIncrement(), sections);
+ UriCompliance compliance = new UriCompliance("CUSTOM" + __custom.getAndIncrement(), violations);
if (LOG.isDebugEnabled())
LOG.debug("UriCompliance from {}->{}", spec, compliance);
return compliance;
@@ -192,7 +221,7 @@ public final class UriCompliance implements ComplianceViolation.Mode
private final String _name;
private final Set _allowed;
- private UriCompliance(String name, Set violations)
+ public UriCompliance(String name, Set violations)
{
Objects.requireNonNull(violations);
_name = name;
diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/SyntaxTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/SyntaxTest.java
index a31995cbd24..7e944e38f55 100644
--- a/jetty-http/src/test/java/org/eclipse/jetty/http/SyntaxTest.java
+++ b/jetty-http/src/test/java/org/eclipse/jetty/http/SyntaxTest.java
@@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
public class SyntaxTest
@@ -61,17 +62,11 @@ public class SyntaxTest
for (String token : tokens)
{
- try
- {
- Syntax.requireValidRFC2616Token(token, "Test Based");
- fail("RFC2616 Token [" + token + "] Should have thrown " + IllegalArgumentException.class.getName());
- }
- catch (IllegalArgumentException e)
- {
- assertThat("Testing Bad RFC2616 Token [" + token + "]", e.getMessage(),
+ Throwable e = assertThrows(IllegalArgumentException.class,
+ () -> Syntax.requireValidRFC2616Token(token, "Test Based"));
+ assertThat("Testing Bad RFC2616 Token [" + token + "]", e.getMessage(),
allOf(containsString("Test Based"),
- containsString("RFC2616")));
- }
+ containsString("RFC2616")));
}
}
diff --git a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/SmallThreadPoolLoadTest.java b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/SmallThreadPoolLoadTest.java
index 74b8bdf0a2f..cba850eac9a 100644
--- a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/SmallThreadPoolLoadTest.java
+++ b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/SmallThreadPoolLoadTest.java
@@ -44,7 +44,6 @@ import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.Scheduler;
import org.hamcrest.Matchers;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -52,7 +51,6 @@ import org.slf4j.LoggerFactory;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
-@Disabled
public class SmallThreadPoolLoadTest extends AbstractTest
{
private final Logger logger = LoggerFactory.getLogger(SmallThreadPoolLoadTest.class);
@@ -83,8 +81,8 @@ public class SmallThreadPoolLoadTest extends AbstractTest
boolean result = IntStream.range(0, 16).parallel()
.mapToObj(i -> IntStream.range(0, runs)
.mapToObj(j -> run(session, iterations))
- .reduce(true, (acc, res) -> acc && res))
- .reduce(true, (acc, res) -> acc && res);
+ .reduce(true, Boolean::logicalAnd))
+ .reduce(true, Boolean::logicalAnd);
assertTrue(result);
}
@@ -94,10 +92,10 @@ public class SmallThreadPoolLoadTest extends AbstractTest
try
{
CountDownLatch latch = new CountDownLatch(iterations);
- int factor = (logger.isDebugEnabled() ? 25 : 1) * 100;
+ long factor = (logger.isDebugEnabled() ? 25 : 1) * 100;
// Dumps the state of the client if the test takes too long.
- final Thread testThread = Thread.currentThread();
+ Thread testThread = Thread.currentThread();
Scheduler.Task task = client.getScheduler().schedule(() ->
{
logger.warn("Interrupting test, it is taking too long{}Server:{}{}{}Client:{}{}",
@@ -123,7 +121,7 @@ public class SmallThreadPoolLoadTest extends AbstractTest
logger.info("{} requests in {} ms, {}/{} success/failure, {} req/s",
iterations, elapsed,
successes, iterations - successes,
- elapsed > 0 ? iterations * 1000 / elapsed : -1);
+ elapsed > 0 ? iterations * 1000L / elapsed : -1);
return true;
}
catch (Exception x)
diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java
index fd672e39912..9bd94727f72 100644
--- a/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java
+++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java
@@ -42,6 +42,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint
static final Logger LOG = LoggerFactory.getLogger(ByteArrayEndPoint.class);
static final InetAddress NOIP;
static final InetSocketAddress NOIPPORT;
+ private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 1024;
static
{
@@ -67,6 +68,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint
private final AutoLock _lock = new AutoLock();
private final Condition _hasOutput = _lock.newCondition();
private final Queue _inQ = new ArrayDeque<>();
+ private final int _outputSize;
private ByteBuffer _out;
private boolean _growOutput;
@@ -113,7 +115,8 @@ public class ByteArrayEndPoint extends AbstractEndPoint
super(timer);
if (BufferUtil.hasContent(input))
addInput(input);
- _out = output == null ? BufferUtil.allocate(1024) : output;
+ _outputSize = (output == null) ? 1024 : output.capacity();
+ _out = output == null ? BufferUtil.allocate(_outputSize) : output;
setIdleTimeout(idleTimeoutMs);
onOpen();
}
@@ -290,7 +293,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint
try (AutoLock lock = _lock.lock())
{
b = _out;
- _out = BufferUtil.allocate(b.capacity());
+ _out = BufferUtil.allocate(_outputSize);
}
getWriteFlusher().completeWrite();
return b;
@@ -316,7 +319,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint
return null;
}
b = _out;
- _out = BufferUtil.allocate(b.capacity());
+ _out = BufferUtil.allocate(_outputSize);
}
getWriteFlusher().completeWrite();
return b;
@@ -424,9 +427,14 @@ public class ByteArrayEndPoint extends AbstractEndPoint
BufferUtil.compact(_out);
if (b.remaining() > BufferUtil.space(_out))
{
- ByteBuffer n = BufferUtil.allocate(_out.capacity() + b.remaining() * 2);
- BufferUtil.append(n, _out);
- _out = n;
+ // Don't grow larger than MAX_BUFFER_SIZE to avoid memory issues.
+ if (_out.capacity() < MAX_BUFFER_SIZE)
+ {
+ long newBufferCapacity = Math.min((long)(_out.capacity() + b.remaining() * 1.5), MAX_BUFFER_SIZE);
+ ByteBuffer n = BufferUtil.allocate(Math.toIntExact(newBufferCapacity));
+ BufferUtil.append(n, _out);
+ _out = n;
+ }
}
}
diff --git a/jetty-jspc-maven-plugin/src/main/java/org/eclipse/jetty/jspc/plugin/JspcMojo.java b/jetty-jspc-maven-plugin/src/main/java/org/eclipse/jetty/jspc/plugin/JspcMojo.java
index 50d9fd01031..0353f10929c 100644
--- a/jetty-jspc-maven-plugin/src/main/java/org/eclipse/jetty/jspc/plugin/JspcMojo.java
+++ b/jetty-jspc-maven-plugin/src/main/java/org/eclipse/jetty/jspc/plugin/JspcMojo.java
@@ -83,6 +83,7 @@ public class JspcMojo extends AbstractMojo
{
private boolean scanAll;
+ private boolean scanManifest;
public void setClassLoader(ClassLoader loader)
{
@@ -99,6 +100,16 @@ public class JspcMojo extends AbstractMojo
return this.scanAll;
}
+ public void setScanManifest(boolean scanManifest)
+ {
+ this.scanManifest = scanManifest;
+ }
+
+ public boolean getScanManifest()
+ {
+ return this.scanManifest;
+ }
+
@Override
protected TldScanner newTldScanner(JspCServletContext context, boolean namespaceAware, boolean validate, boolean blockExternal)
{
@@ -106,6 +117,7 @@ public class JspcMojo extends AbstractMojo
{
StandardJarScanner jarScanner = new StandardJarScanner();
jarScanner.setScanAllDirectories(getScanAllDirectories());
+ jarScanner.setScanManifest(getScanManifest());
context.setAttribute(JarScanner.class.getName(), jarScanner);
}
@@ -243,6 +255,13 @@ public class JspcMojo extends AbstractMojo
@Parameter(defaultValue = "true")
private boolean scanAllDirectories;
+ /**
+ * Determines if the manifest of JAR files found on the classpath should be scanned.
+ * True by default.
+ */
+ @Parameter(defaultValue = "true")
+ private boolean scanManifest;
+
@Override
public void execute() throws MojoExecutionException, MojoFailureException
{
@@ -319,6 +338,7 @@ public class JspcMojo extends AbstractMojo
jspc.setOutputDir(generatedClasses);
jspc.setClassLoader(fakeWebAppClassLoader);
jspc.setScanAllDirectories(scanAllDirectories);
+ jspc.setScanManifest(scanManifest);
jspc.setCompile(true);
if (sourceVersion != null)
jspc.setCompilerSourceVM(sourceVersion);
diff --git a/jetty-maven-plugin/src/it/it-parent-pom/pom.xml b/jetty-maven-plugin/src/it/it-parent-pom/pom.xml
index 443f71b8ebb..72cac69f64b 100644
--- a/jetty-maven-plugin/src/it/it-parent-pom/pom.xml
+++ b/jetty-maven-plugin/src/it/it-parent-pom/pom.xml
@@ -17,7 +17,7 @@
commons-io
commons-io
- 2.6
+ 2.7
org.eclipse.jetty.toolchain
diff --git a/jetty-runner/pom.xml b/jetty-runner/pom.xml
index 753e6c4a5a8..5589cc85ba7 100644
--- a/jetty-runner/pom.xml
+++ b/jetty-runner/pom.xml
@@ -50,6 +50,17 @@
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ test-jar
+
+
+
+
org.apache.maven.plugins
maven-invoker-plugin
@@ -65,8 +76,12 @@
+ ${it.debug}
+ true
${maven.dependency.plugin.version}
+ ${maven.surefire.version}
+ ${hamcrest.version}
clean
@@ -149,5 +164,18 @@
jetty-slf4j-impl
runtime
+
+ org.eclipse.jetty.demos
+ demo-simple-webapp
+ ${project.version}
+ war
+ test
+
+
+ org.eclipse.jetty
+ jetty-client
+ ${project.version}
+ test
+
diff --git a/jetty-runner/src/it/demo-simple-webapp-runner-with-path/invoker.properties b/jetty-runner/src/it/demo-simple-webapp-runner-with-path/invoker.properties
new file mode 100644
index 00000000000..fd18ebccf10
--- /dev/null
+++ b/jetty-runner/src/it/demo-simple-webapp-runner-with-path/invoker.properties
@@ -0,0 +1 @@
+invoker.goals = test
diff --git a/jetty-runner/src/it/demo-simple-webapp-runner-with-path/pom.xml b/jetty-runner/src/it/demo-simple-webapp-runner-with-path/pom.xml
new file mode 100644
index 00000000000..44b419977fb
--- /dev/null
+++ b/jetty-runner/src/it/demo-simple-webapp-runner-with-path/pom.xml
@@ -0,0 +1,139 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.jetty.its
+ jetty-runner-it-test-demo-simple-webapp
+ 1.0.0-SNAPSHOT
+ war
+
+
+ UTF-8
+
+
+
+
+
+ org.eclipse.jetty
+ jetty-runner
+ @project.version@
+
+
+ org.eclipse.jetty
+ jetty-runner
+ @project.version@
+ tests
+ test-jar
+ test
+
+
+ org.eclipse.jetty.demos
+ demo-simple-webapp
+ @project.version@
+ war
+
+
+ org.eclipse.jetty
+ jetty-client
+ @project.version@
+ test
+
+
+ org.hamcrest
+ hamcrest-core
+ @hamcrest.version@
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ @maven.dependency.plugin.version@
+
+
+ copy-jetty-runner
+ generate-resources
+
+ copy
+
+
+
+
+ org.eclipse.jetty
+ jetty-runner
+ @project.version@
+ jar
+ false
+ ${project.build.directory}/
+ jetty-runner.jar
+
+
+ org.eclipse.jetty.demos
+ demo-simple-webapp
+ @project.version@
+ war
+ false
+ ${project.build.directory}
+ demo-simple-webapp.war
+
+
+ false
+ true
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ @maven.exec.plugin.version@
+
+
+
+ exec
+
+ generate-test-resources
+
+ ${project.build.directory}/jetty-runner.log
+ true
+ ${java.home}/bin/java
+
+ -jar
+ ${project.build.directory}/jetty-runner.jar
+ --out
+ ${project.build.directory}/jetty-runner.out
+ --port
+ 0
+ --path
+ french-chocolate-rocks
+ --server-uri-file
+ ${project.build.directory}/server-uri.txt
+ ${project.build.directory}/demo-simple-webapp.war
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ @maven.surefire.version@
+
+
+ IntegrationTest*.java
+
+
+
+
+ org.eclipse.jetty:jetty-runner
+
+
+
+
+
+
diff --git a/jetty-runner/src/it/demo-simple-webapp-runner/invoker.properties b/jetty-runner/src/it/demo-simple-webapp-runner/invoker.properties
new file mode 100644
index 00000000000..fd18ebccf10
--- /dev/null
+++ b/jetty-runner/src/it/demo-simple-webapp-runner/invoker.properties
@@ -0,0 +1 @@
+invoker.goals = test
diff --git a/jetty-runner/src/it/demo-simple-webapp-runner/pom.xml b/jetty-runner/src/it/demo-simple-webapp-runner/pom.xml
new file mode 100644
index 00000000000..df7e6d8e0dc
--- /dev/null
+++ b/jetty-runner/src/it/demo-simple-webapp-runner/pom.xml
@@ -0,0 +1,139 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.jetty.its
+ jetty-runner-it-test-demo-simple-webapp
+ 1.0.0-SNAPSHOT
+ war
+
+
+ UTF-8
+
+
+
+
+
+ org.eclipse.jetty
+ jetty-runner
+ @project.version@
+
+
+ org.eclipse.jetty
+ jetty-runner
+ @project.version@
+ tests
+ test-jar
+ test
+
+
+ org.eclipse.jetty.demos
+ demo-simple-webapp
+ @project.version@
+ war
+
+
+ org.eclipse.jetty
+ jetty-client
+ @project.version@
+ test
+
+
+ org.hamcrest
+ hamcrest-core
+ @hamcrest.version@
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ @maven.dependency.plugin.version@
+
+
+ copy-jetty-runner
+ generate-resources
+
+ copy
+
+
+
+
+ org.eclipse.jetty
+ jetty-runner
+ @project.version@
+ jar
+ false
+ ${project.build.directory}/
+ jetty-runner.jar
+
+
+ org.eclipse.jetty.demos
+ demo-simple-webapp
+ @project.version@
+ war
+ false
+ ${project.build.directory}
+ demo-simple-webapp.war
+
+
+ false
+ true
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ @maven.exec.plugin.version@
+
+
+
+ exec
+
+ generate-test-resources
+
+ ${project.build.directory}/jetty-runner.log
+ true
+ ${java.home}/bin/java
+
+
+
+ -jar
+ ${project.build.directory}/jetty-runner.jar
+ --out
+ ${project.build.directory}/jetty-runner.out
+ --port
+ 0
+ --server-uri-file
+ ${project.build.directory}/server-uri.txt
+ ${project.build.directory}/demo-simple-webapp.war
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ @maven.surefire.version@
+
+
+ IntegrationTest*.java
+
+
+
+
+ org.eclipse.jetty:jetty-runner
+
+
+
+
+
+
diff --git a/jetty-runner/src/main/java/org/eclipse/jetty/runner/Runner.java b/jetty-runner/src/main/java/org/eclipse/jetty/runner/Runner.java
index ebaf15d51f8..abc00a97639 100644
--- a/jetty-runner/src/main/java/org/eclipse/jetty/runner/Runner.java
+++ b/jetty-runner/src/main/java/org/eclipse/jetty/runner/Runner.java
@@ -19,6 +19,9 @@ import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -78,7 +81,9 @@ public class Runner
org.eclipse.jetty.plus.webapp.EnvConfiguration.class.getCanonicalName(),
org.eclipse.jetty.plus.webapp.PlusConfiguration.class.getCanonicalName(),
org.eclipse.jetty.annotations.AnnotationConfiguration.class.getCanonicalName(),
- org.eclipse.jetty.webapp.JettyWebXmlConfiguration.class.getCanonicalName()
+ org.eclipse.jetty.webapp.JettyWebXmlConfiguration.class.getCanonicalName(),
+ org.eclipse.jetty.webapp.WebAppConfiguration.class.getCanonicalName(),
+ org.eclipse.jetty.webapp.JspConfiguration.class.getCanonicalName()
};
public static final String CONTAINER_INCLUDE_JAR_PATTERN = ".*/jetty-runner-[^/]*\\.jar$";
public static final String DEFAULT_CONTEXT_PATH = "/";
@@ -92,6 +97,7 @@ public class Runner
protected ArrayList _configFiles;
protected boolean _enableStats = false;
protected String _statsPropFile;
+ protected String _serverUriFile;
/**
* Classpath
@@ -164,6 +170,7 @@ public class Runner
System.err.println(" --out file - info/warn/debug log filename (with optional 'yyyy_mm_dd' wildcard");
System.err.println(" --host name|ip - interface to listen on (default is all interfaces)");
System.err.println(" --port n - port to listen on (default 8080)");
+ System.err.println(" --server-uri-file path - file to write a single line with server base URI");
System.err.println(" --stop-port n - port to listen for stop command (or -DSTOP.PORT=n)");
System.err.println(" --stop-key n - security string for stop command (required if --stop-port is present) (or -DSTOP.KEY=n)");
System.err.println(" [--jar file]*n - each tuple specifies an extra jar to be added to the classloader");
@@ -293,6 +300,9 @@ public class Runner
_statsPropFile = args[++i];
_statsPropFile = ("unsecure".equalsIgnoreCase(_statsPropFile) ? null : _statsPropFile);
break;
+ case "--server-uri-file":
+ _serverUriFile = args[++i];
+ break;
default:
// process system property type argument so users can use in second args part
if (args[i].startsWith("-D"))
@@ -310,7 +320,7 @@ public class Runner
}
}
-// process contexts
+ // process contexts
if (!runnerServerInitialized) // log handlers not registered, server maybe not created, etc
{
@@ -334,7 +344,7 @@ public class Runner
}
//check that everything got configured, and if not, make the handlers
- HandlerCollection handlers = (HandlerCollection)_server.getChildHandlerByClass(HandlerCollection.class);
+ HandlerCollection handlers = _server.getChildHandlerByClass(HandlerCollection.class);
if (handlers == null)
{
handlers = new HandlerList();
@@ -342,7 +352,7 @@ public class Runner
}
//check if contexts already configured
- _contexts = (ContextHandlerCollection)handlers.getChildHandlerByClass(ContextHandlerCollection.class);
+ _contexts = handlers.getChildHandlerByClass(ContextHandlerCollection.class);
if (_contexts == null)
{
_contexts = new ContextHandlerCollection();
@@ -519,6 +529,13 @@ public class Runner
public void run() throws Exception
{
_server.start();
+ if (_serverUriFile != null)
+ {
+ Path fileWithPort = Paths.get(_serverUriFile);
+ Files.deleteIfExists(fileWithPort);
+ String serverUri = _server.getURI().toString();
+ Files.writeString(fileWithPort, serverUri);
+ }
_server.join();
}
diff --git a/jetty-runner/src/test/java/org/eclipse/jetty/maven/jettyrunner/it/IntegrationTestJettyRunner.java b/jetty-runner/src/test/java/org/eclipse/jetty/maven/jettyrunner/it/IntegrationTestJettyRunner.java
new file mode 100644
index 00000000000..e505e78cbc1
--- /dev/null
+++ b/jetty-runner/src/test/java/org/eclipse/jetty/maven/jettyrunner/it/IntegrationTestJettyRunner.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 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.maven.jettyrunner.it;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class IntegrationTestJettyRunner
+{
+ @Test
+ public void testGet() throws Exception
+ {
+ String serverUri = findServerUri();
+ HttpClient httpClient = new HttpClient();
+ try
+ {
+ httpClient.start();
+ ContentResponse response = httpClient.newRequest(serverUri).send();
+ String res = response.getContentAsString();
+ assertThat(res, Matchers.containsString("Hello World!"));
+ }
+ finally
+ {
+ httpClient.stop();
+ }
+ }
+
+ private String findServerUri() throws Exception
+ {
+ long now = System.currentTimeMillis();
+
+ while (System.currentTimeMillis() - now < MINUTES.toMillis(2))
+ {
+ Path portTxt = Paths.get("target", "server-uri.txt");
+ if (Files.exists(portTxt))
+ {
+ List lines = Files.readAllLines(portTxt);
+ return lines.get(0);
+ }
+ }
+
+ throw new Exception("cannot find started Jetty");
+ }
+
+}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContentProducer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContentProducer.java
index 72202727e9f..c8484963f6b 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContentProducer.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContentProducer.java
@@ -13,6 +13,7 @@
package org.eclipse.jetty.server;
+import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
@@ -302,12 +303,16 @@ class AsyncContentProducer implements ContentProducer
// In case the _rawContent was set by consumeAll(), check the httpChannel
// to see if it has a more precise error. Otherwise, the exact same
- // special content will be returned by the httpChannel.
- HttpInput.Content refreshedRawContent = produceRawContent();
- if (refreshedRawContent != null)
- _rawContent = refreshedRawContent;
+ // special content will be returned by the httpChannel; do not do that
+ // if the _error flag was set, meaning the current error is definitive.
+ if (!_error)
+ {
+ HttpInput.Content refreshedRawContent = produceRawContent();
+ if (refreshedRawContent != null)
+ _rawContent = refreshedRawContent;
+ _error = _rawContent.getError() != null;
+ }
- _error = _rawContent.getError() != null;
if (LOG.isDebugEnabled())
LOG.debug("raw content is special (with error = {}), returning it {}", _error, this);
return _rawContent;
@@ -317,7 +322,9 @@ class AsyncContentProducer implements ContentProducer
{
if (LOG.isDebugEnabled())
LOG.debug("using interceptor to transform raw content {}", this);
- _transformedContent = _interceptor.readFrom(_rawContent);
+ _transformedContent = intercept();
+ if (_error)
+ return _rawContent;
}
else
{
@@ -369,6 +376,26 @@ class AsyncContentProducer implements ContentProducer
return _transformedContent;
}
+ private HttpInput.Content intercept()
+ {
+ try
+ {
+ return _interceptor.readFrom(_rawContent);
+ }
+ catch (Throwable x)
+ {
+ IOException failure = new IOException("Bad content", x);
+ failCurrentContent(failure);
+ // Set the _error flag to mark the error as definitive, i.e.:
+ // do not try to produce new raw content to get a fresher error.
+ _error = true;
+ Response response = _httpChannel.getResponse();
+ if (response.isCommitted())
+ _httpChannel.abort(failure);
+ return null;
+ }
+ }
+
private HttpInput.Content produceRawContent()
{
HttpInput.Content content = _httpChannel.produceContent();
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java b/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java
index 33db3e00871..9cfd28e557c 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java
@@ -22,6 +22,7 @@ import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
+import java.util.function.BiPredicate;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -279,6 +280,7 @@ public class CustomRequestLog extends ContainerLifeCycle implements RequestLog
private final String _formatString;
private transient PathMappings _ignorePathMap;
private String[] _ignorePaths;
+ private BiPredicate _filter;
public CustomRequestLog()
{
@@ -311,6 +313,16 @@ public class CustomRequestLog extends ContainerLifeCycle implements RequestLog
}
}
+ /**
+ * This allows you to set a custom filter to decide whether to log a request or omit it from the request log.
+ * This filter is evaluated after path filtering is applied from {@link #setIgnorePaths(String[])}.
+ * @param filter - a BiPredicate which returns true if this request should be logged.
+ */
+ public void setFilter(BiPredicate filter)
+ {
+ _filter = filter;
+ }
+
@ManagedAttribute("The RequestLogWriter")
public RequestLog.Writer getWriter()
{
@@ -325,11 +337,14 @@ public class CustomRequestLog extends ContainerLifeCycle implements RequestLog
@Override
public void log(Request request, Response response)
{
+ if (_ignorePathMap != null && _ignorePathMap.getMatch(request.getRequestURI()) != null)
+ return;
+
+ if (_filter != null && !_filter.test(request, response))
+ return;
+
try
{
- if (_ignorePathMap != null && _ignorePathMap.getMatch(request.getRequestURI()) != null)
- return;
-
StringBuilder sb = _buffers.get();
sb.setLength(0);
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java
index 824b1b21676..728214a7572 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java
@@ -263,9 +263,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
{
// Fill the request buffer (if needed).
int filled = fillRequestBuffer();
- if (filled > 0)
- bytesIn.add(filled);
- else if (filled == -1 && getEndPoint().isOutputShutdown())
+ if (filled < 0 && getEndPoint().isOutputShutdown())
close();
// Parse the request buffer.
@@ -300,6 +298,14 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
}
}
}
+ catch (Throwable x)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} caught exception {}", this, _channel.getState(), x);
+ BufferUtil.clear(_requestBuffer);
+ releaseRequestBuffer();
+ getEndPoint().close(x);
+ }
finally
{
setCurrentConnection(last);
@@ -333,10 +339,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
private int fillRequestBuffer()
{
if (_contentBufferReferences.get() > 0)
- {
- LOG.warn("{} fill with unconsumed content!", this);
- return 0;
- }
+ throw new IllegalStateException("fill with unconsumed content on " + this);
if (BufferUtil.isEmpty(_requestBuffer))
{
@@ -352,8 +355,9 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
if (filled == 0) // Do a retry on fill 0 (optimization for SSL connections)
filled = getEndPoint().fill(_requestBuffer);
- // tell parser
- if (filled < 0)
+ if (filled > 0)
+ bytesIn.add(filled);
+ else if (filled < 0)
_parser.atEOF();
if (LOG.isDebugEnabled())
@@ -363,7 +367,8 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
}
catch (IOException e)
{
- LOG.debug("Unable to fill from endpoint {}", getEndPoint(), e);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Unable to fill from endpoint {}", getEndPoint(), e);
_parser.atEOF();
return -1;
}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java
index 7baeffa8442..65a182b24fd 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java
@@ -23,7 +23,6 @@ import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
-import java.util.ResourceBundle;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
import javax.servlet.RequestDispatcher;
@@ -627,6 +626,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
catch (Throwable t)
{
onWriteComplete(true, t);
+ throw t;
}
}
}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
index 27bf9d2e3b7..95866fb9910 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
@@ -67,7 +67,6 @@ import javax.servlet.http.WebConnection;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.ComplianceViolation;
import org.eclipse.jetty.http.HostPortHttpField;
-import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookie.SetCookieHttpField;
import org.eclipse.jetty.http.HttpField;
@@ -1692,10 +1691,11 @@ public class Request implements HttpServletRequest
_httpFields = request.getFields();
final HttpURI uri = request.getURI();
+ UriCompliance compliance = null;
boolean ambiguous = uri.isAmbiguous();
if (ambiguous)
{
- UriCompliance compliance = _channel == null || _channel.getHttpConfiguration() == null ? null : _channel.getHttpConfiguration().getUriCompliance();
+ compliance = _channel == null || _channel.getHttpConfiguration() == null ? null : _channel.getHttpConfiguration().getUriCompliance();
if (uri.hasAmbiguousSegment() && (compliance == null || !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_SEGMENT)))
throw new BadMessageException("Ambiguous segment in URI");
if (uri.hasAmbiguousSeparator() && (compliance == null || !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_SEPARATOR)))
@@ -1746,9 +1746,9 @@ public class Request implements HttpServletRequest
path = (encoded.length() == 1) ? "/" : _uri.getDecodedPath();
// Strictly speaking if a URI is legal and encodes ambiguous segments, then they should be
// reflected in the decoded string version. However, it can be ambiguous to provide a decoded path as
- // a string, so we normalize again. If an application wishes to see ambiguous URIs, then they can look
- // at the encoded form of the URI
- if (ambiguous)
+ // a string, so we normalize again. If an application wishes to see ambiguous URIs, then they must
+ // set the {@link UriCompliance.Violation#NON_CANONICAL_AMBIGUOUS_PATHS} compliance.
+ if (ambiguous && (compliance == null || !compliance.allows(UriCompliance.Violation.NON_CANONICAL_AMBIGUOUS_PATHS)))
path = URIUtil.canonicalPath(path);
}
else if ("*".equals(encoded) || HttpMethod.CONNECT.is(getMethod()))
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java
index 95fb38c85a3..78274594816 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java
@@ -15,14 +15,15 @@ package org.eclipse.jetty.server.handler;
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
import java.util.Queue;
-import java.util.concurrent.ConcurrentLinkedQueue;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.pathmap.PathSpecSet;
import org.eclipse.jetty.server.HttpChannel;
@@ -39,16 +40,17 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
- * Buffered Response Handler
*
* A Handler that can apply a {@link org.eclipse.jetty.server.HttpOutput.Interceptor}
* mechanism to buffer the entire response content until the output is closed.
* This allows the commit to be delayed until the response is complete and thus
* headers and response status can be changed while writing the body.
+ *
*
* Note that the decision to buffer is influenced by the headers and status at the
* first write, and thus subsequent changes to those headers will not influence the
* decision to buffer or not.
+ *
*
* Note also that there are no memory limits to the size of the buffer, thus
* this handler can represent an unbounded memory commitment if the content
@@ -57,7 +59,7 @@ import org.slf4j.LoggerFactory;
*/
public class BufferedResponseHandler extends HandlerWrapper
{
- static final Logger LOG = LoggerFactory.getLogger(BufferedResponseHandler.class);
+ private static final Logger LOG = LoggerFactory.getLogger(BufferedResponseHandler.class);
private final IncludeExclude _methods = new IncludeExclude<>();
private final IncludeExclude _paths = new IncludeExclude<>(PathSpecSet.class);
@@ -65,10 +67,7 @@ public class BufferedResponseHandler extends HandlerWrapper
public BufferedResponseHandler()
{
- // include only GET requests
-
_methods.include(HttpMethod.GET.asString());
- // Exclude images, aduio and video from buffering
for (String type : MimeTypes.getKnownMimeTypes())
{
if (type.startsWith("image/") ||
@@ -76,7 +75,9 @@ public class BufferedResponseHandler extends HandlerWrapper
type.startsWith("video/"))
_mimeTypes.exclude(type);
}
- LOG.debug("{} mime types {}", this, _mimeTypes);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} mime types {}", this, _mimeTypes);
}
public IncludeExclude getMethodIncludeExclude()
@@ -94,66 +95,6 @@ public class BufferedResponseHandler extends HandlerWrapper
return _mimeTypes;
}
- @Override
- public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
- {
- final ServletContext context = baseRequest.getServletContext();
- final String path = baseRequest.getPathInContext();
- LOG.debug("{} handle {} in {}", this, baseRequest, context);
-
- HttpOutput out = baseRequest.getResponse().getHttpOutput();
-
- // Are we already being gzipped?
- HttpOutput.Interceptor interceptor = out.getInterceptor();
- while (interceptor != null)
- {
- if (interceptor instanceof BufferedInterceptor)
- {
- LOG.debug("{} already intercepting {}", this, request);
- _handler.handle(target, baseRequest, request, response);
- return;
- }
- interceptor = interceptor.getNextInterceptor();
- }
-
- // If not a supported method - no Vary because no matter what client, this URI is always excluded
- if (!_methods.test(baseRequest.getMethod()))
- {
- LOG.debug("{} excluded by method {}", this, request);
- _handler.handle(target, baseRequest, request, response);
- return;
- }
-
- // If not a supported URI- no Vary because no matter what client, this URI is always excluded
- // Use pathInfo because this is be
- if (!isPathBufferable(path))
- {
- LOG.debug("{} excluded by path {}", this, request);
- _handler.handle(target, baseRequest, request, response);
- return;
- }
-
- // If the mime type is known from the path, then apply mime type filtering
- String mimeType = context == null ? MimeTypes.getDefaultMimeByExtension(path) : context.getMimeType(path);
- if (mimeType != null)
- {
- mimeType = MimeTypes.getContentTypeWithoutCharset(mimeType);
- if (!isMimeTypeBufferable(mimeType))
- {
- LOG.debug("{} excluded by path suffix mime type {}", this, request);
- // handle normally without setting vary header
- _handler.handle(target, baseRequest, request, response);
- return;
- }
- }
-
- // install interceptor and handle
- out.setInterceptor(new BufferedInterceptor(baseRequest.getHttpChannel(), out.getInterceptor()));
-
- if (_handler != null)
- _handler.handle(target, baseRequest, request, response);
- }
-
protected boolean isMimeTypeBufferable(String mimetype)
{
return _mimeTypes.test(mimetype);
@@ -167,116 +108,197 @@ public class BufferedResponseHandler extends HandlerWrapper
return _paths.test(requestURI);
}
- private class BufferedInterceptor implements HttpOutput.Interceptor
+ protected boolean shouldBuffer(HttpChannel channel, boolean last)
{
- final Interceptor _next;
- final HttpChannel _channel;
- final Queue _buffers = new ConcurrentLinkedQueue<>();
- Boolean _aggregating;
- ByteBuffer _aggregate;
+ if (last)
+ return false;
- public BufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
+ Response response = channel.getResponse();
+ int status = response.getStatus();
+ if (HttpStatus.hasNoBody(status) || HttpStatus.isRedirection(status))
+ return false;
+
+ String ct = response.getContentType();
+ if (ct == null)
+ return true;
+
+ ct = MimeTypes.getContentTypeWithoutCharset(ct);
+ return isMimeTypeBufferable(StringUtil.asciiToLowerCase(ct));
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ final ServletContext context = baseRequest.getServletContext();
+ final String path = baseRequest.getPathInContext();
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} handle {} in {}", this, baseRequest, context);
+
+ // Are we already buffering?
+ HttpOutput out = baseRequest.getResponse().getHttpOutput();
+ HttpOutput.Interceptor interceptor = out.getInterceptor();
+ while (interceptor != null)
+ {
+ if (interceptor instanceof BufferedInterceptor)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} already intercepting {}", this, request);
+ _handler.handle(target, baseRequest, request, response);
+ return;
+ }
+ interceptor = interceptor.getNextInterceptor();
+ }
+
+ // If not a supported method this URI is always excluded.
+ if (!_methods.test(baseRequest.getMethod()))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} excluded by method {}", this, request);
+ _handler.handle(target, baseRequest, request, response);
+ return;
+ }
+
+ // If not a supported path this URI is always excluded.
+ if (!isPathBufferable(path))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} excluded by path {}", this, request);
+ _handler.handle(target, baseRequest, request, response);
+ return;
+ }
+
+ // If the mime type is known from the path then apply mime type filtering.
+ String mimeType = context == null ? MimeTypes.getDefaultMimeByExtension(path) : context.getMimeType(path);
+ if (mimeType != null)
+ {
+ mimeType = MimeTypes.getContentTypeWithoutCharset(mimeType);
+ if (!isMimeTypeBufferable(mimeType))
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} excluded by path suffix mime type {}", this, request);
+
+ // handle normally without setting vary header
+ _handler.handle(target, baseRequest, request, response);
+ return;
+ }
+ }
+
+ // Install buffered interceptor and handle.
+ out.setInterceptor(newBufferedInterceptor(baseRequest.getHttpChannel(), out.getInterceptor()));
+ if (_handler != null)
+ _handler.handle(target, baseRequest, request, response);
+ }
+
+ protected BufferedInterceptor newBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
+ {
+ return new ArrayBufferedInterceptor(httpChannel, interceptor);
+ }
+
+ /**
+ * An {@link HttpOutput.Interceptor} which is created by {@link #newBufferedInterceptor(HttpChannel, Interceptor)}
+ * and is used by the implementation to buffer outgoing content.
+ */
+ protected interface BufferedInterceptor extends HttpOutput.Interceptor
+ {
+ }
+
+ private class ArrayBufferedInterceptor implements BufferedInterceptor
+ {
+ private final Interceptor _next;
+ private final HttpChannel _channel;
+ private final Queue _buffers = new ArrayDeque<>();
+ private Boolean _aggregating;
+ private ByteBuffer _aggregate;
+
+ public ArrayBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
{
_next = interceptor;
_channel = httpChannel;
}
- @Override
- public void resetBuffer()
- {
- _buffers.clear();
- _aggregating = null;
- _aggregate = null;
- }
-
- ;
-
- @Override
- public void write(ByteBuffer content, boolean last, Callback callback)
- {
- if (LOG.isDebugEnabled())
- LOG.debug("{} write last={} {}", this, last, BufferUtil.toDetailString(content));
- // if we are not committed, have to decide if we should aggregate or not
- if (_aggregating == null)
- {
- Response response = _channel.getResponse();
- int sc = response.getStatus();
- if (sc > 0 && (sc < 200 || sc == 204 || sc == 205 || sc >= 300))
- _aggregating = Boolean.FALSE; // No body
- else
- {
- String ct = response.getContentType();
- if (ct == null)
- _aggregating = Boolean.TRUE;
- else
- {
- ct = MimeTypes.getContentTypeWithoutCharset(ct);
- _aggregating = isMimeTypeBufferable(StringUtil.asciiToLowerCase(ct));
- }
- }
- }
-
- // If we are not aggregating, then handle normally
- if (!_aggregating.booleanValue())
- {
- getNextInterceptor().write(content, last, callback);
- return;
- }
-
- // If last
- if (last)
- {
- // Add the current content to the buffer list without a copy
- if (BufferUtil.length(content) > 0)
- _buffers.add(content);
-
- if (LOG.isDebugEnabled())
- LOG.debug("{} committing {}", this, _buffers.size());
- commit(_buffers, callback);
- }
- else
- {
- if (LOG.isDebugEnabled())
- LOG.debug("{} aggregating", this);
-
- // Aggregate the content into buffer chain
- while (BufferUtil.hasContent(content))
- {
- // Do we need a new aggregate buffer
- if (BufferUtil.space(_aggregate) == 0)
- {
- int size = Math.max(_channel.getHttpConfiguration().getOutputBufferSize(), BufferUtil.length(content));
- _aggregate = BufferUtil.allocate(size); // TODO use a buffer pool
- _buffers.add(_aggregate);
- }
-
- BufferUtil.append(_aggregate, content);
- }
- callback.succeeded();
- }
- }
-
@Override
public Interceptor getNextInterceptor()
{
return _next;
}
- protected void commit(Queue buffers, Callback callback)
+ @Override
+ public void resetBuffer()
{
- // If only 1 buffer
- if (_buffers.size() == 0)
- getNextInterceptor().write(BufferUtil.EMPTY_BUFFER, true, callback);
- else if (_buffers.size() == 1)
- // just flush it with the last callback
- getNextInterceptor().write(_buffers.remove(), true, callback);
+ _buffers.clear();
+ _aggregating = null;
+ _aggregate = null;
+ BufferedInterceptor.super.resetBuffer();
+ }
+
+ @Override
+ public void write(ByteBuffer content, boolean last, Callback callback)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} write last={} {}", this, last, BufferUtil.toDetailString(content));
+
+ // If we are not committed, have to decide if we should aggregate or not.
+ if (_aggregating == null)
+ _aggregating = shouldBuffer(_channel, last);
+
+ // If we are not aggregating, then handle normally.
+ if (!_aggregating)
+ {
+ getNextInterceptor().write(content, last, callback);
+ return;
+ }
+
+ if (last)
+ {
+ // Add the current content to the buffer list without a copy.
+ if (BufferUtil.length(content) > 0)
+ _buffers.offer(content);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} committing {}", this, _buffers.size());
+ commit(callback);
+ }
else
{
- // Create an iterating callback to do the writing
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} aggregating", this);
+
+ // Aggregate the content into buffer chain.
+ while (BufferUtil.hasContent(content))
+ {
+ // Do we need a new aggregate buffer.
+ if (BufferUtil.space(_aggregate) == 0)
+ {
+ // TODO: use a buffer pool always allocating with outputBufferSize to avoid polluting the ByteBufferPool.
+ int size = Math.max(_channel.getHttpConfiguration().getOutputBufferSize(), BufferUtil.length(content));
+ _aggregate = BufferUtil.allocate(size);
+ _buffers.offer(_aggregate);
+ }
+
+ BufferUtil.append(_aggregate, content);
+ }
+ callback.succeeded();
+ }
+ }
+
+ private void commit(Callback callback)
+ {
+ if (_buffers.size() == 0)
+ {
+ getNextInterceptor().write(BufferUtil.EMPTY_BUFFER, true, callback);
+ }
+ else if (_buffers.size() == 1)
+ {
+ getNextInterceptor().write(_buffers.poll(), true, callback);
+ }
+ else
+ {
+ // Create an iterating callback to do the writing.
IteratingCallback icb = new IteratingCallback()
{
@Override
- protected Action process() throws Exception
+ protected Action process()
{
ByteBuffer buffer = _buffers.poll();
if (buffer == null)
@@ -289,14 +311,14 @@ public class BufferedResponseHandler extends HandlerWrapper
@Override
protected void onCompleteSuccess()
{
- // Signal last callback
+ // Signal last callback.
callback.succeeded();
}
@Override
protected void onCompleteFailure(Throwable cause)
{
- // Signal last callback
+ // Signal last callback.
callback.failed(cause);
}
};
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandler.java
new file mode 100644
index 00000000000..e6b6e0b36ee
--- /dev/null
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandler.java
@@ -0,0 +1,232 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 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.handler;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Objects;
+
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.HttpOutput.Interceptor;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.IteratingCallback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ * A Handler that can apply a {@link org.eclipse.jetty.server.HttpOutput.Interceptor}
+ * mechanism to buffer the entire response content until the output is closed.
+ * This allows the commit to be delayed until the response is complete and thus
+ * headers and response status can be changed while writing the body.
+ *
+ *
+ * Note that the decision to buffer is influenced by the headers and status at the
+ * first write, and thus subsequent changes to those headers will not influence the
+ * decision to buffer or not.
+ *
+ *
+ * Note also that there are no memory limits to the size of the buffer, thus
+ * this handler can represent an unbounded memory commitment if the content
+ * generated can also be unbounded.
+ *
+ */
+public class FileBufferedResponseHandler extends BufferedResponseHandler
+{
+ private static final Logger LOG = LoggerFactory.getLogger(FileBufferedResponseHandler.class);
+
+ private Path _tempDir = new File(System.getProperty("java.io.tmpdir")).toPath();
+
+ public Path getTempDir()
+ {
+ return _tempDir;
+ }
+
+ public void setTempDir(Path tempDir)
+ {
+ _tempDir = Objects.requireNonNull(tempDir);
+ }
+
+ @Override
+ protected BufferedInterceptor newBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
+ {
+ return new FileBufferedInterceptor(httpChannel, interceptor);
+ }
+
+ private class FileBufferedInterceptor implements BufferedResponseHandler.BufferedInterceptor
+ {
+ private static final int MAX_MAPPED_BUFFER_SIZE = Integer.MAX_VALUE / 2;
+
+ private final Interceptor _next;
+ private final HttpChannel _channel;
+ private Boolean _aggregating;
+ private Path _filePath;
+ private OutputStream _fileOutputStream;
+
+ public FileBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
+ {
+ _next = interceptor;
+ _channel = httpChannel;
+ }
+
+ @Override
+ public Interceptor getNextInterceptor()
+ {
+ return _next;
+ }
+
+ @Override
+ public void resetBuffer()
+ {
+ dispose();
+ BufferedInterceptor.super.resetBuffer();
+ }
+
+ private void dispose()
+ {
+ IO.close(_fileOutputStream);
+ _fileOutputStream = null;
+ _aggregating = null;
+
+ if (_filePath != null)
+ {
+ try
+ {
+ Files.delete(_filePath);
+ }
+ catch (Throwable t)
+ {
+ LOG.warn("Could not delete file {}", _filePath, t);
+ }
+ _filePath = null;
+ }
+ }
+
+ @Override
+ public void write(ByteBuffer content, boolean last, Callback callback)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} write last={} {}", this, last, BufferUtil.toDetailString(content));
+
+ // If we are not committed, must decide if we should aggregate or not.
+ if (_aggregating == null)
+ _aggregating = shouldBuffer(_channel, last);
+
+ // If we are not aggregating, then handle normally.
+ if (!_aggregating)
+ {
+ getNextInterceptor().write(content, last, callback);
+ return;
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} aggregating", this);
+
+ try
+ {
+ if (BufferUtil.hasContent(content))
+ aggregate(content);
+ }
+ catch (Throwable t)
+ {
+ dispose();
+ callback.failed(t);
+ return;
+ }
+
+ if (last)
+ commit(callback);
+ else
+ callback.succeeded();
+ }
+
+ private void aggregate(ByteBuffer content) throws IOException
+ {
+ if (_fileOutputStream == null)
+ {
+ // Create a new OutputStream to a file.
+ _filePath = Files.createTempFile(_tempDir, "BufferedResponse", "");
+ _fileOutputStream = Files.newOutputStream(_filePath, StandardOpenOption.WRITE);
+ }
+
+ BufferUtil.writeTo(content, _fileOutputStream);
+ }
+
+ private void commit(Callback callback)
+ {
+ if (_fileOutputStream == null)
+ {
+ // We have no content to write, signal next interceptor that we are finished.
+ getNextInterceptor().write(BufferUtil.EMPTY_BUFFER, true, callback);
+ return;
+ }
+
+ try
+ {
+ _fileOutputStream.close();
+ _fileOutputStream = null;
+ }
+ catch (Throwable t)
+ {
+ dispose();
+ callback.failed(t);
+ return;
+ }
+
+ // Create an iterating callback to do the writing
+ IteratingCallback icb = new IteratingCallback()
+ {
+ private final long fileLength = _filePath.toFile().length();
+ private long _pos = 0;
+ private boolean _last = false;
+
+ @Override
+ protected Action process() throws Exception
+ {
+ if (_last)
+ return Action.SUCCEEDED;
+
+ long len = Math.min(MAX_MAPPED_BUFFER_SIZE, fileLength - _pos);
+ _last = (_pos + len == fileLength);
+ ByteBuffer buffer = BufferUtil.toMappedBuffer(_filePath, _pos, len);
+ getNextInterceptor().write(buffer, _last, this);
+ _pos += len;
+ return Action.SCHEDULED;
+ }
+
+ @Override
+ protected void onCompleteSuccess()
+ {
+ dispose();
+ callback.succeeded();
+ }
+
+ @Override
+ protected void onCompleteFailure(Throwable cause)
+ {
+ dispose();
+ callback.failed(cause);
+ }
+ };
+ icb.iterate();
+ }
+ }
+}
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/FileBufferedResponseHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/FileBufferedResponseHandlerTest.java
new file mode 100644
index 00000000000..c6bb92f8b3f
--- /dev/null
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/FileBufferedResponseHandlerTest.java
@@ -0,0 +1,609 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 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.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.FileBufferedResponseHandler;
+import org.eclipse.jetty.server.handler.HandlerCollection;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.Callback;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class FileBufferedResponseHandlerTest
+{
+ private static final Logger LOG = LoggerFactory.getLogger(FileBufferedResponseHandlerTest.class);
+
+ private Server _server;
+ private LocalConnector _localConnector;
+ private ServerConnector _serverConnector;
+ private Path _testDir;
+ private FileBufferedResponseHandler _bufferedHandler;
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ _testDir = MavenTestingUtils.getTargetTestingPath(FileBufferedResponseHandlerTest.class.getName());
+ FS.ensureDirExists(_testDir);
+
+ _server = new Server();
+ HttpConfiguration config = new HttpConfiguration();
+ config.setOutputBufferSize(1024);
+ config.setOutputAggregationSize(256);
+
+ _localConnector = new LocalConnector(_server, new HttpConnectionFactory(config));
+ _localConnector.setIdleTimeout(Duration.ofMinutes(1).toMillis());
+ _server.addConnector(_localConnector);
+ _serverConnector = new ServerConnector(_server, new HttpConnectionFactory(config));
+ _server.addConnector(_serverConnector);
+
+ _bufferedHandler = new FileBufferedResponseHandler();
+ _bufferedHandler.setTempDir(_testDir);
+ _bufferedHandler.getPathIncludeExclude().include("/include/*");
+ _bufferedHandler.getPathIncludeExclude().exclude("*.exclude");
+ _bufferedHandler.getMimeIncludeExclude().exclude("text/excluded");
+ _server.setHandler(_bufferedHandler);
+
+ FS.ensureEmpty(_testDir);
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ _server.stop();
+ }
+
+ @Test
+ public void testPathNotIncluded() throws Exception
+ {
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(10);
+ PrintWriter writer = response.getWriter();
+ writer.println("a string larger than the buffer size");
+ writer.println("Committed: " + response.isCommitted());
+ writer.println("NumFiles: " + getNumFiles());
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The response was committed after the first write and we never created a file to buffer the response into.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, containsString("Committed: true"));
+ assertThat(responseContent, containsString("NumFiles: 0"));
+ assertThat(getNumFiles(), is(0));
+ }
+
+ @Test
+ public void testIncludedByPath() throws Exception
+ {
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(10);
+ PrintWriter writer = response.getWriter();
+ writer.println("a string larger than the buffer size");
+ writer.println("Committed: " + response.isCommitted());
+ writer.println("NumFiles: " + getNumFiles());
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The response was not committed after the first write and a file was created to buffer the response.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, containsString("Committed: false"));
+ assertThat(responseContent, containsString("NumFiles: 1"));
+ assertThat(getNumFiles(), is(0));
+ }
+
+ @Test
+ public void testExcludedByPath() throws Exception
+ {
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(10);
+ PrintWriter writer = response.getWriter();
+ writer.println("a string larger than the buffer size");
+ writer.println("Committed: " + response.isCommitted());
+ writer.println("NumFiles: " + getNumFiles());
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path.exclude HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The response was committed after the first write and we never created a file to buffer the response into.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, containsString("Committed: true"));
+ assertThat(responseContent, containsString("NumFiles: 0"));
+ assertThat(getNumFiles(), is(0));
+ }
+
+ @Test
+ public void testExcludedByMime() throws Exception
+ {
+ String excludedMimeType = "text/excluded";
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setContentType(excludedMimeType);
+ response.setBufferSize(10);
+ PrintWriter writer = response.getWriter();
+ writer.println("a string larger than the buffer size");
+ writer.println("Committed: " + response.isCommitted());
+ writer.println("NumFiles: " + getNumFiles());
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The response was committed after the first write and we never created a file to buffer the response into.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, containsString("Committed: true"));
+ assertThat(responseContent, containsString("NumFiles: 0"));
+ assertThat(getNumFiles(), is(0));
+ }
+
+ @Test
+ public void testFlushed() throws Exception
+ {
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(1024);
+ PrintWriter writer = response.getWriter();
+ writer.println("a string smaller than the buffer size");
+ writer.println("NumFilesBeforeFlush: " + getNumFiles());
+ writer.flush();
+ writer.println("Committed: " + response.isCommitted());
+ writer.println("NumFiles: " + getNumFiles());
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The response was not committed after the buffer was flushed and a file was created to buffer the response.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, containsString("NumFilesBeforeFlush: 0"));
+ assertThat(responseContent, containsString("Committed: false"));
+ assertThat(responseContent, containsString("NumFiles: 1"));
+ assertThat(getNumFiles(), is(0));
+ }
+
+ @Test
+ public void testClosed() throws Exception
+ {
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(10);
+ PrintWriter writer = response.getWriter();
+ writer.println("a string larger than the buffer size");
+ writer.println("NumFiles: " + getNumFiles());
+ writer.close();
+ writer.println("writtenAfterClose");
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The content written after close was not sent.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, not(containsString("writtenAfterClose")));
+ assertThat(responseContent, containsString("NumFiles: 1"));
+ assertThat(getNumFiles(), is(0));
+ }
+
+ @Test
+ public void testBufferSizeBig() throws Exception
+ {
+ int bufferSize = 4096;
+ String largeContent = generateContent(bufferSize - 64);
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(bufferSize);
+ PrintWriter writer = response.getWriter();
+ writer.println(largeContent);
+ writer.println("Committed: " + response.isCommitted());
+ writer.println("NumFiles: " + getNumFiles());
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The content written was not buffered as a file as it was less than the buffer size.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, not(containsString("writtenAfterClose")));
+ assertThat(responseContent, containsString("Committed: false"));
+ assertThat(responseContent, containsString("NumFiles: 0"));
+ assertThat(getNumFiles(), is(0));
+ }
+
+ @Test
+ public void testFlushEmpty() throws Exception
+ {
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(1024);
+ PrintWriter writer = response.getWriter();
+ writer.flush();
+ int numFiles = getNumFiles();
+ writer.println("NumFiles: " + numFiles);
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // The flush should not create the file unless there is content to write.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, containsString("NumFiles: 0"));
+ assertThat(getNumFiles(), is(0));
+ }
+
+ @Test
+ public void testReset() throws Exception
+ {
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ response.setBufferSize(8);
+ PrintWriter writer = response.getWriter();
+ writer.println("THIS WILL BE RESET");
+ writer.flush();
+ writer.println("THIS WILL BE RESET");
+ int numFilesBeforeReset = getNumFiles();
+ response.resetBuffer();
+ int numFilesAfterReset = getNumFiles();
+
+ writer.println("NumFilesBeforeReset: " + numFilesBeforeReset);
+ writer.println("NumFilesAfterReset: " + numFilesAfterReset);
+ writer.println("a string larger than the buffer size");
+ writer.println("NumFilesAfterWrite: " + getNumFiles());
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ String responseContent = response.getContent();
+
+ // Resetting the response buffer will delete the file.
+ assertThat(response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(responseContent, not(containsString("THIS WILL BE RESET")));
+ assertThat(responseContent, containsString("NumFilesBeforeReset: 1"));
+ assertThat(responseContent, containsString("NumFilesAfterReset: 0"));
+ assertThat(responseContent, containsString("NumFilesAfterWrite: 1"));
+ assertThat(getNumFiles(), is(0));
+ }
+
+ @Test
+ public void testFileLargerThanMaxInteger() throws Exception
+ {
+ long fileSize = Integer.MAX_VALUE + 1234L;
+ byte[] bytes = randomBytes(1024 * 1024);
+
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ ServletOutputStream outputStream = response.getOutputStream();
+
+ long written = 0;
+ while (written < fileSize)
+ {
+ int length = Math.toIntExact(Math.min(bytes.length, fileSize - written));
+ outputStream.write(bytes, 0, length);
+ written += length;
+ }
+ outputStream.flush();
+
+ response.setHeader("NumFiles", Integer.toString(getNumFiles()));
+ response.setHeader("FileSize", Long.toString(getFileSize()));
+ }
+ });
+
+ _server.start();
+
+ AtomicLong received = new AtomicLong();
+ HttpTester.Response response = new HttpTester.Response()
+ {
+ @Override
+ public boolean content(ByteBuffer ref)
+ {
+ // Verify the content is what was sent.
+ while (ref.hasRemaining())
+ {
+ byte byteFromBuffer = ref.get();
+ long totalReceived = received.getAndIncrement();
+ int bytesIndex = (int)(totalReceived % bytes.length);
+ byte byteFromArray = bytes[bytesIndex];
+
+ if (byteFromBuffer != byteFromArray)
+ {
+ LOG.warn("Mismatch at index {} received bytes {}, {}!={}", bytesIndex, totalReceived, byteFromBuffer, byteFromArray, new IllegalStateException());
+ return true;
+ }
+ }
+
+ return false;
+ }
+ };
+
+ try (Socket socket = new Socket("localhost", _serverConnector.getLocalPort()))
+ {
+ OutputStream output = socket.getOutputStream();
+ String request = "GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n";
+ output.write(request.getBytes(StandardCharsets.UTF_8));
+ output.flush();
+
+ HttpTester.Input input = HttpTester.from(socket.getInputStream());
+ HttpTester.parseResponse(input, response);
+ }
+
+ assertTrue(response.isComplete());
+ assertThat(response.get("NumFiles"), is("1"));
+ assertThat(response.get("FileSize"), is(Long.toString(fileSize)));
+ assertThat(received.get(), is(fileSize));
+ assertThat(getNumFiles(), is(0));
+ }
+
+ @Test
+ public void testNextInterceptorFailed() throws Exception
+ {
+ AbstractHandler failingInterceptorHandler = new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ HttpOutput httpOutput = baseRequest.getResponse().getHttpOutput();
+ HttpOutput.Interceptor nextInterceptor = httpOutput.getInterceptor();
+ httpOutput.setInterceptor(new HttpOutput.Interceptor()
+ {
+ @Override
+ public void write(ByteBuffer content, boolean last, Callback callback)
+ {
+ callback.failed(new Throwable("intentionally throwing from interceptor"));
+ }
+
+ @Override
+ public HttpOutput.Interceptor getNextInterceptor()
+ {
+ return nextInterceptor;
+ }
+ });
+ }
+ };
+
+ _server.setHandler(new HandlerCollection(failingInterceptorHandler, _server.getHandler()));
+ CompletableFuture errorFuture = new CompletableFuture<>();
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ byte[] chunk1 = "this content will ".getBytes();
+ byte[] chunk2 = "be buffered in a file".getBytes();
+ response.setContentLength(chunk1.length + chunk2.length);
+ ServletOutputStream outputStream = response.getOutputStream();
+
+ // Write chunk1 and then flush so it is written to the file.
+ outputStream.write(chunk1);
+ outputStream.flush();
+ assertThat(getNumFiles(), is(1));
+
+ try
+ {
+ // ContentLength is set so it knows this is the last write.
+ // This will cause the file to be written to the next interceptor which will fail.
+ outputStream.write(chunk2);
+ }
+ catch (Throwable t)
+ {
+ errorFuture.complete(t);
+ throw t;
+ }
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ // Response was aborted.
+ assertThat(response.getStatus(), is(0));
+
+ // We failed because of the next interceptor.
+ Throwable error = errorFuture.get(5, TimeUnit.SECONDS);
+ assertThat(error.getMessage(), containsString("intentionally throwing from interceptor"));
+
+ // All files were deleted.
+ assertThat(getNumFiles(), is(0));
+ }
+
+ @Test
+ public void testFileWriteFailed() throws Exception
+ {
+ // Set the temp directory to an empty directory so that the file cannot be created.
+ File tempDir = MavenTestingUtils.getTargetTestingDir(getClass().getSimpleName());
+ FS.ensureDeleted(tempDir);
+ _bufferedHandler.setTempDir(tempDir.toPath());
+
+ CompletableFuture errorFuture = new CompletableFuture<>();
+ _bufferedHandler.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ baseRequest.setHandled(true);
+ ServletOutputStream outputStream = response.getOutputStream();
+ byte[] content = "this content will be buffered in a file".getBytes();
+
+ try
+ {
+ // Write the content and flush it to the file.
+ // This should throw as it cannot create the file to aggregate into.
+ outputStream.write(content);
+ outputStream.flush();
+ }
+ catch (Throwable t)
+ {
+ errorFuture.complete(t);
+ throw t;
+ }
+ }
+ });
+
+ _server.start();
+ String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ // Response was aborted.
+ assertThat(response.getStatus(), is(0));
+
+ // We failed because cannot create the file.
+ Throwable error = errorFuture.get(5, TimeUnit.SECONDS);
+ assertThat(error, instanceOf(NoSuchFileException.class));
+
+ // No files were created.
+ assertThat(getNumFiles(), is(0));
+ }
+
+ private int getNumFiles()
+ {
+ File[] files = _testDir.toFile().listFiles();
+ if (files == null)
+ return 0;
+
+ return files.length;
+ }
+
+ private long getFileSize()
+ {
+ File[] files = _testDir.toFile().listFiles();
+ assertNotNull(files);
+ assertThat(files.length, is(1));
+ return files[0].length();
+ }
+
+ private static String generateContent(int size)
+ {
+ Random random = new Random();
+ StringBuilder stringBuilder = new StringBuilder(size);
+ for (int i = 0; i < size; i++)
+ {
+ stringBuilder.append((char)Math.abs(random.nextInt(0x7F)));
+ }
+ return stringBuilder.toString();
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private byte[] randomBytes(int size)
+ {
+ byte[] data = new byte[size];
+ new Random().nextBytes(data);
+ return data;
+ }
+}
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
index 4963430c818..4d1c0d29d4c 100644
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
@@ -32,6 +32,8 @@ import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@@ -40,6 +42,7 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpParser;
+import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.logging.StacklessLogging;
@@ -47,6 +50,7 @@ import org.eclipse.jetty.server.LocalConnector.LocalEndPoint;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.IO;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -59,9 +63,11 @@ import org.slf4j.LoggerFactory;
import static org.hamcrest.MatcherAssert.assertThat;
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.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class HttpConnectionTest
@@ -1353,6 +1359,68 @@ public class HttpConnectionTest
}
}
+ @Test
+ public void testBytesIn() throws Exception
+ {
+ String chunk1 = "0123456789ABCDEF";
+ String chunk2 = IntStream.range(0, 64).mapToObj(i -> chunk1).collect(Collectors.joining());
+ long dataLength = chunk1.length() + chunk2.length();
+ server.stop();
+ server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ jettyRequest.setHandled(true);
+ IO.copy(request.getInputStream(), IO.getNullStream());
+
+ HttpConnection connection = HttpConnection.getCurrentConnection();
+ long bytesIn = connection.getBytesIn();
+ assertThat(bytesIn, greaterThan(dataLength));
+ }
+ });
+ server.start();
+
+ LocalEndPoint localEndPoint = connector.executeRequest("" +
+ "POST / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Content-Length: " + dataLength + "\r\n" +
+ "\r\n" +
+ chunk1);
+
+ // Wait for the server to block on the read().
+ Thread.sleep(500);
+
+ // Send more content.
+ localEndPoint.addInput(chunk2);
+
+ HttpTester.Response response = HttpTester.parseResponse(localEndPoint.getResponse());
+ assertEquals(response.getStatus(), HttpStatus.OK_200);
+ localEndPoint.close();
+
+ localEndPoint = connector.executeRequest("" +
+ "POST / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n" +
+ Integer.toHexString(chunk1.length()) + "\r\n" +
+ chunk1 + "\r\n");
+
+ // Wait for the server to block on the read().
+ Thread.sleep(500);
+
+ // Send more content.
+ localEndPoint.addInput("" +
+ Integer.toHexString(chunk2.length()) + "\r\n" +
+ chunk2 + "\r\n" +
+ "0\r\n" +
+ "\r\n");
+
+ response = HttpTester.parseResponse(localEndPoint.getResponse());
+ assertEquals(response.getStatus(), HttpStatus.OK_200);
+ localEndPoint.close();
+ }
+
private int checkContains(String s, int offset, String c)
{
assertThat(s.substring(offset), Matchers.containsString(c));
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
index 28a736d5b72..2661e609e23 100644
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
@@ -28,6 +28,7 @@ import java.security.Principal;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.EnumSet;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
@@ -1733,12 +1734,45 @@ public class RequestTest
"Host: whatever\r\n" +
"\r\n";
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.DEFAULT);
+ assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
+ _connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(new UriCompliance("Test", EnumSet.noneOf(UriCompliance.Violation.class)));
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.LEGACY);
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
}
+
+ @Test
+ public void testAmbiguousPaths() throws Exception
+ {
+ _handler._checker = (request, response) ->
+ {
+ response.getOutputStream().println("servletPath=" + request.getServletPath());
+ response.getOutputStream().println("pathInfo=" + request.getPathInfo());
+ return true;
+ };
+ String request = "GET /unnormal/.././path/ambiguous%2f%2e%2e/%2e;/info HTTP/1.0\r\n" +
+ "Host: whatever\r\n" +
+ "\r\n";
+
+ _connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.from(EnumSet.of(
+ UriCompliance.Violation.AMBIGUOUS_PATH_SEPARATOR,
+ UriCompliance.Violation.AMBIGUOUS_PATH_SEGMENT,
+ UriCompliance.Violation.AMBIGUOUS_PATH_PARAMETER)));
+ assertThat(_connector.getResponse(request), Matchers.allOf(
+ startsWith("HTTP/1.1 200"),
+ containsString("pathInfo=/path/info")));
+
+ _connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.from(EnumSet.of(
+ UriCompliance.Violation.AMBIGUOUS_PATH_SEPARATOR,
+ UriCompliance.Violation.AMBIGUOUS_PATH_SEGMENT,
+ UriCompliance.Violation.AMBIGUOUS_PATH_PARAMETER,
+ UriCompliance.Violation.NON_CANONICAL_AMBIGUOUS_PATHS)));
+ assertThat(_connector.getResponse(request), Matchers.allOf(
+ startsWith("HTTP/1.1 200"),
+ containsString("pathInfo=/path/ambiguous/.././info")));
+ }
@Test
public void testPushBuilder()
diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/BaseBuilder.java b/jetty-start/src/main/java/org/eclipse/jetty/start/BaseBuilder.java
index 985da7b50cb..74a35389b4b 100644
--- a/jetty-start/src/main/java/org/eclipse/jetty/start/BaseBuilder.java
+++ b/jetty-start/src/main/java/org/eclipse/jetty/start/BaseBuilder.java
@@ -39,7 +39,7 @@ import org.eclipse.jetty.start.fileinits.TestFileInitializer;
import org.eclipse.jetty.start.fileinits.UriFileInitializer;
/**
- * Build a start configuration in ${jetty.base}
, including
+ * Build a start configuration in {@code ${jetty.base}}, including
* ini files, directories, and libs. Also handles License management.
*/
public class BaseBuilder
@@ -47,7 +47,7 @@ public class BaseBuilder
public static interface Config
{
/**
- * Add a module to the start environment in ${jetty.base}
+ * Add a module to the start environment in {@code ${jetty.base}}
*
* @param module the module to add
* @param props The properties to substitute into a template
@@ -163,7 +163,7 @@ public class BaseBuilder
}
// generate the files
- List files = new ArrayList();
+ List files = new ArrayList<>();
AtomicReference builder = new AtomicReference<>();
AtomicBoolean modified = new AtomicBoolean();
@@ -184,18 +184,21 @@ public class BaseBuilder
if (Files.exists(startd))
{
// Copy start.d files into start.ini
- DirectoryStream.Filter filter = new DirectoryStream.Filter()
+ DirectoryStream.Filter filter = new DirectoryStream.Filter<>()
{
- PathMatcher iniMatcher = PathMatchers.getMatcher("glob:**/start.d/*.ini");
+ private final PathMatcher iniMatcher = PathMatchers.getMatcher("glob:**/start.d/*.ini");
+
@Override
- public boolean accept(Path entry) throws IOException
+ public boolean accept(Path entry)
{
return iniMatcher.matches(entry);
}
};
List paths = new ArrayList<>();
for (Path path : Files.newDirectoryStream(startd, filter))
+ {
paths.add(path);
+ }
paths.sort(new NaturalSort.Paths());
// Read config from start.d
@@ -212,12 +215,16 @@ public class BaseBuilder
try (FileWriter out = new FileWriter(startini.toFile(), true))
{
for (String line : startLines)
+ {
out.append(line).append(System.lineSeparator());
+ }
}
// delete start.d files
for (Path path : paths)
+ {
Files.delete(path);
+ }
Files.delete(startd);
}
}
@@ -264,56 +271,66 @@ public class BaseBuilder
StartLog.warn("Use of both %s and %s is deprecated", getBaseHome().toShortForm(startd), getBaseHome().toShortForm(startini));
builder.set(useStartD ? new StartDirBuilder(this) : new StartIniBuilder(this));
- newlyAdded.stream().map(modules::get).forEach(module ->
- {
- String ini = null;
- try
- {
- if (module.isSkipFilesValidation())
- {
- StartLog.debug("Skipping [files] validation on %s", module.getName());
- }
- else
- {
- // if (explicitly added and ini file modified)
- if (startArgs.getStartModules().contains(module.getName()))
- {
- ini = builder.get().addModule(module, startArgs.getProperties());
- if (ini != null)
- modified.set(true);
- }
- for (String file : module.getFiles())
- {
- files.add(new FileArg(module, startArgs.getProperties().expand(file)));
- }
- }
- }
- catch (Exception e)
- {
- throw new RuntimeException(e);
- }
- if (module.isDynamic())
+ // Collect the filesystem operations to perform,
+ // only for those modules that are enabled.
+ newlyAdded.stream()
+ .map(modules::get)
+ .filter(Module::isEnabled)
+ .forEach(module ->
{
- for (String s : module.getEnableSources())
+ String ini = null;
+ try
{
- StartLog.info("%-15s %s", module.getName(), s);
+ if (module.isSkipFilesValidation())
+ {
+ StartLog.debug("Skipping [files] validation on %s", module.getName());
+ }
+ else
+ {
+ // if (explicitly added and ini file modified)
+ if (startArgs.getStartModules().contains(module.getName()))
+ {
+ ini = builder.get().addModule(module, startArgs.getProperties());
+ if (ini != null)
+ modified.set(true);
+ }
+ for (String file : module.getFiles())
+ {
+ files.add(new FileArg(module, startArgs.getProperties().expand(file)));
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+
+ if (module.isDynamic())
+ {
+ for (String s : module.getEnableSources())
+ {
+ StartLog.info("%-15s %s", module.getName(), s);
+ }
+ }
+ else if (module.isTransitive())
+ {
+ if (module.hasIniTemplate())
+ {
+ StartLog.info("%-15s transitively enabled, ini template available with --add-module=%s",
+ module.getName(),
+ module.getName());
+ }
+ else
+ {
+ StartLog.info("%-15s transitively enabled", module.getName());
+ }
}
- }
- else if (module.isTransitive())
- {
- if (module.hasIniTemplate())
- StartLog.info("%-15s transitively enabled, ini template available with --add-module=%s",
- module.getName(),
- module.getName());
else
- StartLog.info("%-15s transitively enabled", module.getName());
- }
- else
- {
- StartLog.info("%-15s initialized in %s", module.getName(), ini);
- }
- });
+ {
+ StartLog.info("%-15s initialized in %s", module.getName(), ini);
+ }
+ });
files.addAll(startArgs.getFiles());
if (!files.isEmpty() && processFileResources(files))
@@ -370,7 +387,7 @@ public class BaseBuilder
* @param files the list of {@link FileArg}s to process
* @return true if base directory modified, false if left untouched
*/
- private boolean processFileResources(List files) throws IOException
+ private boolean processFileResources(List files)
{
if ((files == null) || (files.isEmpty()))
{
diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java b/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java
index 17bc231e8d5..37074630866 100644
--- a/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java
+++ b/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java
@@ -18,7 +18,6 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
@@ -552,7 +551,7 @@ public class Modules implements Iterable
Set providers = _provided.get(name);
StartLog.debug("Providers of [%s] are %s", name, providers);
if (providers == null || providers.isEmpty())
- return Collections.emptySet();
+ return Set.of();
providers = new HashSet<>(providers);
diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java b/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java
index 8516c0573d4..46b629b14e5 100644
--- a/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java
+++ b/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java
@@ -52,14 +52,8 @@ import org.eclipse.jetty.util.ManifestUtils;
public class StartArgs
{
public static final String VERSION;
- public static final Set ALL_PARTS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
- "java",
- "opts",
- "path",
- "main",
- "args")));
- public static final Set ARG_PARTS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
- "args")));
+ public static final Set ALL_PARTS = Set.of("java", "opts", "path", "main", "args");
+ public static final Set ARG_PARTS = Set.of("args");
static
{
@@ -126,12 +120,12 @@ public class StartArgs
/**
* List of enabled modules
*/
- private List modules = new ArrayList<>();
+ private final List modules = new ArrayList<>();
/**
* List of modules to skip [files] section validation
*/
- private Set skipFileValidationModules = new HashSet<>();
+ private final Set skipFileValidationModules = new HashSet<>();
/**
* Map of enabled modules to the source of where that activation occurred
@@ -141,56 +135,56 @@ public class StartArgs
/**
* List of all active [files] sections from enabled modules
*/
- private List files = new ArrayList<>();
+ private final List files = new ArrayList<>();
/**
* List of all active [lib] sections from enabled modules
*/
- private Classpath classpath;
+ private final Classpath classpath;
/**
* List of all active [xml] sections from enabled modules
*/
- private List xmls = new ArrayList<>();
+ private final List xmls = new ArrayList<>();
/**
* List of all active [jpms] sections for enabled modules
*/
- private Set jmodAdds = new LinkedHashSet<>();
- private Map> jmodPatch = new LinkedHashMap<>();
- private Map> jmodOpens = new LinkedHashMap<>();
- private Map> jmodExports = new LinkedHashMap<>();
- private Map> jmodReads = new LinkedHashMap<>();
+ private final Set jmodAdds = new LinkedHashSet<>();
+ private final Map> jmodPatch = new LinkedHashMap<>();
+ private final Map> jmodOpens = new LinkedHashMap<>();
+ private final Map> jmodExports = new LinkedHashMap<>();
+ private final Map> jmodReads = new LinkedHashMap<>();
/**
* JVM arguments, found via command line and in all active [exec] sections from enabled modules
*/
- private List jvmArgs = new ArrayList<>();
+ private final List jvmArgs = new ArrayList<>();
/**
* List of all xml references found directly on command line or start.ini
*/
- private List xmlRefs = new ArrayList<>();
+ private final List xmlRefs = new ArrayList<>();
/**
* List of all property references found directly on command line or start.ini
*/
- private List propertyFileRefs = new ArrayList<>();
+ private final List propertyFileRefs = new ArrayList<>();
/**
* List of all property files
*/
- private List propertyFiles = new ArrayList<>();
+ private final List propertyFiles = new ArrayList<>();
- private Props properties = new Props();
- private Map systemPropertySource = new HashMap<>();
- private List rawLibs = new ArrayList<>();
+ private final Props properties = new Props();
+ private final Map systemPropertySource = new HashMap<>();
+ private final List rawLibs = new ArrayList<>();
// jetty.base - build out commands
/**
* --add-module=[module,[module]]
*/
- private List startModules = new ArrayList<>();
+ private final List startModules = new ArrayList<>();
// module inspection commands
/**
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java
index 0292223d1e1..227906aa53c 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java
@@ -25,6 +25,7 @@ import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
@@ -1032,9 +1033,14 @@ public class BufferUtil
public static ByteBuffer toMappedBuffer(File file) throws IOException
{
- try (FileChannel channel = FileChannel.open(file.toPath(), StandardOpenOption.READ))
+ return toMappedBuffer(file.toPath(), 0, file.length());
+ }
+
+ public static ByteBuffer toMappedBuffer(Path filePath, long pos, long len) throws IOException
+ {
+ try (FileChannel channel = FileChannel.open(filePath, StandardOpenOption.READ))
{
- return channel.map(MapMode.READ_ONLY, 0, file.length());
+ return channel.map(MapMode.READ_ONLY, pos, len);
}
}
diff --git a/jetty-websocket/websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/messages/MessageInputStream.java b/jetty-websocket/websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/messages/MessageInputStream.java
index bf11dc2e4dc..cad559725e3 100644
--- a/jetty-websocket/websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/messages/MessageInputStream.java
+++ b/jetty-websocket/websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/messages/MessageInputStream.java
@@ -95,7 +95,9 @@ public class MessageInputStream extends InputStream implements MessageSink
@Override
public int read(final byte[] b, final int off, final int len) throws IOException
{
- return read(ByteBuffer.wrap(b, off, len).flip());
+ ByteBuffer buffer = ByteBuffer.wrap(b, off, len).slice();
+ BufferUtil.clear(buffer);
+ return read(buffer);
}
public int read(ByteBuffer buffer) throws IOException
diff --git a/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/autobahn/AutobahnTests.java b/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/autobahn/AutobahnTests.java
index 7a4b4232ba3..0346be14eb3 100644
--- a/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/autobahn/AutobahnTests.java
+++ b/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/autobahn/AutobahnTests.java
@@ -51,7 +51,7 @@ import org.testcontainers.utility.MountableFile;
import static org.junit.jupiter.api.Assertions.assertTrue;
-@Disabled
+@Disabled("Disable this test so it doesn't run locally as it takes 1h+ to run.")
@Testcontainers
public class AutobahnTests
{
diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler.java
index 21c6e28a533..99cfd8bb2e6 100644
--- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler.java
+++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler.java
@@ -266,6 +266,10 @@ public class JavaxWebSocketFrameHandler implements FrameHandler
{
notifyOnClose(closeStatus, callback);
container.notifySessionListeners((listener) -> listener.onJavaxWebSocketSessionClosed(session));
+
+ // Close AvailableEncoders and AvailableDecoders to call destroy() on any instances of Encoder/Encoder created.
+ session.getDecoders().close();
+ session.getEncoders().close();
}
private void notifyOnClose(CloseStatus closeStatus, Callback callback)
diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/AvailableDecoders.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/AvailableDecoders.java
index e0bbe141404..3aa44344cc2 100644
--- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/AvailableDecoders.java
+++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/AvailableDecoders.java
@@ -13,6 +13,7 @@
package org.eclipse.jetty.websocket.javax.common.decoders;
+import java.io.Closeable;
import java.io.InputStream;
import java.io.Reader;
import java.nio.ByteBuffer;
@@ -30,7 +31,7 @@ import org.eclipse.jetty.websocket.core.exception.InvalidSignatureException;
import org.eclipse.jetty.websocket.core.exception.InvalidWebSocketException;
import org.eclipse.jetty.websocket.core.internal.util.ReflectUtils;
-public class AvailableDecoders implements Iterable
+public class AvailableDecoders implements Iterable, Closeable
{
private final List registeredDecoders = new ArrayList<>();
private final EndpointConfig config;
@@ -211,4 +212,10 @@ public class AvailableDecoders implements Iterable
{
return registeredDecoders.stream();
}
+
+ @Override
+ public void close()
+ {
+ registeredDecoders.forEach(RegisteredDecoder::destroyInstance);
+ }
}
diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/RegisteredDecoder.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/RegisteredDecoder.java
index 3608d7796b4..b2ba440a86c 100644
--- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/RegisteredDecoder.java
+++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/RegisteredDecoder.java
@@ -19,10 +19,12 @@ import javax.websocket.EndpointConfig;
import org.eclipse.jetty.websocket.core.WebSocketComponents;
import org.eclipse.jetty.websocket.javax.common.InitException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class RegisteredDecoder
{
- private final WebSocketComponents components;
+ private static final Logger LOG = LoggerFactory.getLogger(RegisteredDecoder.class);
// The user supplied Decoder class
public final Class extends Decoder> decoder;
@@ -31,6 +33,7 @@ public class RegisteredDecoder
public final Class> objectType;
public final boolean primitive;
public final EndpointConfig config;
+ private final WebSocketComponents components;
private Decoder instance;
@@ -78,6 +81,23 @@ public class RegisteredDecoder
return (T)instance;
}
+ public void destroyInstance()
+ {
+ if (instance != null)
+ {
+ try
+ {
+ instance.destroy();
+ }
+ catch (Throwable t)
+ {
+ LOG.warn("Error destroying Decoder", t);
+ }
+
+ instance = null;
+ }
+ }
+
@Override
public String toString()
{
diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/AvailableEncoders.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/AvailableEncoders.java
index 45ba8718fa2..96b4fc10b20 100644
--- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/AvailableEncoders.java
+++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/AvailableEncoders.java
@@ -13,6 +13,7 @@
package org.eclipse.jetty.websocket.javax.common.encoders;
+import java.io.Closeable;
import java.lang.reflect.InvocationTargetException;
import java.nio.ByteBuffer;
import java.util.LinkedList;
@@ -29,9 +30,12 @@ import org.eclipse.jetty.websocket.core.exception.InvalidSignatureException;
import org.eclipse.jetty.websocket.core.exception.InvalidWebSocketException;
import org.eclipse.jetty.websocket.core.internal.util.ReflectUtils;
import org.eclipse.jetty.websocket.javax.common.InitException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-public class AvailableEncoders implements Predicate>
+public class AvailableEncoders implements Predicate>, Closeable
{
+ private static final Logger LOG = LoggerFactory.getLogger(AvailableEncoders.class);
private final EndpointConfig config;
private final WebSocketComponents components;
@@ -241,4 +245,10 @@ public class AvailableEncoders implements Predicate>
{
return registeredEncoders.stream().anyMatch(registered -> registered.isType(type));
}
+
+ @Override
+ public void close()
+ {
+ registeredEncoders.forEach(RegisteredEncoder::destroyInstance);
+ }
}
diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/RegisteredEncoder.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/RegisteredEncoder.java
index e15ce8c4a44..c2c108c6e6b 100644
--- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/RegisteredEncoder.java
+++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/RegisteredEncoder.java
@@ -15,8 +15,13 @@ package org.eclipse.jetty.websocket.javax.common.encoders;
import javax.websocket.Encoder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
public class RegisteredEncoder
{
+ private static final Logger LOG = LoggerFactory.getLogger(RegisteredEncoder.class);
+
public final Class extends Encoder> encoder;
public final Class extends Encoder> interfaceType;
public final Class> objectType;
@@ -46,6 +51,23 @@ public class RegisteredEncoder
return objectType.isAssignableFrom(type);
}
+ public void destroyInstance()
+ {
+ if (instance != null)
+ {
+ try
+ {
+ instance.destroy();
+ }
+ catch (Throwable t)
+ {
+ LOG.warn("Error destroying Decoder", t);
+ }
+
+ instance = null;
+ }
+ }
+
@Override
public String toString()
{
diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderLifeCycleTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderLifeCycleTest.java
index 72acf34cc6d..d8b410d89a2 100644
--- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderLifeCycleTest.java
+++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderLifeCycleTest.java
@@ -37,7 +37,6 @@ import org.eclipse.jetty.websocket.javax.common.encoders.AvailableEncoders;
import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer;
import org.eclipse.jetty.websocket.javax.tests.EchoSocket;
import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
@@ -147,8 +146,6 @@ public class EncoderLifeCycleTest
}
}
- // TODO: Encoder.destroy() is never called in Jetty 10.
- @Disabled()
@ParameterizedTest
@ValueSource(classes = {StringHolder.class, StringHolderSubtype.class})
public void testEncoderLifeCycle(Class extends StringHolder> clazz) throws Exception
diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/SessionTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/SessionTest.java
index a0d0c38a795..9714a3d59a0 100644
--- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/SessionTest.java
+++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/SessionTest.java
@@ -37,7 +37,6 @@ import org.eclipse.jetty.websocket.core.OpCode;
import org.eclipse.jetty.websocket.javax.tests.Fuzzer;
import org.eclipse.jetty.websocket.javax.tests.LocalServer;
import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@@ -62,11 +61,7 @@ public class SessionTest
else
{
ret.append('[').append(pathParams.size()).append(']');
- List keys = new ArrayList<>();
- for (String key : pathParams.keySet())
- {
- keys.add(key);
- }
+ List keys = new ArrayList<>(pathParams.keySet());
Collections.sort(keys);
for (String key : keys)
{
@@ -126,11 +121,7 @@ public class SessionTest
else
{
ret.append('[').append(pathParams.size()).append(']');
- List keys = new ArrayList<>();
- for (String key : pathParams.keySet())
- {
- keys.add(key);
- }
+ List keys = new ArrayList<>(pathParams.keySet());
Collections.sort(keys);
for (String key : keys)
{
@@ -227,14 +218,13 @@ public class SessionTest
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/info/{a}/{b}/").build());
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/info/{a}/{b}/{c}/").build());
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/info/{a}/{b}/{c}/{d}/").build());
- /*
+
endpointClass = SessionInfoEndpoint.class;
- container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass,"/einfo/").build());
- container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass,"/einfo/{a}/").build());
- container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass,"/einfo/{a}/{b}/").build());
- container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass,"/einfo/{a}/{b}/{c}/").build());
- container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass,"/einfo/{a}/{b}/{c}/{d}/").build());
- */
+ container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/einfo/").build());
+ container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/einfo/{a}/").build());
+ container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/einfo/{a}/{b}/").build());
+ container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/einfo/{a}/{b}/{c}/").build());
+ container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/einfo/{a}/{b}/{c}/{d}/").build());
}
private void assertResponse(String requestPath, String requestMessage,
@@ -293,7 +283,6 @@ public class SessionTest
@ParameterizedTest(name = "{0}")
@MethodSource("data")
- @Disabled
public void testPathParamsEndpointEmpty(Case testCase) throws Exception
{
setup(testCase);
@@ -303,7 +292,6 @@ public class SessionTest
@ParameterizedTest(name = "{0}")
@MethodSource("data")
- @Disabled
public void testPathParamsEndpointSingle(Case testCase) throws Exception
{
setup(testCase);
@@ -313,7 +301,6 @@ public class SessionTest
@ParameterizedTest(name = "{0}")
@MethodSource("data")
- @Disabled
public void testPathParamsEndpointDouble(Case testCase) throws Exception
{
setup(testCase);
@@ -323,7 +310,6 @@ public class SessionTest
@ParameterizedTest(name = "{0}")
@MethodSource("data")
- @Disabled
public void testPathParamsEndpointTriple(Case testCase) throws Exception
{
setup(testCase);
@@ -363,7 +349,6 @@ public class SessionTest
@ParameterizedTest(name = "{0}")
@MethodSource("data")
- @Disabled
public void testRequestUriEndpointBasic(Case testCase) throws Exception
{
setup(testCase);
@@ -373,7 +358,6 @@ public class SessionTest
@ParameterizedTest(name = "{0}")
@MethodSource("data")
- @Disabled
public void testRequestUriEndpointWithPathParam(Case testCase) throws Exception
{
setup(testCase);
@@ -383,7 +367,6 @@ public class SessionTest
@ParameterizedTest(name = "{0}")
@MethodSource("data")
- @Disabled
public void testRequestUriEndpointWithPathParamWithQuery(Case testCase) throws Exception
{
setup(testCase);
diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/TextStreamTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/TextStreamTest.java
index 15c8773939b..9291affd1f8 100644
--- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/TextStreamTest.java
+++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/TextStreamTest.java
@@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;
+import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.websocket.ClientEndpointConfig;
@@ -50,7 +51,6 @@ import org.eclipse.jetty.websocket.javax.tests.WSEndpointTracker;
import org.hamcrest.Matchers;
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;
import org.slf4j.LoggerFactory;
@@ -116,33 +116,26 @@ public class TextStreamTest
send.add(CloseStatus.toFrame(CloseStatus.NORMAL));
ByteBuffer expectedMessage = DataUtils.copyOf(data);
- List expect = new ArrayList<>();
- expect.add(new Frame(OpCode.TEXT).setPayload(expectedMessage));
- expect.add(CloseStatus.toFrame(CloseStatus.NORMAL));
-
try (Fuzzer fuzzer = server.newNetworkFuzzer("/echo"))
{
fuzzer.sendBulk(send);
- fuzzer.expect(expect);
+ BlockingQueue receivedFrames = fuzzer.getOutputFrames();
+ fuzzer.expectMessage(receivedFrames, OpCode.TEXT, expectedMessage);
+ fuzzer.expect(List.of(CloseStatus.toFrame(CloseStatus.NORMAL)));
}
}
- // TODO These tests incorrectly assumes no frame fragmentation.
- // When message fragmentation is implemented in PartialStringMessageSink then update
- // this test to check on the server side for no buffers larger than the maxTextMessageBufferSize.
-
- @Disabled
@Test
public void testAtMaxDefaultMessageBufferSize() throws Exception
{
testEcho(container.getDefaultMaxTextMessageBufferSize());
}
- @Disabled
@Test
public void testLargerThenMaxDefaultMessageBufferSize() throws Exception
{
- int size = container.getDefaultMaxTextMessageBufferSize() + 16;
+ int maxTextMessageBufferSize = container.getDefaultMaxTextMessageBufferSize();
+ int size = maxTextMessageBufferSize + 16;
byte[] data = newData(size);
List send = new ArrayList<>();
@@ -153,19 +146,13 @@ public class TextStreamTest
byte[] expectedData = new byte[data.length];
System.arraycopy(data, 0, expectedData, 0, data.length);
- // Frames expected are influenced by container.getDefaultMaxTextMessageBufferSize setting
- ByteBuffer frame1 = ByteBuffer.wrap(expectedData, 0, container.getDefaultMaxTextMessageBufferSize());
- ByteBuffer frame2 = ByteBuffer
- .wrap(expectedData, container.getDefaultMaxTextMessageBufferSize(), size - container.getDefaultMaxTextMessageBufferSize());
- List expect = new ArrayList<>();
- expect.add(new Frame(OpCode.TEXT).setPayload(frame1).setFin(false));
- expect.add(new Frame(OpCode.CONTINUATION).setPayload(frame2).setFin(true));
- expect.add(CloseStatus.toFrame(CloseStatus.NORMAL));
-
try (Fuzzer fuzzer = server.newNetworkFuzzer("/echo"))
{
fuzzer.sendBulk(send);
- fuzzer.expect(expect);
+
+ BlockingQueue receivedFrames = fuzzer.getOutputFrames();
+ fuzzer.expectMessage(receivedFrames, OpCode.TEXT, ByteBuffer.wrap(expectedData));
+ fuzzer.expect(List.of(CloseStatus.toFrame(CloseStatus.NORMAL)));
}
}
diff --git a/jetty-websocket/websocket-jetty-common/src/test/java/org/eclipse/jetty/websocket/common/MessageInputStreamTest.java b/jetty-websocket/websocket-jetty-common/src/test/java/org/eclipse/jetty/websocket/common/MessageInputStreamTest.java
index ba204f54521..436b1abb689 100644
--- a/jetty-websocket/websocket-jetty-common/src/test/java/org/eclipse/jetty/websocket/common/MessageInputStreamTest.java
+++ b/jetty-websocket/websocket-jetty-common/src/test/java/org/eclipse/jetty/websocket/common/MessageInputStreamTest.java
@@ -69,6 +69,36 @@ public class MessageInputStreamTest
});
}
+ @Test
+ public void testMultipleReadsIntoSingleByteArray() throws IOException
+ {
+ try (MessageInputStream stream = new MessageInputStream())
+ {
+ // Append a single message (simple, short)
+ Frame frame = new Frame(OpCode.TEXT);
+ frame.setPayload("Hello World");
+ frame.setFin(true);
+ stream.accept(frame, Callback.NOOP);
+
+ // Read entire message it from the stream.
+ byte[] bytes = new byte[100];
+
+ int read = stream.read(bytes, 0, 6);
+ assertThat(read, is(6));
+
+ read = stream.read(bytes, 6, 10);
+ assertThat(read, is(5));
+
+ read = stream.read(bytes, 11, 10);
+ assertThat(read, is(-1));
+
+ String message = new String(bytes, 0, 11, StandardCharsets.UTF_8);
+
+ // Test it
+ assertThat("Message", message, is("Hello World"));
+ }
+ }
+
@Test
public void testBlockOnRead() throws Exception
{
diff --git a/pom.xml b/pom.xml
index b6e7682738d..577ffd2c9e6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -59,6 +59,7 @@
3.6.0
3.0.0-M1
3.0.0-M1
+ 3.0.0
false
@@ -768,8 +769,7 @@
asciidoctor-diagram
- http://www.eclipse.org/jetty/javadoc/${project.version}
- http://download.eclipse.org/jetty/stable-9/xref
+ https://www.eclipse.org/jetty/javadoc/jetty-10
${basedir}/..
https://github.com/eclipse/jetty.project/tree/jetty-9.4.x
https://github.com/eclipse/jetty.project/tree/jetty-10.0.x-doc-refactor/jetty-documentation/src/main/asciidoc
@@ -813,7 +813,7 @@
org.codehaus.mojo
exec-maven-plugin
- 3.0.0
+ ${maven.exec.plugin.version}
org.eclipse.m2e
diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java
index 0924c38af36..97d940c3ea6 100644
--- a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java
+++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java
@@ -859,4 +859,54 @@ public class DistributionTests extends AbstractJettyHomeTest
}
}
}
+
+ @Test
+ public void testDefaultLoggingProviderNotActiveWhenExplicitProviderIsPresent() throws Exception
+ {
+ String jettyVersion = System.getProperty("jettyVersion");
+ JettyHomeTester distribution1 = JettyHomeTester.Builder.newInstance()
+ .jettyVersion(jettyVersion)
+ .mavenLocalRepository(System.getProperty("mavenRepoPath"))
+ .build();
+
+ String[] args1 = {
+ "--approve-all-licenses",
+ "--add-modules=logging-logback,http"
+ };
+
+ try (JettyHomeTester.Run run1 = distribution1.start(args1))
+ {
+ assertTrue(run1.awaitFor(10, TimeUnit.SECONDS));
+ assertEquals(0, run1.getExitValue());
+
+ Path jettyBase = run1.getConfig().getJettyBase();
+
+ assertTrue(Files.exists(jettyBase.resolve("resources/logback.xml")));
+ // The jetty-logging.properties should be absent.
+ assertFalse(Files.exists(jettyBase.resolve("resources/jetty-logging.properties")));
+ }
+
+ JettyHomeTester distribution2 = JettyHomeTester.Builder.newInstance()
+ .jettyVersion(jettyVersion)
+ .mavenLocalRepository(System.getProperty("mavenRepoPath"))
+ .build();
+
+ // Try the modules in reverse order, since it may execute a different code path.
+ String[] args2 = {
+ "--approve-all-licenses",
+ "--add-modules=http,logging-logback"
+ };
+
+ try (JettyHomeTester.Run run2 = distribution2.start(args2))
+ {
+ assertTrue(run2.awaitFor(1000, TimeUnit.SECONDS));
+ assertEquals(0, run2.getExitValue());
+
+ Path jettyBase = run2.getConfig().getJettyBase();
+
+ assertTrue(Files.exists(jettyBase.resolve("resources/logback.xml")));
+ // The jetty-logging.properties should be absent.
+ assertFalse(Files.exists(jettyBase.resolve("resources/jetty-logging.properties")));
+ }
+ }
}
diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ConnectionStatisticsTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ConnectionStatisticsTest.java
index f6d3de2a54d..b0ea1757b79 100644
--- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ConnectionStatisticsTest.java
+++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ConnectionStatisticsTest.java
@@ -32,7 +32,6 @@ import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.IO;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assumptions;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
@@ -50,7 +49,6 @@ public class ConnectionStatisticsTest extends AbstractTest
Assumptions.assumeTrue(scenario.transport == HTTP || scenario.transport == H2C);
}
- @Disabled
@ParameterizedTest
@ArgumentsSource(TransportProvider.class)
public void testConnectionStatistics(Transport transport) throws Exception
diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/CustomRequestLogTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/CustomRequestLogTest.java
index 41a04b19882..84c8f0cb1af 100644
--- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/CustomRequestLogTest.java
+++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/CustomRequestLogTest.java
@@ -14,7 +14,6 @@
package org.eclipse.jetty.test;
import java.io.IOException;
-import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.NetworkInterface;
@@ -27,7 +26,7 @@ import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
-import javax.servlet.ServletException;
+import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
@@ -53,6 +52,7 @@ import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.util.DateCache;
+import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Credential;
import org.junit.jupiter.api.AfterEach;
@@ -66,22 +66,24 @@ import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.fail;
public class CustomRequestLogTest
{
- CustomRequestLog _log;
- Server _server;
- LocalConnector _connector;
- BlockingQueue _entries = new BlockingArrayQueue<>();
- BlockingQueue requestTimes = new BlockingArrayQueue<>();
- ServerConnector _serverConnector;
- URI _serverURI;
+ private final BlockingQueue _entries = new BlockingArrayQueue<>();
+ private final BlockingQueue requestTimes = new BlockingArrayQueue<>();
+ private CustomRequestLog _log;
+ private Server _server;
+ private LocalConnector _connector;
+ private ServerConnector _serverConnector;
+ private URI _serverURI;
private static final long DELAY = 2000;
@BeforeEach
- public void before() throws Exception
+ public void before()
{
_server = new Server();
_connector = new LocalConnector(_server);
@@ -111,6 +113,7 @@ public class CustomRequestLogTest
_serverURI = new URI(String.format("http://%s:%d/", host, localPort));
}
+ @SuppressWarnings("SameParameterValue")
private static SecurityHandler getSecurityHandler(String username, String password, String realm)
{
HashLoginService loginService = new HashLoginService();
@@ -142,6 +145,22 @@ public class CustomRequestLogTest
_server.stop();
}
+ @Test
+ public void testRequestFilter() throws Exception
+ {
+ AtomicReference logRequest = new AtomicReference<>();
+ testHandlerServerStart("RequestPath: %U");
+ _log.setFilter((request, response) -> logRequest.get());
+
+ logRequest.set(true);
+ _connector.getResponse("GET /path HTTP/1.0\n\n");
+ assertThat(_entries.poll(5, TimeUnit.SECONDS), is("RequestPath: /path"));
+
+ logRequest.set(false);
+ _connector.getResponse("GET /path HTTP/1.0\n\n");
+ assertNull(_entries.poll(1, TimeUnit.SECONDS));
+ }
+
@Test
public void testLogRemoteUser() throws Exception
{
@@ -197,16 +216,16 @@ public class CustomRequestLogTest
"%{server}a|%{server}p|" +
"%{client}a|%{client}p");
- Enumeration e = NetworkInterface.getNetworkInterfaces();
+ Enumeration e = NetworkInterface.getNetworkInterfaces();
while (e.hasMoreElements())
{
- NetworkInterface n = (NetworkInterface)e.nextElement();
+ NetworkInterface n = e.nextElement();
if (n.isLoopback())
{
- Enumeration ee = n.getInetAddresses();
+ Enumeration ee = n.getInetAddresses();
while (ee.hasMoreElements())
{
- InetAddress i = (InetAddress)ee.nextElement();
+ InetAddress i = ee.nextElement();
try (Socket client = newSocket(i.getHostAddress(), _serverURI.getPort()))
{
OutputStream os = client.getOutputStream();
@@ -217,7 +236,7 @@ public class CustomRequestLogTest
os.write(request.getBytes(StandardCharsets.ISO_8859_1));
os.flush();
- String[] log = _entries.poll(5, TimeUnit.SECONDS).split("\\|");
+ String[] log = Objects.requireNonNull(_entries.poll(5, TimeUnit.SECONDS)).split("\\|");
assertThat(log.length, is(8));
String localAddr = log[0];
@@ -428,7 +447,7 @@ public class CustomRequestLogTest
_connector.getResponse("GET / HTTP/1.0\n\n");
String log = _entries.poll(5, TimeUnit.SECONDS);
- long requestTime = requestTimes.poll(5, TimeUnit.SECONDS);
+ long requestTime = getTimeRequestReceived();
DateCache dateCache = new DateCache(CustomRequestLog.DEFAULT_DATE_FORMAT, Locale.getDefault(), "GMT");
assertThat(log, is("RequestTime: [" + dateCache.format(requestTime) + "]"));
}
@@ -442,7 +461,8 @@ public class CustomRequestLogTest
_connector.getResponse("GET / HTTP/1.0\n\n");
String log = _entries.poll(5, TimeUnit.SECONDS);
- long requestTime = requestTimes.poll(5, TimeUnit.SECONDS);
+ assertNotNull(log);
+ long requestTime = getTimeRequestReceived();
DateCache dateCache1 = new DateCache("EEE MMM dd HH:mm:ss zzz yyyy", Locale.getDefault(), "GMT");
DateCache dateCache2 = new DateCache("EEE MMM dd HH:mm:ss zzz yyyy", Locale.getDefault(), "EST");
@@ -461,7 +481,8 @@ public class CustomRequestLogTest
_connector.getResponse("GET /delay HTTP/1.0\n\n");
String log = _entries.poll(5, TimeUnit.SECONDS);
- long lowerBound = requestTimes.poll(5, TimeUnit.SECONDS);
+ assertNotNull(log);
+ long lowerBound = getTimeRequestReceived();
long upperBound = System.currentTimeMillis();
long measuredDuration = Long.parseLong(log);
@@ -479,7 +500,8 @@ public class CustomRequestLogTest
_connector.getResponse("GET /delay HTTP/1.0\n\n");
String log = _entries.poll(5, TimeUnit.SECONDS);
- long lowerBound = requestTimes.poll(5, TimeUnit.SECONDS);
+ assertNotNull(log);
+ long lowerBound = getTimeRequestReceived();
long upperBound = System.currentTimeMillis();
long measuredDuration = Long.parseLong(log);
@@ -497,7 +519,8 @@ public class CustomRequestLogTest
_connector.getResponse("GET /delay HTTP/1.0\n\n");
String log = _entries.poll(5, TimeUnit.SECONDS);
- long lowerBound = requestTimes.poll(5, TimeUnit.SECONDS);
+ assertNotNull(log);
+ long lowerBound = getTimeRequestReceived();
long upperBound = System.currentTimeMillis();
long measuredDuration = Long.parseLong(log);
@@ -575,11 +598,6 @@ public class CustomRequestLogTest
fail(log);
}
- protected Socket newSocket() throws Exception
- {
- return newSocket(_serverURI.getHost(), _serverURI.getPort());
- }
-
protected Socket newSocket(String host, int port) throws Exception
{
Socket socket = new Socket(host, port);
@@ -604,10 +622,17 @@ public class CustomRequestLogTest
}
}
+ private long getTimeRequestReceived() throws InterruptedException
+ {
+ Long requestTime = requestTimes.poll(5, TimeUnit.SECONDS);
+ assertNotNull(requestTime);
+ return requestTime;
+ }
+
private class TestServlet extends HttpServlet
{
@Override
- protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(request));
@@ -652,8 +677,7 @@ public class CustomRequestLogTest
if (request.getContentLength() > 0)
{
- InputStream in = request.getInputStream();
- while (in.read() > 0);
+ IO.readBytes(request.getInputStream());
}
}
}
diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/GzipWithSendErrorTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/GzipWithSendErrorTest.java
index c71b048cef3..5261ba53b48 100644
--- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/GzipWithSendErrorTest.java
+++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/GzipWithSendErrorTest.java
@@ -251,7 +251,8 @@ public class GzipWithSendErrorTest
assertThat("Request Input Content Received", inputContentReceived.get(), is(0L));
assertThat("Request Input Content Received less then initial buffer", inputContentReceived.get(), lessThanOrEqualTo((long)sizeActuallySent));
assertThat("Request Connection BytesIn should have some minimal data", inputBytesIn.get(), greaterThanOrEqualTo(1024L));
- assertThat("Request Connection BytesIn read should not have read all of the data", inputBytesIn.get(), lessThanOrEqualTo((long)sizeActuallySent));
+ long requestBytesSent = sizeActuallySent + 512; // Take into account headers and chunked metadata.
+ assertThat("Request Connection BytesIn read should not have read all of the data", inputBytesIn.get(), lessThanOrEqualTo(requestBytesSent));
// Now provide rest
content.offer(ByteBuffer.wrap(compressedRequest, sizeActuallySent, compressedRequest.length - sizeActuallySent));
@@ -351,7 +352,8 @@ public class GzipWithSendErrorTest
assertThat("Request Input Content Received", inputContentReceived.get(), read ? greaterThan(0L) : is(0L));
assertThat("Request Input Content Received less then initial buffer", inputContentReceived.get(), lessThanOrEqualTo((long)sizeActuallySent));
assertThat("Request Connection BytesIn should have some minimal data", inputBytesIn.get(), greaterThanOrEqualTo(1024L));
- assertThat("Request Connection BytesIn read should not have read all of the data", inputBytesIn.get(), lessThanOrEqualTo((long)sizeActuallySent));
+ long requestBytesSent = sizeActuallySent + 512; // Take into account headers and chunked metadata.
+ assertThat("Request Connection BytesIn read should not have read all of the data", inputBytesIn.get(), lessThanOrEqualTo(requestBytesSent));
// Now provide rest
content.offer(ByteBuffer.wrap(compressedRequest, sizeActuallySent, compressedRequest.length - sizeActuallySent));
diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/HttpInputInterceptorTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/HttpInputInterceptorTest.java
new file mode 100644
index 00000000000..30824c514ca
--- /dev/null
+++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/HttpInputInterceptorTest.java
@@ -0,0 +1,337 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 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.nio.ByteBuffer;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.servlet.AsyncContext;
+import javax.servlet.ReadListener;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.util.AsyncRequestContent;
+import org.eclipse.jetty.client.util.BytesRequestContent;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.HttpInput;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HttpInputInterceptorTest
+{
+ private Server server;
+ private HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory();
+ private ServerConnector connector;
+ private HttpClient client;
+
+ private void start(Handler handler) throws Exception
+ {
+ server = new Server();
+ connector = new ServerConnector(server, 1, 1, httpConnectionFactory);
+ server.addConnector(connector);
+
+ server.setHandler(handler);
+
+ client = new HttpClient();
+ server.addBean(client);
+
+ server.start();
+ }
+
+ @AfterEach
+ public void dispose()
+ {
+ LifeCycle.stop(server);
+ }
+
+ @Test
+ public void testBlockingReadInterceptorThrows() throws Exception
+ {
+ CountDownLatch serverLatch = new CountDownLatch(1);
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
+ {
+ jettyRequest.setHandled(true);
+
+ // Throw immediately from the interceptor.
+ jettyRequest.getHttpInput().addInterceptor(content ->
+ {
+ throw new RuntimeException();
+ });
+
+ assertThrows(IOException.class, () -> IO.readBytes(request.getInputStream()));
+ serverLatch.countDown();
+ response.setStatus(HttpStatus.NO_CONTENT_204);
+ }
+ });
+
+ ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
+ .method(HttpMethod.POST)
+ .body(new BytesRequestContent(new byte[1]))
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ assertTrue(serverLatch.await(5, TimeUnit.SECONDS));
+ assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus());
+ }
+
+ @Test
+ public void testBlockingReadInterceptorConsumesHalfThenThrows() throws Exception
+ {
+ CountDownLatch serverLatch = new CountDownLatch(1);
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
+ {
+ jettyRequest.setHandled(true);
+
+ // Consume some and then throw.
+ AtomicInteger readCount = new AtomicInteger();
+ jettyRequest.getHttpInput().addInterceptor(content ->
+ {
+ int reads = readCount.incrementAndGet();
+ if (reads == 1)
+ {
+ ByteBuffer buffer = content.getByteBuffer();
+ int half = buffer.remaining() / 2;
+ int limit = buffer.limit();
+ buffer.limit(buffer.position() + half);
+ ByteBuffer chunk = buffer.slice();
+ buffer.position(buffer.limit());
+ buffer.limit(limit);
+ return new HttpInput.Content(chunk);
+ }
+ throw new RuntimeException();
+ });
+
+ assertThrows(IOException.class, () -> IO.readBytes(request.getInputStream()));
+ serverLatch.countDown();
+ response.setStatus(HttpStatus.NO_CONTENT_204);
+ }
+ });
+
+ ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
+ .method(HttpMethod.POST)
+ .body(new BytesRequestContent(new byte[1024]))
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ assertTrue(serverLatch.await(5, TimeUnit.SECONDS));
+ assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus());
+ }
+
+ @Test
+ public void testAvailableReadInterceptorThrows() throws Exception
+ {
+ CountDownLatch interceptorLatch = new CountDownLatch(1);
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ jettyRequest.setHandled(true);
+
+ // Throw immediately from the interceptor.
+ jettyRequest.getHttpInput().addInterceptor(content ->
+ {
+ interceptorLatch.countDown();
+ throw new RuntimeException();
+ });
+
+ int available = request.getInputStream().available();
+ assertEquals(0, available);
+ }
+ });
+
+ ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
+ .method(HttpMethod.POST)
+ .body(new BytesRequestContent(new byte[1]))
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ assertTrue(interceptorLatch.await(5, TimeUnit.SECONDS));
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ }
+
+ @Test
+ public void testIsReadyReadInterceptorThrows() throws Exception
+ {
+ AsyncRequestContent asyncRequestContent = new AsyncRequestContent(ByteBuffer.wrap(new byte[1]));
+ CountDownLatch interceptorLatch = new CountDownLatch(1);
+ CountDownLatch readFailureLatch = new CountDownLatch(1);
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ jettyRequest.setHandled(true);
+
+ AtomicBoolean onDataAvailable = new AtomicBoolean();
+ jettyRequest.getHttpInput().addInterceptor(content ->
+ {
+ if (onDataAvailable.get())
+ {
+ interceptorLatch.countDown();
+ throw new RuntimeException();
+ }
+ else
+ {
+ return content;
+ }
+ });
+
+ AsyncContext asyncContext = request.startAsync();
+ ServletInputStream input = request.getInputStream();
+ input.setReadListener(new ReadListener()
+ {
+ @Override
+ public void onDataAvailable()
+ {
+ onDataAvailable.set(true);
+
+ // The input.setReadListener() call called the interceptor so there is content for read().
+ assertThat(input.isReady(), is(true));
+ assertDoesNotThrow(() -> assertEquals(0, input.read()));
+
+ // Make the client send more content so that the interceptor will be called again.
+ asyncRequestContent.offer(ByteBuffer.wrap(new byte[1]));
+ asyncRequestContent.close();
+ sleep(500); // Wait a little to make sure the content arrived by next isReady() call.
+
+ // The interceptor should throw, but isReady() should not.
+ assertThat(input.isReady(), is(true));
+ assertThrows(IOException.class, () -> assertEquals(0, input.read()));
+ readFailureLatch.countDown();
+ response.setStatus(HttpStatus.NO_CONTENT_204);
+ asyncContext.complete();
+ }
+
+ @Override
+ public void onAllDataRead()
+ {
+ }
+
+ @Override
+ public void onError(Throwable error)
+ {
+ error.printStackTrace();
+ }
+ });
+ }
+ });
+
+ ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
+ .method(HttpMethod.POST)
+ .body(asyncRequestContent)
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ assertTrue(interceptorLatch.await(5, TimeUnit.SECONDS));
+ assertTrue(readFailureLatch.await(5, TimeUnit.SECONDS));
+ assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus());
+ }
+
+ @Test
+ public void testSetReadListenerReadInterceptorThrows() throws Exception
+ {
+ RuntimeException failure = new RuntimeException();
+ CountDownLatch interceptorLatch = new CountDownLatch(1);
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ jettyRequest.setHandled(true);
+
+ // Throw immediately from the interceptor.
+ jettyRequest.getHttpInput().addInterceptor(content ->
+ {
+ interceptorLatch.countDown();
+ failure.addSuppressed(new Throwable());
+ throw failure;
+ });
+
+ AsyncContext asyncContext = request.startAsync();
+ ServletInputStream input = request.getInputStream();
+ input.setReadListener(new ReadListener()
+ {
+ @Override
+ public void onDataAvailable()
+ {
+ }
+
+ @Override
+ public void onAllDataRead()
+ {
+ }
+
+ @Override
+ public void onError(Throwable error)
+ {
+ assertSame(failure, error.getCause());
+ response.setStatus(HttpStatus.NO_CONTENT_204);
+ asyncContext.complete();
+ }
+ });
+ }
+ });
+
+ ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
+ .method(HttpMethod.POST)
+ .body(new BytesRequestContent(new byte[1]))
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ assertTrue(interceptorLatch.await(5, TimeUnit.SECONDS));
+ assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus());
+ }
+
+ private static void sleep(long time)
+ {
+ try
+ {
+ Thread.sleep(time);
+ }
+ catch (InterruptedException x)
+ {
+ throw new RuntimeException(x);
+ }
+ }
+}
diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SameNodeLoadTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/ConcurrencyTest.java
similarity index 92%
rename from tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SameNodeLoadTest.java
rename to tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/ConcurrencyTest.java
index a49cfda1c1e..93798a411d8 100644
--- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SameNodeLoadTest.java
+++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/ConcurrencyTest.java
@@ -37,14 +37,14 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
- * SameNodeLoadTest
+ * ConcurrencyTest
*
- * This test performs multiple concurrent requests for the same session on the same node.
+ * This test performs multiple concurrent requests from different clients
+ * for the same session on the same node.
*/
-public class SameNodeLoadTest
+public class ConcurrencyTest
{
@Test
- @DisabledIfSystemProperty(named = "env", matches = "ci") // TODO: SLOW, needs review
public void testLoad() throws Exception
{
DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory();
@@ -68,17 +68,18 @@ public class SameNodeLoadTest
{
String url = "http://localhost:" + port1 + contextPath + servletMapping;
- //create session via first server
+ //create session upfront so the session id is established and
+ //can be shared to all clients
ContentResponse response1 = client.GET(url + "?action=init");
assertEquals(HttpServletResponse.SC_OK, response1.getStatus());
String sessionCookie = response1.getHeaders().get("Set-Cookie");
assertTrue(sessionCookie != null);
- //simulate 10 clients making 100 requests each
+ //simulate 10 clients making 10 requests each for the same session
ExecutorService executor = Executors.newCachedThreadPool();
int clientsCount = 10;
CyclicBarrier barrier = new CyclicBarrier(clientsCount + 1);
- int requestsCount = 100;
+ int requestsCount = 10;
Worker[] workers = new Worker[clientsCount];
for (int i = 0; i < clientsCount; ++i)
{
@@ -96,7 +97,9 @@ public class SameNodeLoadTest
System.err.println("Elapsed ms:" + elapsed);
executor.shutdownNow();
- // Perform one request to get the result
+ // Perform one request to get the result - the session
+ // should have counted all the requests by incrementing
+ // a counter in an attribute.
Request request = client.newRequest(url + "?action=result");
ContentResponse response2 = request.send();
assertEquals(HttpServletResponse.SC_OK, response2.getStatus());