From 5d1a9f6f2edce84ee2994d11f9013142c70c5b35 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 23 Jul 2024 16:54:54 +1000 Subject: [PATCH 01/61] Revert delayed dispatch handling to the connections. --- .../server/internal/HttpChannelState.java | 16 +- .../jetty/server/internal/HttpConnection.java | 49 +++- .../jetty/server/HttpConnectionTest.java | 80 ++++++ .../jetty/server/handler/DumpHandler.java | 261 +++++++++--------- .../handler/ThreadLimitHandlerTest.java | 19 +- 5 files changed, 287 insertions(+), 138 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java index e97331b573ae..04d082a0fbbe 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java @@ -690,18 +690,22 @@ public void run() @Override public void succeeded() { - HttpStream stream; - boolean completeStream; + HttpStream completeStream = null; + Throwable failure = null; try (AutoLock ignored = _lock.lock()) { assert _callbackCompleted; _streamSendState = StreamSendState.LAST_COMPLETE; - completeStream = _handling == null; - stream = _stream; + if (_handling == null) + { + completeStream = _stream; + _stream = null; + failure = _callbackFailure; + } } - if (completeStream) - completeStream(stream, null); + if (completeStream != null) + completeStream(completeStream, failure); } /** diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index 2027b6542fc0..7a1e4da0066b 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -106,6 +106,7 @@ public class HttpConnection extends AbstractMetaDataConnection implements Runnab private volatile RetainableByteBuffer _retainableByteBuffer; private HttpFields.Mutable _trailers; private Runnable _onRequest; + private boolean _delayedForContent; private long _requests; // TODO why is this not on HttpConfiguration? private boolean _useInputDirectByteBuffers; @@ -596,7 +597,31 @@ public boolean onIdleExpired(TimeoutException timeout) { if (_httpChannel.getRequest() == null) return true; - Runnable task = _httpChannel.onIdleTimeout(timeout); + + Runnable task; + if (_delayedForContent && _onRequest != null) + { + Runnable onRequest = _onRequest; + _onRequest = null; + task = () -> + { + try + { + onRequest.run(); + } + finally + { + _handling.set(false); + Runnable next = _httpChannel.onIdleTimeout(timeout); + if (next != null) + getExecutor().execute(next); + } + }; + } + else + { + task = _httpChannel.onIdleTimeout(timeout); + } if (task != null) getExecutor().execute(task); return false; // We've handle the exception @@ -940,6 +965,7 @@ public void startRequest(String method, String uri, HttpVersion version) throw new IllegalStateException("Stream pending"); _headerBuilder.clear(); _httpChannel.setHttpStream(stream); + _delayedForContent = false; } @Override @@ -951,8 +977,18 @@ public void parsedHeader(HttpField field) @Override public boolean headerComplete() { - _onRequest = _stream.get().headerComplete(); - return true; + HttpStreamOverHTTP1 stream = _stream.get(); + _onRequest = stream.headerComplete(); + + // Should we delay dispatch until we have some content? + // We should not delay if there is no content expect or client is expecting 100 or the response is already committed or the request buffer already has something in it to parse + _delayedForContent = getHttpConfiguration().isDelayDispatchUntilContent() && + (_parser.getContentLength() > 0 || _parser.isChunking()) && + !stream._expects100Continue && + !stream.isCommitted() && + _retainableByteBuffer != null && _retainableByteBuffer.isEmpty(); + + return !_delayedForContent; } @Override @@ -967,15 +1003,18 @@ public boolean content(ByteBuffer buffer) _retainableByteBuffer.retain(); stream._chunk = Content.Chunk.asChunk(buffer, false, _retainableByteBuffer); + _delayedForContent = false; return true; } @Override public boolean contentComplete() { - // Do nothing at this point. + // Do nothing at this point unless we delayed for content // Wait for messageComplete so any trailers can be sent as special content - return false; + boolean delayed = _delayedForContent; + _delayedForContent = false; + return delayed; } @Override diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java index cef0156fddca..dbb06f5a26ed 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java @@ -34,6 +34,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import org.awaitility.Awaitility; import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpParser; @@ -48,6 +49,7 @@ import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.NanoTime; +import org.eclipse.jetty.util.statistic.CounterStatistic; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -1083,6 +1085,84 @@ public void testConnection() throws Exception } } + /** + * Ensure that excessively large hexadecimal chunk body length is parsed properly. + */ + @Test + public void testDelayedDispatch() throws Exception + { + try (LocalConnector.LocalEndPoint connection = _connector.connect()) + { + CounterStatistic dumpCounter = _server.getBean(DumpHandler.class).getHandledCounter(); + + // Dispatch with content + connection.addInput(""" + POST /test HTTP/1.1\r + Host: localhost\r + Content-Length: 5\r + Content-Type: text/plain; charset=utf-8\r + \r + 12345 + """ + ); + + Awaitility.waitAtMost(1, TimeUnit.SECONDS).until(() -> dumpCounter.getTotal() == 1L); + Awaitility.waitAtMost(1, TimeUnit.SECONDS).until(() -> dumpCounter.getCurrent() == 0L); + + String raw = connection.getResponse(); + assertThat(raw, containsString("200 OK")); + + // Dispatch delayed for content + dumpCounter.reset(); + connection.addInput(""" + POST /test HTTP/1.1\r + Host: localhost\r + Content-Length: 5\r + Content-Type: text/plain; charset=utf-8\r + \r + """ + ); + + Thread.sleep(10); + assertThat(dumpCounter.getTotal(), is(0L)); + assertThat(dumpCounter.getCurrent(), is(0L)); + + connection.addInput("12345"); + Awaitility.waitAtMost(1, TimeUnit.SECONDS).until(() -> dumpCounter.getTotal() == 1L); + Awaitility.waitAtMost(1, TimeUnit.SECONDS).until(() -> dumpCounter.getCurrent() == 0L); + + raw = connection.getResponse(); + assertThat(raw, containsString("200 OK")); + + // Dispatch delayed for chunked content + dumpCounter.reset(); + connection.addInput(""" + POST /test HTTP/1.1\r + Host: localhost\r + Transfer-Encoding: chunked\r + Content-Type: text/plain; charset=utf-8\r + \r + """ + ); + + Thread.sleep(10); + assertThat(dumpCounter.getTotal(), is(0L)); + assertThat(dumpCounter.getCurrent(), is(0L)); + + connection.addInput(""" + 5;\r + 12345\r + 0;\r + \r + """); + Awaitility.waitAtMost(1, TimeUnit.SECONDS).until(() -> dumpCounter.getTotal() == 1L); + Awaitility.waitAtMost(1, TimeUnit.SECONDS).until(() -> dumpCounter.getCurrent() == 0L); + + raw = connection.getResponse(); + assertThat(raw, containsString("200 OK")); + } + } + /** * Creates a request header over 1k in size, by creating a single header entry with an huge value. * diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java index 8e4037113556..bf53bc6c895a 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java @@ -32,6 +32,7 @@ import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.Utf8StringBuilder; +import org.eclipse.jetty.util.statistic.CounterStatistic; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,6 +47,7 @@ public class DumpHandler extends Handler.Abstract private final Blocker.Shared _blocker = new Blocker.Shared(); private final String _label; + private final CounterStatistic _handled = new CounterStatistic(); public DumpHandler() { @@ -57,163 +59,176 @@ public DumpHandler(String label) _label = label; } + public CounterStatistic getHandledCounter() + { + return _handled; + } + @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - if (LOG.isDebugEnabled()) - LOG.debug("dump {}", request); - HttpURI httpURI = request.getHttpURI(); + try + { + _handled.increment(); + if (LOG.isDebugEnabled()) + LOG.debug("dump {}", request); + HttpURI httpURI = request.getHttpURI(); - Fields params = Request.extractQueryParameters(request); + Fields params = Request.extractQueryParameters(request); - if (Boolean.parseBoolean(params.getValue("flush"))) - { - try (Blocker.Callback blocker = _blocker.callback()) + if (Boolean.parseBoolean(params.getValue("flush"))) { - response.write(false, null, blocker); - blocker.block(); + try (Blocker.Callback blocker = _blocker.callback()) + { + response.write(false, null, blocker); + blocker.block(); + } } - } - - if (Boolean.parseBoolean(params.getValue("empty"))) - { - response.setStatus(200); - callback.succeeded(); - return true; - } - Utf8StringBuilder read = null; - if (params.getValue("read") != null) - { - read = new Utf8StringBuilder(); - int len = Integer.parseInt(params.getValue("read")); - byte[] buffer = new byte[8192]; + if (Boolean.parseBoolean(params.getValue("empty"))) + { + response.setStatus(200); + callback.succeeded(); + return true; + } - Content.Chunk chunk = null; - while (len > 0) + Utf8StringBuilder read = null; + if (params.getValue("read") != null) { - if (chunk == null) + read = new Utf8StringBuilder(); + int len = Integer.parseInt(params.getValue("read")); + byte[] buffer = new byte[8192]; + + Content.Chunk chunk = null; + while (len > 0) { - chunk = request.read(); if (chunk == null) { - try (Blocker.Runnable blocker = _blocker.runnable()) + chunk = request.read(); + if (chunk == null) { - request.demand(blocker); - blocker.block(); + try (Blocker.Runnable blocker = _blocker.runnable()) + { + request.demand(blocker); + blocker.block(); + } + continue; } - continue; } - } - if (Content.Chunk.isFailure(chunk)) - { - callback.failed(chunk.getFailure()); - return true; - } + if (Content.Chunk.isFailure(chunk)) + { + callback.failed(chunk.getFailure()); + return true; + } - int l = Math.min(buffer.length, Math.min(len, chunk.remaining())); - int r = chunk.get(buffer, 0, l); - read.append(buffer, 0, r); - len -= r; + int l = Math.min(buffer.length, Math.min(len, chunk.remaining())); + int r = chunk.get(buffer, 0, l); + read.append(buffer, 0, r); + len -= r; - if (!chunk.hasRemaining()) - { - boolean last = chunk.isLast(); - chunk.release(); - chunk = null; - if (last) - break; + if (!chunk.hasRemaining()) + { + boolean last = chunk.isLast(); + chunk.release(); + chunk = null; + if (last) + break; + } } + if (chunk != null) + chunk.release(); } - if (chunk != null) - chunk.release(); - } - if (params.getValue("date") != null) - response.getHeaders().put("Date", params.getValue("date")); + if (params.getValue("date") != null) + response.getHeaders().put("Date", params.getValue("date")); - if (params.getValue("ISE") != null) - throw new IllegalStateException("Testing ISE"); + if (params.getValue("ISE") != null) + throw new IllegalStateException("Testing ISE"); - if (params.getValue("error") != null) - { - response.setStatus(Integer.parseInt(params.getValue("error"))); - callback.succeeded(); - return true; - } + if (params.getValue("error") != null) + { + response.setStatus(Integer.parseInt(params.getValue("error"))); + callback.succeeded(); + return true; + } - response.getHeaders().put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.TEXT_HTML.asString()); - - ByteArrayOutputStream buf = new ByteArrayOutputStream(2048); - Writer writer = new OutputStreamWriter(buf, StandardCharsets.ISO_8859_1); - writer.write("

" + _label + "

\n"); - writer.write("
httpURI=" + httpURI + "

\n"); - writer.write("
httpURI.path=" + httpURI.getPath() + "

\n"); - writer.write("
httpURI.query=" + httpURI.getQuery() + "

\n"); - writer.write("
httpURI.pathQuery=" + httpURI.getPathQuery() + "

\n"); - writer.write("
locales=" + Request.getLocales(request).stream().map(Locale::toLanguageTag).toList() + "

\n"); - writer.write("
pathInContext=" + Request.getPathInContext(request) + "

\n"); - writer.write("
contentType=" + request.getHeaders().get(HttpHeader.CONTENT_TYPE) + "

\n"); - writer.write("
servername=" + Request.getServerName(request) + "

\n"); - writer.write("
local=" + Request.getLocalAddr(request) + ":" + Request.getLocalPort(request) + "

\n"); - writer.write("
remote=" + Request.getRemoteAddr(request) + ":" + Request.getRemotePort(request) + "

\n"); - writer.write("

Header:

");
-        writer.write(String.format("%4s %s %s\n", request.getMethod(), httpURI.getPathQuery(), request.getConnectionMetaData().getProtocol()));
-        for (HttpField field : request.getHeaders())
-        {
-            String name = field.getName();
-            writer.write(name);
-            writer.write(": ");
-            String value = field.getValue();
-            writer.write(value == null ? "" : value);
-            writer.write("\n");
-        }
+            response.getHeaders().put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.TEXT_HTML.asString());
+
+            ByteArrayOutputStream buf = new ByteArrayOutputStream(2048);
+            Writer writer = new OutputStreamWriter(buf, StandardCharsets.ISO_8859_1);
+            writer.write("

" + _label + "

\n"); + writer.write("
httpURI=" + httpURI + "

\n"); + writer.write("
httpURI.path=" + httpURI.getPath() + "

\n"); + writer.write("
httpURI.query=" + httpURI.getQuery() + "

\n"); + writer.write("
httpURI.pathQuery=" + httpURI.getPathQuery() + "

\n"); + writer.write("
locales=" + Request.getLocales(request).stream().map(Locale::toLanguageTag).toList() + "

\n"); + writer.write("
pathInContext=" + Request.getPathInContext(request) + "

\n"); + writer.write("
contentType=" + request.getHeaders().get(HttpHeader.CONTENT_TYPE) + "

\n"); + writer.write("
servername=" + Request.getServerName(request) + "

\n"); + writer.write("
local=" + Request.getLocalAddr(request) + ":" + Request.getLocalPort(request) + "

\n"); + writer.write("
remote=" + Request.getRemoteAddr(request) + ":" + Request.getRemotePort(request) + "

\n"); + writer.write("

Header:

");
+            writer.write(String.format("%4s %s %s\n", request.getMethod(), httpURI.getPathQuery(), request.getConnectionMetaData().getProtocol()));
+            for (HttpField field : request.getHeaders())
+            {
+                String name = field.getName();
+                writer.write(name);
+                writer.write(": ");
+                String value = field.getValue();
+                writer.write(value == null ? "" : value);
+                writer.write("\n");
+            }
 
-        writer.write("
\n

Attributes:

\n
");
-        for (String attr : request.getAttributeNameSet())
-        {
-            writer.write(attr);
-            writer.write("=");
-            writer.write(request.getAttribute(attr).toString());
-            writer.write("\n");
-        }
+            writer.write("
\n

Attributes:

\n
");
+            for (String attr : request.getAttributeNameSet())
+            {
+                writer.write(attr);
+                writer.write("=");
+                writer.write(request.getAttribute(attr).toString());
+                writer.write("\n");
+            }
 
-        writer.write("
\n

Content:

\n
");
-        if (read != null)
-            writer.write(read.toCompleteString());
-        else
-            writer.write(Content.Source.asString(request));
+            writer.write("
\n

Content:

\n
");
+            if (read != null)
+                writer.write(read.toCompleteString());
+            else
+                writer.write(Content.Source.asString(request));
 
-        writer.write("
\n"); - writer.write("\n"); - writer.flush(); + writer.write("
\n"); + writer.write("\n"); + writer.flush(); - // commit now - if (!Boolean.parseBoolean(params.getValue("no-content-length"))) - response.getHeaders().put(HttpHeader.CONTENT_LENGTH, buf.size() + 1000); + // commit now + if (!Boolean.parseBoolean(params.getValue("no-content-length"))) + response.getHeaders().put(HttpHeader.CONTENT_LENGTH, buf.size() + 1000); - response.getHeaders().add("Before-Flush", response.isCommitted() ? "Committed???" : "Not Committed"); + response.getHeaders().add("Before-Flush", response.isCommitted() ? "Committed???" : "Not Committed"); - try (Blocker.Callback blocker = _blocker.callback()) - { - response.write(false, BufferUtil.toBuffer(buf.toByteArray()), blocker); - blocker.block(); - } - response.getHeaders().add("After-Flush", "These headers should not be seen in the response!!!"); - String value = response.isCommitted() ? "Committed" : "Not Committed?"; - response.getHeaders().add("After-Flush", value); + try (Blocker.Callback blocker = _blocker.callback()) + { + response.write(false, BufferUtil.toBuffer(buf.toByteArray()), blocker); + blocker.block(); + } + response.getHeaders().add("After-Flush", "These headers should not be seen in the response!!!"); + String value = response.isCommitted() ? "Committed" : "Not Committed?"; + response.getHeaders().add("After-Flush", value); + + // write remaining content after commit + String padding = "ABCDEFGHIJ".repeat(99) + "ABCDEFGH\r\n"; - // write remaining content after commit - String padding = "ABCDEFGHIJ".repeat(99) + "ABCDEFGH\r\n"; + try (Blocker.Callback blocker = _blocker.callback()) + { + response.write(true, BufferUtil.toBuffer(padding.getBytes(StandardCharsets.ISO_8859_1)), blocker); + blocker.block(); + } - try (Blocker.Callback blocker = _blocker.callback()) + callback.succeeded(); + return true; + } + finally { - response.write(true, BufferUtil.toBuffer(padding.getBytes(StandardCharsets.ISO_8859_1)), blocker); - blocker.block(); + _handled.decrement(); } - - callback.succeeded(); - return true; } } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ThreadLimitHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ThreadLimitHandlerTest.java index dd7d58152365..060b80a149da 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ThreadLimitHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ThreadLimitHandlerTest.java @@ -42,6 +42,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertTrue; public class ThreadLimitHandlerTest @@ -280,6 +281,16 @@ public boolean handle(Request request, Response response, Callback callback) thr @Override public void run() { + // Read the first byte we know is there. This is to get around any delayed dispatch + if (read.get() == 0) + { + Content.Chunk chunk = request.read(); + assertThat(chunk, notNullValue()); + assertThat(chunk.remaining(), is(1)); + read.incrementAndGet(); + request.demand(this); + return; + } count.incrementAndGet(); try { @@ -331,7 +342,7 @@ public void run() for (int i = 0; i < client.length; i++) { client[i] = new Socket("127.0.0.1", _connector.getLocalPort()); - client[i].getOutputStream().write(("POST /" + i + " HTTP/1.0\r\nForwarded: for=1.2.3.4\r\nContent-Length: 2\r\n\r\n").getBytes()); + client[i].getOutputStream().write(("POST /" + i + " HTTP/1.0\r\nForwarded: for=1.2.3.4\r\nContent-Length: 3\r\n\r\nX").getBytes()); client[i].getOutputStream().flush(); } @@ -344,7 +355,7 @@ public void run() // Send some content for the clients for (Socket socket : client) { - socket.getOutputStream().write('X'); + socket.getOutputStream().write('Y'); socket.getOutputStream().flush(); } @@ -364,7 +375,7 @@ public void run() // Send the rest of the content for the clients for (Socket socket : client) { - socket.getOutputStream().write('Y'); + socket.getOutputStream().write('Z'); socket.getOutputStream().flush(); } @@ -373,7 +384,7 @@ public void run() { response = IO.toString(socket.getInputStream()); assertThat(response, containsString(" 200 OK")); - assertThat(response, containsString(" read 2")); + assertThat(response, containsString(" read 3")); } await().atMost(5, TimeUnit.SECONDS).until(handler::getRemoteCount, is(0)); From 8c072781656896ed891807543b90a6a94d03bfba Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 23 Jul 2024 17:53:47 +1000 Subject: [PATCH 02/61] Revert delayed dispatch handling to the connections. --- .../test/java/org/eclipse/jetty/ee10/servlet/ServletTest.java | 2 ++ .../test/java/org/eclipse/jetty/ee11/servlet/ServletTest.java | 2 ++ .../test/java/org/eclipse/jetty/ee9/servlet/ServletTest.java | 2 ++ 3 files changed, 6 insertions(+) diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletTest.java index 23dd4839ce98..59551d07f4fd 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletTest.java @@ -22,6 +22,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.MappingMatch; +import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.IO; @@ -130,6 +131,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I }, "/post"); _connector.setIdleTimeout(idleTimeout); + _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setDelayDispatchUntilContent(false); _server.start(); try (LocalConnector.LocalEndPoint endPoint = _connector.connect()) diff --git a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ServletTest.java b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ServletTest.java index f15bec7a39e8..64c1dea84abe 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ServletTest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ServletTest.java @@ -22,6 +22,7 @@ import jakarta.servlet.http.HttpServletMapping; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.IO; @@ -132,6 +133,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I }, "/post"); _connector.setIdleTimeout(idleTimeout); + _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setDelayDispatchUntilContent(false); _server.start(); try (LocalConnector.LocalEndPoint endPoint = _connector.connect()) diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ServletTest.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ServletTest.java index 927633a324f3..6f21807398c7 100644 --- a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ServletTest.java +++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ServletTest.java @@ -20,6 +20,7 @@ import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.IO; @@ -127,6 +128,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I }), "/post"); _connector.setIdleTimeout(idleTimeout); + _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setDelayDispatchUntilContent(false); _server.start(); try (LocalConnector.LocalEndPoint endPoint = _connector.connect()) From 548770c4c5cd952da933c93405dd7e4471794d09 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 6 Aug 2024 17:14:36 +1000 Subject: [PATCH 03/61] Cleanup of HttpConnection --- .../eclipse/jetty/io/AbstractConnection.java | 5 +- .../jetty/server/HttpConnectionFactory.java | 35 +----- .../jetty/server/internal/HttpConnection.java | 104 +++++++----------- 3 files changed, 45 insertions(+), 99 deletions(-) diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java index 3f40f11c3902..ba0e3e1b604a 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java @@ -52,12 +52,9 @@ protected AbstractConnection(EndPoint endPoint, Executor executor) _readCallback = new ReadCallback(); } - @Deprecated @Override public InvocationType getInvocationType() { - // TODO consider removing the #fillInterested method from the connection and only use #fillInterestedCallback - // so a connection need not be Invocable return Invocable.super.getInvocationType(); } @@ -170,7 +167,7 @@ public boolean isFillInterested() protected void onFillInterestedFailed(Throwable cause) { if (LOG.isDebugEnabled()) - LOG.debug("{} onFillInterestedFailed {}", this, cause); + LOG.debug("onFillInterestedFailed {}", this, cause); if (_endPoint.isOpen()) { boolean close = true; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java index 30b5d9b5067a..c6a5249d745c 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java @@ -15,14 +15,11 @@ import java.util.Objects; -import org.eclipse.jetty.http.ComplianceViolation; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.internal.HttpConnection; import org.eclipse.jetty.util.annotation.Name; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * A Connection Factory for HTTP Connections. @@ -32,7 +29,6 @@ */ public class HttpConnectionFactory extends AbstractConnectionFactory implements HttpConfiguration.ConnectionFactory { - private static final Logger LOG = LoggerFactory.getLogger(HttpConnectionFactory.class); private final HttpConfiguration _config; private boolean _useInputDirectByteBuffers; private boolean _useOutputDirectByteBuffers; @@ -57,45 +53,26 @@ public HttpConfiguration getHttpConfiguration() return _config; } - /** - * @deprecated use {@link HttpConfiguration#getComplianceViolationListeners()} instead to know if there - * are any {@link ComplianceViolation.Listener} to notify. this method will be removed in Jetty 12.1.0 - */ - @Deprecated(since = "12.0.6", forRemoval = true) - public boolean isRecordHttpComplianceViolations() - { - return !_config.getComplianceViolationListeners().isEmpty(); - } - - /** - * Does nothing. - * @deprecated use {@link HttpConfiguration#addComplianceViolationListener(ComplianceViolation.Listener)} instead. - * this method will be removed in Jetty 12.1.0 - */ - @Deprecated(since = "12.0.6", forRemoval = true) - public void setRecordHttpComplianceViolations(boolean recordHttpComplianceViolations) - { - _config.addComplianceViolationListener(new ComplianceViolation.LoggingListener()); - } - public boolean isUseInputDirectByteBuffers() { - return _useInputDirectByteBuffers; + return _config.isUseInputDirectByteBuffers(); } + @Deprecated(forRemoval = true, since = "12.1.0") public void setUseInputDirectByteBuffers(boolean useInputDirectByteBuffers) { - _useInputDirectByteBuffers = useInputDirectByteBuffers; + _config.setUseInputDirectByteBuffers(useInputDirectByteBuffers); } public boolean isUseOutputDirectByteBuffers() { - return _useOutputDirectByteBuffers; + return _config.isUseOutputDirectByteBuffers(); } + @Deprecated(forRemoval = true, since = "12.1.0") public void setUseOutputDirectByteBuffers(boolean useOutputDirectByteBuffers) { - _useOutputDirectByteBuffers = useOutputDirectByteBuffers; + _config.setUseOutputDirectByteBuffers(useOutputDirectByteBuffers); } @Override diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index 7a1e4da0066b..4b0b0e16aa76 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -24,7 +24,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.atomic.LongAdder; import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.ComplianceViolation; @@ -99,8 +98,6 @@ public class HttpConnection extends AbstractMetaDataConnection implements Runnab private final Lazy _attributes = new Lazy(); private final DemandContentCallback _demandContentCallback = new DemandContentCallback(); private final SendCallback _sendCallback = new SendCallback(); - private final LongAdder bytesIn = new LongAdder(); - private final LongAdder bytesOut = new LongAdder(); private final AtomicBoolean _handling = new AtomicBoolean(false); private final HttpFields.Mutable _headerBuilder = HttpFields.build(); private volatile RetainableByteBuffer _retainableByteBuffer; @@ -108,9 +105,9 @@ public class HttpConnection extends AbstractMetaDataConnection implements Runnab private Runnable _onRequest; private boolean _delayedForContent; private long _requests; - // TODO why is this not on HttpConfiguration? - private boolean _useInputDirectByteBuffers; - private boolean _useOutputDirectByteBuffers; + private long _responses; + private long _bytesIn; + private long _bytesOut; /** * Get the current connection that this thread is dispatched to. @@ -132,15 +129,6 @@ protected static HttpConnection setCurrentConnection(HttpConnection connection) return last; } - /** - * @deprecated use {@link #HttpConnection(HttpConfiguration, Connector, EndPoint)} instead. Will be removed in Jetty 12.1.0 - */ - @Deprecated(since = "12.0.6", forRemoval = true) - public HttpConnection(HttpConfiguration configuration, Connector connector, EndPoint endPoint, boolean recordComplianceViolations) - { - this(configuration, connector, endPoint); - } - public HttpConnection(HttpConfiguration configuration, Connector connector, EndPoint endPoint) { super(connector, configuration, endPoint); @@ -160,15 +148,6 @@ public InvocationType getInvocationType() return getServer().getInvocationType(); } - /** - * @deprecated No replacement, no longer used within {@link HttpConnection}, will be removed in Jetty 12.1.0 - */ - @Deprecated(since = "12.0.6", forRemoval = true) - public boolean isRecordHttpComplianceViolations() - { - return false; - } - protected HttpGenerator newHttpGenerator() { return new HttpGenerator(); @@ -291,29 +270,29 @@ public long getMessagesIn() @Override public long getMessagesOut() { - return _requests; // TODO not strictly correct + return _responses; } public boolean isUseInputDirectByteBuffers() { - return _useInputDirectByteBuffers; + return getHttpConfiguration().isUseInputDirectByteBuffers(); } + @Deprecated(forRemoval = true, since = "12.1.0") public void setUseInputDirectByteBuffers(boolean useInputDirectByteBuffers) { - // TODO why is this not on HttpConfiguration? - _useInputDirectByteBuffers = useInputDirectByteBuffers; + getHttpConfiguration().setUseInputDirectByteBuffers(useInputDirectByteBuffers); } public boolean isUseOutputDirectByteBuffers() { - return _useOutputDirectByteBuffers; + return getHttpConfiguration().isUseOutputDirectByteBuffers(); } + @Deprecated(forRemoval = true, since = "12.1.0") public void setUseOutputDirectByteBuffers(boolean useOutputDirectByteBuffers) { - // TODO why is this not on HttpConfiguration? - _useOutputDirectByteBuffers = useOutputDirectByteBuffers; + getHttpConfiguration().setUseOutputDirectByteBuffers(useOutputDirectByteBuffers); } @Override @@ -352,7 +331,14 @@ void releaseRequestBuffer() private ByteBuffer getRequestBuffer() { if (_retainableByteBuffer == null) + { + _retainableByteBuffer = _bufferPool.acquire(getInputBufferSize(), isUseInputDirectByteBuffers()); + } + else if (_retainableByteBuffer.isRetained()) + { + _retainableByteBuffer.release(); _retainableByteBuffer = _bufferPool.acquire(getInputBufferSize(), isUseInputDirectByteBuffers()); + } return _retainableByteBuffer.getByteBuffer(); } @@ -403,9 +389,9 @@ public void onFillable() _onRequest = null; onRequest.run(); - // If the _handling boolean has already been CaS'd to false, then stream is completed and we are no longer + // If the _handling boolean has already been CaS'd to false, then stream is completed, and we are no longer // handling, so the caller can continue to fill and parse more connections. If it is still true, then some - // thread is still handling the request and they will need to organize more filling and parsing once complete. + // thread is still handling the request, and they will need to organize more filling and parsing once complete. if (_handling.compareAndSet(true, false)) { if (LOG.isDebugEnabled()) @@ -495,16 +481,6 @@ void parseAndFillForContent() private int fillRequestBuffer() { - if (_retainableByteBuffer != null && _retainableByteBuffer.isRetained()) - { - // TODO this is almost certainly wrong - RetainableByteBuffer newBuffer = _bufferPool.acquire(getInputBufferSize(), isUseInputDirectByteBuffers()); - if (LOG.isDebugEnabled()) - LOG.debug("replace buffer {} <- {} in {}", _retainableByteBuffer, newBuffer, this); - _retainableByteBuffer.release(); - _retainableByteBuffer = newBuffer; - } - if (!isRequestBufferEmpty()) return _retainableByteBuffer.remaining(); @@ -525,7 +501,7 @@ private int fillRequestBuffer() if (filled > 0) { - bytesIn.add(filled); + _bytesIn += filled; } else { @@ -649,9 +625,6 @@ public void onOpen() @Override public void onClose(Throwable cause) { - // TODO: do we really need to do this? - // This event is fired really late, sendCallback should already be failed at this point. - // Revisit whether we still need IteratingCallback.close(). if (cause == null) _sendCallback.close(); else @@ -665,21 +638,16 @@ public void run() onFillable(); } - public void asyncReadFillInterested() - { - getEndPoint().tryFillInterested(_demandContentCallback); - } - @Override public long getBytesIn() { - return bytesIn.longValue(); + return _bytesIn; } @Override public long getBytesOut() { - return bytesOut.longValue(); + return _bytesOut; } @Override @@ -846,7 +814,7 @@ public Action process() throws Exception gatherWrite += 1; bytes += _content.remaining(); } - HttpConnection.this.bytesOut.add(bytes); + HttpConnection.this._bytesOut += bytes; switch (gatherWrite) { case 7: @@ -1281,14 +1249,14 @@ public boolean is100ContinueExpected() }; Runnable handle = _httpChannel.onRequest(_request); - ++_requests; + _requests++; Request request = _httpChannel.getRequest(); getHttpChannel().getComplianceViolationListener().onRequestBegin(request); if (_complianceViolations != null && !_complianceViolations.isEmpty()) { - _httpChannel.getRequest().setAttribute(HttpCompliance.VIOLATIONS_ATTR, _complianceViolations); + _httpChannel.getRequest().setAttribute(ComplianceViolation.CapturingListener.VIOLATIONS_ATTR_KEY, _complianceViolations); _complianceViolations = null; } @@ -1442,17 +1410,21 @@ else if (_generator.isCommitted()) { callback.failed(new IllegalStateException("Committed")); } - else if (_expects100Continue) + else { - if (response.getStatus() == HttpStatus.CONTINUE_100) + _responses++; + if (_expects100Continue) { - _expects100Continue = false; - } - else - { - // Expecting to send a 100 Continue response, but it's a different response, - // then cannot be persistent because likely the client did not send the content. - _generator.setPersistent(false); + if (response.getStatus() == HttpStatus.CONTINUE_100) + { + _expects100Continue = false; + } + else + { + // Expecting to send a 100 Continue response, but it's a different response, + // then cannot be persistent because likely the client did not send the content. + _generator.setPersistent(false); + } } } From ec0d325c56fdc2b7a69e0ddfe848655e560e5c3c Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 7 Aug 2024 11:58:47 +1000 Subject: [PATCH 04/61] Delay until MultiPartFormData --- .../eclipse/jetty/http/MultiPartFormData.java | 21 +-- .../jetty/server/handler/DelayedHandler.java | 116 ++++++++--------- .../server/handler/DelayedHandlerTest.java | 120 ------------------ 3 files changed, 67 insertions(+), 190 deletions(-) diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java index 69293958022f..88f879a54869 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java @@ -489,15 +489,18 @@ public void setMaxParts(long maxParts) */ public void configure(MultiPartConfig config) { - parser.setMaxParts(config.getMaxParts()); - maxMemoryFileSize = config.getMaxMemoryPartSize(); - maxFileSize = config.getMaxPartSize(); - maxLength = config.getMaxSize(); - parser.setPartHeadersMaxLength(config.getMaxHeadersSize()); - useFilesForPartsWithoutFileName = config.isUseFilesForPartsWithoutFileName(); - filesDirectory = config.getLocation(); - complianceListener = config.getViolationListener(); - compliance = config.getMultiPartCompliance(); + if (config != null) + { + parser.setMaxParts(config.getMaxParts()); + maxMemoryFileSize = config.getMaxMemoryPartSize(); + maxFileSize = config.getMaxPartSize(); + maxLength = config.getMaxSize(); + parser.setPartHeadersMaxLength(config.getMaxHeadersSize()); + useFilesForPartsWithoutFileName = config.isUseFilesForPartsWithoutFileName(); + filesDirectory = config.getLocation(); + complianceListener = config.getViolationListener(); + compliance = config.getMultiPartCompliance(); + } } // Only used for testing. diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java index a978b054f202..0791035a5b8a 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -17,14 +17,14 @@ import java.nio.charset.StandardCharsets; import java.util.Objects; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MimeTypes; -import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.http.MultiPartConfig; +import org.eclipse.jetty.http.MultiPartFormData; import org.eclipse.jetty.server.FormFields; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; @@ -104,15 +104,23 @@ protected DelayedProcess newDelayedProcess(boolean contentExpected, String conte if (!request.getConnectionMetaData().getHttpConfiguration().isDelayDispatchUntilContent()) return null; - // If there is no known content type, then delay only until content is available - if (mimeType == null) - return new UntilContentDelayedProcess(handler, request, response, callback); - // Otherwise, delay until a known content type is fully read; or if the type is not known then until the content is available return switch (mimeType) { case FORM_ENCODED -> new UntilFormDelayedProcess(handler, request, response, callback, contentType); - default -> new UntilContentDelayedProcess(handler, request, response, callback); + case MULTIPART_FORM_DATA -> + { + MultiPartConfig config; + if (request.getContext().getAttribute(MultiPartConfig.class.getName()) instanceof MultiPartConfig mpc) + config = mpc; + else if (getHandler().getServer().getAttribute(MultiPartConfig.class.getName()) instanceof MultiPartConfig mpc) + config = mpc; + else + yield null; + + yield new UntilMultipartDelayedProcess(handler, request, response, callback, contentType, config); + } + default -> null; }; } @@ -167,96 +175,82 @@ protected void process() protected abstract void delay() throws Exception; } - protected static class UntilContentDelayedProcess extends DelayedProcess + protected static class UntilFormDelayedProcess extends DelayedProcess { - public UntilContentDelayedProcess(Handler handler, Request request, Response response, Callback callback) + private final Charset _charset; + + public UntilFormDelayedProcess(Handler handler, Request request, Response response, Callback callback, String contentType) { super(handler, request, response, callback); + + String cs = MimeTypes.getCharsetFromContentType(contentType); + _charset = StringUtil.isEmpty(cs) ? StandardCharsets.UTF_8 : Charset.forName(cs); } @Override protected void delay() { - Content.Chunk chunk = super.getRequest().read(); - if (chunk == null) - { - getRequest().demand(this::onContent); - } - else - { - RewindChunkRequest request = new RewindChunkRequest(getRequest(), chunk); - try - { - getHandler().handle(request, getResponse(), getCallback()); - } - catch (Throwable x) - { - // Use the wrapped request so that the error handling can - // consume the request content and release the already read chunk. - Response.writeError(request, getResponse(), getCallback(), x); - } - } + CompletableFuture futureFormFields = FormFields.from(getRequest(), _charset); + + // if we are done already, then we are still in the scope of the original process call and can + // process directly, otherwise we must execute a call to process as we are within a serialized + // demand callback. + futureFormFields.whenComplete(futureFormFields.isDone() ? this::process : this::executeProcess); } - public void onContent() + private void process(Fields fields, Throwable x) { - // We must execute here, because demand callbacks are serialized and process may block on a demand callback - getRequest().getContext().execute(this::process); + if (x == null) + super.process(); + else + Response.writeError(getRequest(), getResponse(), getCallback(), x); } - private static class RewindChunkRequest extends Request.Wrapper + private void executeProcess(Fields fields, Throwable x) { - private final AtomicReference _chunk; - - public RewindChunkRequest(Request wrapped, Content.Chunk chunk) - { - super(wrapped); - _chunk = new AtomicReference<>(chunk); - } - - @Override - public Content.Chunk read() - { - Content.Chunk chunk = _chunk.getAndSet(null); - if (chunk != null) - return chunk; - return super.read(); - } + if (x == null) + // We must execute here as even though we have consumed all the input, we are probably + // invoked in a demand runnable that is serialized with any write callbacks that might be done in process + getRequest().getContext().execute(super::process); + else + Response.writeError(getRequest(), getResponse(), getCallback(), x); } } - protected static class UntilFormDelayedProcess extends DelayedProcess + protected static class UntilMultipartDelayedProcess extends DelayedProcess { - private final Charset _charset; + private final String _contentType; + private final MultiPartConfig _config; - public UntilFormDelayedProcess(Handler handler, Request wrapped, Response response, Callback callback, String contentType) + public UntilMultipartDelayedProcess(Handler handler, Request request, Response response, Callback callback, String contentType, MultiPartConfig config) { - super(handler, wrapped, response, callback); - - String cs = MimeTypes.getCharsetFromContentType(contentType); - _charset = StringUtil.isEmpty(cs) ? StandardCharsets.UTF_8 : Charset.forName(cs); + super(handler, request, response, callback); + _contentType = contentType; + _config = config; } @Override protected void delay() { - CompletableFuture futureFormFields = FormFields.from(getRequest(), _charset); + Request request = getRequest(); + + CompletableFuture futureMultiPart = MultiPartFormData.from(request, request, _contentType, _config); // if we are done already, then we are still in the scope of the original process call and can // process directly, otherwise we must execute a call to process as we are within a serialized // demand callback. - futureFormFields.whenComplete(futureFormFields.isDone() ? this::process : this::executeProcess); + futureMultiPart.whenComplete(futureMultiPart.isDone() ? this::process : this::executeProcess); } - private void process(Fields fields, Throwable x) + private void process(MultiPartFormData.Parts parts, Throwable failure) { - if (x == null) + if (failure == null) super.process(); else - Response.writeError(getRequest(), getResponse(), getCallback(), x); + Response.writeError(getRequest(), getResponse(), getCallback(), failure); } - private void executeProcess(Fields fields, Throwable x) + private void executeProcess(MultiPartFormData.Parts parts, Throwable x) { if (x == null) // We must execute here as even though we have consumed all the input, we are probably diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java index 88c213a3132d..dd21cab37493 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java @@ -42,8 +42,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.sameInstance; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -165,124 +163,6 @@ public boolean handle(Request request, Response response, Callback callback) thr } } - @Test - public void testDelayedUntilContent() throws Exception - { - DelayedHandler delayedHandler = new DelayedHandler(); - - _server.setHandler(delayedHandler); - CountDownLatch processing = new CountDownLatch(1); - delayedHandler.setHandler(new HelloHandler() - { - @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception - { - // Check that we are not called via any demand callback - ByteArrayOutputStream out = new ByteArrayOutputStream(8192); - new Throwable().printStackTrace(new PrintStream(out)); - String stack = out.toString(StandardCharsets.ISO_8859_1); - assertThat(stack, not(containsString("DemandContentCallback.succeeded"))); - assertThat(stack, not(containsString("%s.%s".formatted( - DelayedHandler.UntilContentDelayedProcess.class.getSimpleName(), - DelayedHandler.UntilContentDelayedProcess.class.getMethod("onContent").getName())))); - - processing.countDown(); - return super.handle(request, response, callback); - } - }); - _server.start(); - - try (Socket socket = new Socket("localhost", _connector.getLocalPort())) - { - String request = """ - POST / HTTP/1.1\r - Host: localhost\r - Content-Length: 10\r - \r - """; - OutputStream output = socket.getOutputStream(); - output.write(request.getBytes(StandardCharsets.UTF_8)); - output.flush(); - - assertFalse(processing.await(250, TimeUnit.MILLISECONDS)); - - output.write("01234567\r\n".getBytes(StandardCharsets.UTF_8)); - output.flush(); - - assertTrue(processing.await(10, TimeUnit.SECONDS)); - - HttpTester.Input input = HttpTester.from(socket.getInputStream()); - HttpTester.Response response = HttpTester.parseResponse(input); - assertNotNull(response); - assertEquals(HttpStatus.OK_200, response.getStatus()); - String content = new String(response.getContentBytes(), StandardCharsets.UTF_8); - assertThat(content, containsString("Hello")); - } - } - - @Test - public void testDelayedUntilContentInContext() throws Exception - { - ContextHandler context = new ContextHandler(); - _server.setHandler(context); - DelayedHandler delayedHandler = new DelayedHandler(); - context.setHandler(delayedHandler); - - CountDownLatch processing = new CountDownLatch(1); - delayedHandler.setHandler(new HelloHandler() - { - @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception - { - // Check that we are not called via any demand callback - ByteArrayOutputStream out = new ByteArrayOutputStream(8192); - new Throwable().printStackTrace(new PrintStream(out)); - String stack = out.toString(StandardCharsets.ISO_8859_1); - assertThat(stack, not(containsString("DemandContentCallback.succeeded"))); - assertThat(stack, not(containsString("%s.%s".formatted( - DelayedHandler.UntilContentDelayedProcess.class.getSimpleName(), - DelayedHandler.UntilContentDelayedProcess.class.getMethod("onContent").getName())))); - - // Check the thread is in the context - assertThat(ContextHandler.getCurrentContext(), sameInstance(context.getContext())); - - // Check the request is wrapped in the context - assertThat(request.getContext(), sameInstance(context.getContext())); - - processing.countDown(); - return super.handle(request, response, callback); - } - }); - _server.start(); - - try (Socket socket = new Socket("localhost", _connector.getLocalPort())) - { - String request = """ - POST / HTTP/1.1\r - Host: localhost\r - Content-Length: 10\r - \r - """; - OutputStream output = socket.getOutputStream(); - output.write(request.getBytes(StandardCharsets.UTF_8)); - output.flush(); - - assertFalse(processing.await(250, TimeUnit.MILLISECONDS)); - - output.write("01234567\r\n".getBytes(StandardCharsets.UTF_8)); - output.flush(); - - assertTrue(processing.await(10, TimeUnit.SECONDS)); - - HttpTester.Input input = HttpTester.from(socket.getInputStream()); - HttpTester.Response response = HttpTester.parseResponse(input); - assertNotNull(response); - assertEquals(HttpStatus.OK_200, response.getStatus()); - String content = new String(response.getContentBytes(), StandardCharsets.UTF_8); - assertThat(content, containsString("Hello")); - } - } - @Test public void testNoDelayWithContent() throws Exception { From da5f09f1ce4291be69ae2ffbc63cde89b3c4abbc Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 7 Aug 2024 12:46:44 +1000 Subject: [PATCH 05/61] Wait for release --- .../eclipse/jetty/test/client/transport/AbstractTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java index b51f938ea72f..748ba43dcc13 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java @@ -129,9 +129,15 @@ public void dispose(TestInfo testInfo) throws Exception try { if (serverBufferPool != null && !isLeakTrackingDisabled(testInfo, "server")) + { + Awaitility.waitAtMost(5, TimeUnit.SECONDS).until(serverBufferPool.getLeaks()::isEmpty); assertNoLeaks(serverBufferPool, testInfo, "server-", "\n---\nServer Leaks: " + serverBufferPool.dumpLeaks() + "---\n"); + } if (clientBufferPool != null && !isLeakTrackingDisabled(testInfo, "client")) + { + Awaitility.waitAtMost(5, TimeUnit.SECONDS).until(clientBufferPool.getLeaks()::isEmpty); assertNoLeaks(clientBufferPool, testInfo, "client-", "\n---\nClient Leaks: " + clientBufferPool.dumpLeaks() + "---\n"); + } } finally { From 5993364288b3e46ac0c0464461a119f6df57b6eb Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 7 Aug 2024 12:53:19 +1000 Subject: [PATCH 06/61] fixed removal of deprecated methods --- .../java/org/eclipse/jetty/server/HttpConnectionFactory.java | 2 -- .../jetty/ee10/servlet/ComplianceViolations2616Test.java | 1 - .../jetty/ee11/servlet/ComplianceViolations2616Test.java | 1 - .../src/test/java/org/eclipse/jetty/ee9/nested/RequestTest.java | 1 - 4 files changed, 5 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java index c6a5249d745c..8c9a20932409 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java @@ -30,8 +30,6 @@ public class HttpConnectionFactory extends AbstractConnectionFactory implements HttpConfiguration.ConnectionFactory { private final HttpConfiguration _config; - private boolean _useInputDirectByteBuffers; - private boolean _useOutputDirectByteBuffers; public HttpConnectionFactory() { diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ComplianceViolations2616Test.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ComplianceViolations2616Test.java index f3bea6be187b..c9837de3cd40 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ComplianceViolations2616Test.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ComplianceViolations2616Test.java @@ -109,7 +109,6 @@ public static void startServer() throws Exception config.addComplianceViolationListener(new ComplianceViolation.CapturingListener()); HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(config); - httpConnectionFactory.setRecordHttpComplianceViolations(true); connector = new LocalConnector(server, null, null, null, -1, httpConnectionFactory); ServletContextHandler context = new ServletContextHandler(); diff --git a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ComplianceViolations2616Test.java b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ComplianceViolations2616Test.java index b80086245b85..75ca94c25c02 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ComplianceViolations2616Test.java +++ b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ComplianceViolations2616Test.java @@ -109,7 +109,6 @@ public static void startServer() throws Exception config.addComplianceViolationListener(new ComplianceViolation.CapturingListener()); HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(config); - httpConnectionFactory.setRecordHttpComplianceViolations(true); connector = new LocalConnector(server, null, null, null, -1, httpConnectionFactory); ServletContextHandler context = new ServletContextHandler(); diff --git a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/RequestTest.java b/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/RequestTest.java index c64ad121971a..c0177f611fb8 100644 --- a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/RequestTest.java +++ b/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/RequestTest.java @@ -130,7 +130,6 @@ public void init() throws Exception _server = new Server(); _context = new ContextHandler(_server); HttpConnectionFactory http = new HttpConnectionFactory(); - http.setRecordHttpComplianceViolations(true); http.setInputBufferSize(1024); http.getHttpConfiguration().setRequestHeaderSize(512); http.getHttpConfiguration().setResponseHeaderSize(512); From 3ef3d6e85e8dfa4646c4fc86f531367b7e584d84 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 8 Aug 2024 07:30:10 +1000 Subject: [PATCH 07/61] fixed no mimeType --- .../java/org/eclipse/jetty/server/handler/DelayedHandler.java | 4 ++-- .../jetty/ee9/servlet/ComplianceViolations2616Test.java | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java index 0791035a5b8a..461c847a7f1c 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -100,8 +100,8 @@ protected DelayedProcess newDelayedProcess(boolean contentExpected, String conte if (!contentExpected) return null; - // if we are not configured to delay dispatch, then no delay - if (!request.getConnectionMetaData().getHttpConfiguration().isDelayDispatchUntilContent()) + // if no mimeType, then no delay + if (mimeType == null) return null; // Otherwise, delay until a known content type is fully read; or if the type is not known then until the content is available diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ComplianceViolations2616Test.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ComplianceViolations2616Test.java index e8f49f5436d9..a525885fd8e0 100644 --- a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ComplianceViolations2616Test.java +++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ComplianceViolations2616Test.java @@ -108,7 +108,6 @@ public static void startServer() throws Exception config.addComplianceViolationListener(new ComplianceViolation.CapturingListener()); HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(config); - httpConnectionFactory.setRecordHttpComplianceViolations(true); connector = new LocalConnector(server, null, null, null, -1, httpConnectionFactory); ServletContextHandler context = new ServletContextHandler(); From 5ede88aade55cd59d228a084d0c6c11ebeba51d2 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Thu, 8 Aug 2024 10:09:08 +1000 Subject: [PATCH 08/61] PR #12077 - add test for DelayedHandler with multipart Signed-off-by: Lachlan Roberts --- .../server/handler/DelayedHandlerTest.java | 73 +++++++++++++++++++ .../servlet/ComplianceViolations2616Test.java | 1 - 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java index dd21cab37493..0c561d9e7d9c 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java @@ -18,14 +18,18 @@ import java.io.PrintStream; import java.net.Socket; import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Exchanger; import java.util.concurrent.TimeUnit; import org.awaitility.Awaitility; +import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.http.MultiPartConfig; +import org.eclipse.jetty.http.MultiPartFormData; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.server.FormFields; import org.eclipse.jetty.server.Handler; @@ -33,6 +37,7 @@ import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Fields; import org.junit.jupiter.api.AfterEach; @@ -383,4 +388,72 @@ public boolean handle(Request request, Response response, Callback callback) thr assertThat(content, containsString("x=[1, 2, 3]")); } } + + @Test + public void testDelayedMultipart() throws Exception + { + DelayedHandler delayedHandler = new DelayedHandler(); + _server.setAttribute(MultiPartConfig.class.getName(), new MultiPartConfig.Builder().build()); + _server.setHandler(delayedHandler); + delayedHandler.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + CompletableFuture future = MultiPartFormData.get(request); + assertNotNull(future); + assertTrue(future.isDone()); + MultiPartFormData.Parts parts = future.get(); + assertNotNull(parts); + assertThat(parts.size(), equalTo(3)); + for (int i = 0; i < 3; i++) + { + assertThat(parts.get(i).getName(), equalTo("part" + i)); + assertThat(parts.get(i).getContentAsString(StandardCharsets.ISO_8859_1), + equalTo("This is the content of Part" + i)); + } + + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain"); + response.write(true, BufferUtil.toBuffer("success"), callback); + return true; + } + }); + _server.start(); + + try (Socket socket = new Socket("localhost", _connector.getLocalPort())) + { + String requestContent = """ + --jettyBoundary123\r + Content-Disposition: form-data; name="part0"\r + \r + This is the content of Part0\r + --jettyBoundary123\r + Content-Disposition: form-data; name="part1"\r + \r + This is the content of Part1\r + --jettyBoundary123\r + Content-Disposition: form-data; name="part2"\r + \r + This is the content of Part2\r + --jettyBoundary123--\r + """; + String requestHeaders = String.format(""" + POST / HTTP/1.1\r + Host: localhost\r + Content-Type: multipart/form-data; boundary=jettyBoundary123\r + Content-Length: %s\r + \r + """, requestContent.getBytes(StandardCharsets.UTF_8).length); + OutputStream output = socket.getOutputStream(); + output.write((requestHeaders + requestContent).getBytes(StandardCharsets.UTF_8)); + output.flush(); + + HttpTester.Input input = HttpTester.from(socket.getInputStream()); + HttpTester.Response response = HttpTester.parseResponse(input); + assertNotNull(response); + assertEquals(HttpStatus.OK_200, response.getStatus()); + String content = new String(response.getContentBytes(), StandardCharsets.UTF_8); + assertThat(content, equalTo("success")); + } + } } diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ComplianceViolations2616Test.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ComplianceViolations2616Test.java index e8f49f5436d9..a525885fd8e0 100644 --- a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ComplianceViolations2616Test.java +++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ComplianceViolations2616Test.java @@ -108,7 +108,6 @@ public static void startServer() throws Exception config.addComplianceViolationListener(new ComplianceViolation.CapturingListener()); HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(config); - httpConnectionFactory.setRecordHttpComplianceViolations(true); connector = new LocalConnector(server, null, null, null, -1, httpConnectionFactory); ServletContextHandler context = new ServletContextHandler(); From 9e1f431e464531088d52058fe4ad4e5d55a47285 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 22 Aug 2024 16:24:00 +1000 Subject: [PATCH 09/61] updates from review --- .../org/eclipse/jetty/server/handler/DelayedHandler.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java index 461c847a7f1c..d026d6d40327 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -236,9 +236,8 @@ protected void delay() CompletableFuture futureMultiPart = MultiPartFormData.from(request, request, _contentType, _config); - // if we are done already, then we are still in the scope of the original process call and can - // process directly, otherwise we must execute a call to process as we are within a serialized - // demand callback. + // if we are done already, then we can call process in this thread, otherwise + // we must call executeProcess when the multipart is complete, since it will be called from a serialized callback. futureMultiPart.whenComplete(futureMultiPart.isDone() ? this::process : this::executeProcess); } From e0f5b7e4d753c1f868db71e910aa4530d369e5cd Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 22 Aug 2024 16:51:02 +1000 Subject: [PATCH 10/61] fixed test --- .../test/java/org/eclipse/jetty/server/HttpConnectionTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java index 359dd8302a81..0be149211a69 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java @@ -1133,6 +1133,8 @@ public void testConnection() throws Exception @Test public void testDelayedDispatch() throws Exception { + _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setDelayDispatchUntilContent(true); + _server.start(); try (LocalConnector.LocalEndPoint connection = _connector.connect()) { CounterStatistic dumpCounter = _server.getBean(DumpHandler.class).getHandledCounter(); From d4dcd6a8c85030786c76c405a31b0051cfd607ca Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 18 Sep 2024 08:43:04 +1000 Subject: [PATCH 11/61] WIP updates from review --- .../eclipse/jetty/http/MultiPartFormData.java | 21 ++++++++----------- .../jetty/server/HttpConnectionFactory.java | 2 -- .../server/internal/HttpChannelState.java | 18 ++++++++++------ .../jetty/server/internal/HttpConnection.java | 1 - 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java index 88f879a54869..69293958022f 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java @@ -489,18 +489,15 @@ public void setMaxParts(long maxParts) */ public void configure(MultiPartConfig config) { - if (config != null) - { - parser.setMaxParts(config.getMaxParts()); - maxMemoryFileSize = config.getMaxMemoryPartSize(); - maxFileSize = config.getMaxPartSize(); - maxLength = config.getMaxSize(); - parser.setPartHeadersMaxLength(config.getMaxHeadersSize()); - useFilesForPartsWithoutFileName = config.isUseFilesForPartsWithoutFileName(); - filesDirectory = config.getLocation(); - complianceListener = config.getViolationListener(); - compliance = config.getMultiPartCompliance(); - } + parser.setMaxParts(config.getMaxParts()); + maxMemoryFileSize = config.getMaxMemoryPartSize(); + maxFileSize = config.getMaxPartSize(); + maxLength = config.getMaxSize(); + parser.setPartHeadersMaxLength(config.getMaxHeadersSize()); + useFilesForPartsWithoutFileName = config.isUseFilesForPartsWithoutFileName(); + filesDirectory = config.getLocation(); + complianceListener = config.getViolationListener(); + compliance = config.getMultiPartCompliance(); } // Only used for testing. diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java index 8c9a20932409..45edd74bf7c8 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnectionFactory.java @@ -41,8 +41,6 @@ public HttpConnectionFactory(@Name("config") HttpConfiguration config) super(HttpVersion.HTTP_1_1.asString()); _config = Objects.requireNonNull(config); installBean(_config); - setUseInputDirectByteBuffers(_config.isUseInputDirectByteBuffers()); - setUseOutputDirectByteBuffers(_config.isUseOutputDirectByteBuffers()); } @Override diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java index b5c981676bfb..f567ded55e7f 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java @@ -363,6 +363,10 @@ public Runnable onIdleTimeout(TimeoutException t) if (LOG.isDebugEnabled()) LOG.debug("onIdleTimeout {}", this, t); + // Too late? + if (_request == null || _response == null) + return null; + Runnable invokeOnContentAvailable = null; if (_readFailure == null) { @@ -704,22 +708,24 @@ public void run() @Override public void succeeded() { - HttpStream completeStream = null; - Throwable failure = null; + HttpStream stream = null; try (AutoLock ignored = _lock.lock()) { assert _callbackCompleted; _streamSendState = StreamSendState.LAST_COMPLETE; if (_handling == null) { - completeStream = _stream; + stream = _stream; _stream = null; - failure = _callbackFailure; + + // TODO remove this before merging + if (_callbackFailure != null) + throw new IllegalStateException("failure in succeeded", _callbackFailure); } } - if (completeStream != null) - completeStream(completeStream, failure); + if (stream != null) + completeStream(stream, null); } /** diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index e945294e9120..060f845cd25c 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -967,7 +967,6 @@ public boolean headerComplete() _onRequest = stream.headerComplete(); // Should we delay dispatch until we have some content? - // We should not delay if there is no content expect or client is expecting 100 or the response is already committed or the request buffer already has something in it to parse _delayedForContent = getHttpConfiguration().isDelayDispatchUntilContent() && (_parser.getContentLength() > 0 || _parser.isChunking()) && !stream._expects100Continue && From 9eb9e7d38e21363dc78cfb8c70c74167c7d29594 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 18 Sep 2024 10:13:33 +1000 Subject: [PATCH 12/61] WIP updates from review --- .../server/internal/HttpChannelState.java | 7 ++-- .../jetty/server/internal/HttpConnection.java | 32 ++++++++++++------- .../jetty/ee10/servlet/ServletTest.java | 2 -- .../jetty/ee11/servlet/ServletTest.java | 2 -- .../jetty/ee9/servlet/ServletTest.java | 2 -- 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java index f567ded55e7f..8cd29e8f62d6 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java @@ -363,8 +363,8 @@ public Runnable onIdleTimeout(TimeoutException t) if (LOG.isDebugEnabled()) LOG.debug("onIdleTimeout {}", this, t); - // Too late? - if (_request == null || _response == null) + // too late? + if (_stream == null) return null; Runnable invokeOnContentAvailable = null; @@ -445,7 +445,8 @@ private Runnable onFailure(Throwable x, boolean remote) // If not handled, then we just fail the request callback if (!_handled && _handling == null) { - task = () -> _request._callback.failed(x); + Callback callback = _request._callback; + task = () -> callback.failed(x); } else { diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index 060f845cd25c..88831e1a2eca 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -583,10 +583,16 @@ public boolean onIdleExpired(TimeoutException timeout) return true; Runnable task; - if (_delayedForContent && _onRequest != null) + + if (_delayedForContent || _onRequest == null) + { + task = _httpChannel.onIdleTimeout(timeout); + } + else { Runnable onRequest = _onRequest; _onRequest = null; + task = () -> { try @@ -596,19 +602,12 @@ public boolean onIdleExpired(TimeoutException timeout) finally { _handling.set(false); - Runnable next = _httpChannel.onIdleTimeout(timeout); - if (next != null) - getExecutor().execute(next); } }; } - else - { - task = _httpChannel.onIdleTimeout(timeout); - } if (task != null) getExecutor().execute(task); - return false; // We've handle the exception + return false; // We've handle (or ignored) the timeout } @Override @@ -967,11 +966,16 @@ public boolean headerComplete() _onRequest = stream.headerComplete(); // Should we delay dispatch until we have some content? - _delayedForContent = getHttpConfiguration().isDelayDispatchUntilContent() && + if (getHttpConfiguration().isDelayDispatchUntilContent() && + getEndPoint().getIdleTimeout() > 0 && (_parser.getContentLength() > 0 || _parser.isChunking()) && !stream._expects100Continue && !stream.isCommitted() && - _requestBuffer != null && _requestBuffer.isEmpty(); + _requestBuffer != null && _requestBuffer.isEmpty()) + { + getEndPoint().setIdleTimeout(getEndPoint().getIdleTimeout() / 2); + _delayedForContent = true; + } return !_delayedForContent; } @@ -988,7 +992,11 @@ public boolean content(ByteBuffer buffer) _requestBuffer.retain(); stream._chunk = Content.Chunk.asChunk(buffer, false, _requestBuffer); - _delayedForContent = false; + if (_delayedForContent) + { + _delayedForContent = false; + getEndPoint().setIdleTimeout(getEndPoint().getIdleTimeout() * 2); + } return true; } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletTest.java index 866ca69dc16b..2c6088e93d65 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletTest.java @@ -28,7 +28,6 @@ import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; -import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -139,7 +138,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I }, "/post"); _connector.setIdleTimeout(idleTimeout); - _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setDelayDispatchUntilContent(false); _server.start(); try (LocalConnector.LocalEndPoint endPoint = _connector.connect()) diff --git a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ServletTest.java b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ServletTest.java index d28aec0ff5cd..aa687f40b512 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ServletTest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ServletTest.java @@ -28,7 +28,6 @@ import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; -import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -142,7 +141,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I }, "/post"); _connector.setIdleTimeout(idleTimeout); - _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setDelayDispatchUntilContent(false); _server.start(); try (LocalConnector.LocalEndPoint endPoint = _connector.connect()) diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ServletTest.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ServletTest.java index 12a52276f70f..88fa68a1bd1b 100644 --- a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ServletTest.java +++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ServletTest.java @@ -24,7 +24,6 @@ import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; -import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.IO; @@ -134,7 +133,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I }), "/post"); _connector.setIdleTimeout(idleTimeout); - _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setDelayDispatchUntilContent(false); _server.start(); try (LocalConnector.LocalEndPoint endPoint = _connector.connect()) From 72ab339c31817bf793a7c5171019104d5c108d72 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 18 Sep 2024 15:51:41 +1000 Subject: [PATCH 13/61] WIP updates from review --- .../java/org/eclipse/jetty/server/internal/HttpConnection.java | 2 +- .../java/org/eclipse/jetty/server/ReadWriteFailuresTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index 88831e1a2eca..f2912ded26a0 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -584,7 +584,7 @@ public boolean onIdleExpired(TimeoutException timeout) Runnable task; - if (_delayedForContent || _onRequest == null) + if (!_delayedForContent || _onRequest == null) { task = _httpChannel.onIdleTimeout(timeout); } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ReadWriteFailuresTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ReadWriteFailuresTest.java index f5650926ef68..898099694816 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ReadWriteFailuresTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ReadWriteFailuresTest.java @@ -93,7 +93,7 @@ public boolean handle(Request request, Response response, Callback callback) POST / HTTP/1.1 Host: localhost Content-Length: 1 - + """; HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request, 5, TimeUnit.SECONDS)); From e45ecad65fe4e559f797998b298a0c7fb74ccd22 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 18 Sep 2024 22:05:39 +1000 Subject: [PATCH 14/61] WIP updates from review --- .../java/org/eclipse/jetty/server/internal/HttpConnection.java | 1 + .../jetty/ee10/test/client/transport/RequestReaderTest.java | 1 + .../jetty/ee11/test/client/transport/RequestReaderTest.java | 1 + 3 files changed, 3 insertions(+) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index f2912ded26a0..905ff15146a9 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -973,6 +973,7 @@ public boolean headerComplete() !stream.isCommitted() && _requestBuffer != null && _requestBuffer.isEmpty()) { + // TODO should we max this to 1s? getEndPoint().setIdleTimeout(getEndPoint().getIdleTimeout() / 2); _delayedForContent = true; } diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/RequestReaderTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/RequestReaderTest.java index 576c585f2fdf..9b281e0a70b2 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/RequestReaderTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/RequestReaderTest.java @@ -137,6 +137,7 @@ public void failed(Throwable x) } }); server.start(); + connector.setIdleTimeout(1000); AtomicReference resultRef = new AtomicReference<>(); try (AsyncRequestContent content = new AsyncRequestContent()) diff --git a/jetty-ee11/jetty-ee11-tests/jetty-ee11-test-client-transports/src/test/java/org/eclipse/jetty/ee11/test/client/transport/RequestReaderTest.java b/jetty-ee11/jetty-ee11-tests/jetty-ee11-test-client-transports/src/test/java/org/eclipse/jetty/ee11/test/client/transport/RequestReaderTest.java index 6267b08c84ca..737032090c8f 100644 --- a/jetty-ee11/jetty-ee11-tests/jetty-ee11-test-client-transports/src/test/java/org/eclipse/jetty/ee11/test/client/transport/RequestReaderTest.java +++ b/jetty-ee11/jetty-ee11-tests/jetty-ee11-test-client-transports/src/test/java/org/eclipse/jetty/ee11/test/client/transport/RequestReaderTest.java @@ -137,6 +137,7 @@ public void failed(Throwable x) } }); server.start(); + connector.setIdleTimeout(1000); AtomicReference resultRef = new AtomicReference<>(); try (AsyncRequestContent content = new AsyncRequestContent()) From a585cfcb23307ddc32c86f345d77de3cd9c7c379 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 19 Sep 2024 09:03:27 +1000 Subject: [PATCH 15/61] WIP updates from review Remove delayed dispatch, but keep other improvements --- .../eclipse/jetty/io/AbstractConnection.java | 3 + .../jetty/server/handler/DelayedHandler.java | 64 +++++++++- .../server/internal/HttpChannelState.java | 17 +-- .../jetty/server/internal/HttpConnection.java | 61 +-------- .../jetty/server/HttpConnectionTest.java | 82 ------------ .../server/handler/DelayedHandlerTest.java | 120 ++++++++++++++++++ .../handler/ThreadLimitHandlerTest.java | 19 +-- .../client/transport/RequestReaderTest.java | 1 - .../client/transport/RequestReaderTest.java | 1 - 9 files changed, 200 insertions(+), 168 deletions(-) diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java index 883ea6de054b..9a9f169728f2 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java @@ -52,9 +52,12 @@ protected AbstractConnection(EndPoint endPoint, Executor executor) _readCallback = new ReadCallback(); } + @Deprecated @Override public InvocationType getInvocationType() { + // TODO consider removing the #fillInterested method from the connection and only use #fillInterestedCallback + // so a connection need not be Invocable return Invocable.super.getInvocationType(); } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java index d026d6d40327..d5d2ed5e03fb 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -17,6 +17,7 @@ import java.nio.charset.StandardCharsets; import java.util.Objects; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; @@ -25,6 +26,7 @@ import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.MultiPartConfig; import org.eclipse.jetty.http.MultiPartFormData; +import org.eclipse.jetty.io.Content; import org.eclipse.jetty.server.FormFields; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; @@ -102,7 +104,7 @@ protected DelayedProcess newDelayedProcess(boolean contentExpected, String conte // if no mimeType, then no delay if (mimeType == null) - return null; + return new UntilContentDelayedProcess(handler, request, response, callback); // Otherwise, delay until a known content type is fully read; or if the type is not known then until the content is available return switch (mimeType) @@ -120,7 +122,7 @@ else if (getHandler().getServer().getAttribute(MultiPartConfig.class.getName()) yield new UntilMultipartDelayedProcess(handler, request, response, callback, contentType, config); } - default -> null; + default -> new UntilContentDelayedProcess(handler, request, response, callback); }; } @@ -175,6 +177,64 @@ protected void process() protected abstract void delay() throws Exception; } + protected static class UntilContentDelayedProcess extends DelayedProcess + { + public UntilContentDelayedProcess(Handler handler, Request request, Response response, Callback callback) + { + super(handler, request, response, callback); + } + + @Override + protected void delay() + { + Content.Chunk chunk = super.getRequest().read(); + if (chunk == null) + { + getRequest().demand(this::onContent); + } + else + { + RewindChunkRequest request = new RewindChunkRequest(getRequest(), chunk); + try + { + getHandler().handle(request, getResponse(), getCallback()); + } + catch (Throwable x) + { + // Use the wrapped request so that the error handling can + // consume the request content and release the already read chunk. + Response.writeError(request, getResponse(), getCallback(), x); + } + } + } + + public void onContent() + { + // We must execute here, because demand callbacks are serialized and process may block on a demand callback + getRequest().getContext().execute(this::process); + } + + private static class RewindChunkRequest extends Request.Wrapper + { + private final AtomicReference _chunk; + + public RewindChunkRequest(Request wrapped, Content.Chunk chunk) + { + super(wrapped); + _chunk = new AtomicReference<>(chunk); + } + + @Override + public Content.Chunk read() + { + Content.Chunk chunk = _chunk.getAndSet(null); + if (chunk != null) + return chunk; + return super.read(); + } + } + } + protected static class UntilFormDelayedProcess extends DelayedProcess { private final Charset _charset; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java index 8cd29e8f62d6..30e9ec24211a 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java @@ -709,23 +709,18 @@ public void run() @Override public void succeeded() { - HttpStream stream = null; + HttpStream stream; + boolean completeStream; try (AutoLock ignored = _lock.lock()) { assert _callbackCompleted; + assert _callbackFailure == null; _streamSendState = StreamSendState.LAST_COMPLETE; - if (_handling == null) - { - stream = _stream; - _stream = null; - - // TODO remove this before merging - if (_callbackFailure != null) - throw new IllegalStateException("failure in succeeded", _callbackFailure); - } + completeStream = _handling == null; + stream = _stream; } - if (stream != null) + if (completeStream) completeStream(stream, null); } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index da208b6f1239..704156644815 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -104,7 +104,6 @@ public class HttpConnection extends AbstractMetaDataConnection implements Runnab private volatile RetainableByteBuffer _requestBuffer; private HttpFields.Mutable _trailers; private Runnable _onRequest; - private boolean _delayedForContent; private long _requests; private long _responses; private long _bytesIn; @@ -536,13 +535,9 @@ private int fillRequestBuffer() LOG.debug("filled {} {} {}", filled, _requestBuffer, this); if (filled > 0) - { _bytesIn += filled; - } else if (filled < 0) - { _parser.atEOF(); - } return filled; } @@ -598,30 +593,7 @@ public boolean onIdleExpired(TimeoutException timeout) { if (_httpChannel.getRequest() == null) return true; - - Runnable task; - - if (!_delayedForContent || _onRequest == null) - { - task = _httpChannel.onIdleTimeout(timeout); - } - else - { - Runnable onRequest = _onRequest; - _onRequest = null; - - task = () -> - { - try - { - onRequest.run(); - } - finally - { - _handling.set(false); - } - }; - } + Runnable task = _httpChannel.onIdleTimeout(timeout); if (task != null) getExecutor().execute(task); return false; // We've handle (or ignored) the timeout @@ -967,7 +939,6 @@ public void startRequest(String method, String uri, HttpVersion version) throw new IllegalStateException("Stream pending"); _headerBuilder.clear(); _httpChannel.setHttpStream(stream); - _delayedForContent = false; } @Override @@ -979,23 +950,8 @@ public void parsedHeader(HttpField field) @Override public boolean headerComplete() { - HttpStreamOverHTTP1 stream = _stream.get(); - _onRequest = stream.headerComplete(); - - // Should we delay dispatch until we have some content? - if (getHttpConfiguration().isDelayDispatchUntilContent() && - getEndPoint().getIdleTimeout() > 0 && - (_parser.getContentLength() > 0 || _parser.isChunking()) && - !stream._expects100Continue && - !stream.isCommitted() && - _requestBuffer != null && _requestBuffer.isEmpty()) - { - // TODO should we max this to 1s? - getEndPoint().setIdleTimeout(getEndPoint().getIdleTimeout() / 2); - _delayedForContent = true; - } - - return !_delayedForContent; + _onRequest = _stream.get().headerComplete(); + return true; } @Override @@ -1010,22 +966,15 @@ public boolean content(ByteBuffer buffer) _requestBuffer.retain(); stream._chunk = Content.Chunk.asChunk(buffer, false, _requestBuffer); - if (_delayedForContent) - { - _delayedForContent = false; - getEndPoint().setIdleTimeout(getEndPoint().getIdleTimeout() * 2); - } return true; } @Override public boolean contentComplete() { - // Do nothing at this point unless we delayed for content + // Do nothing at this point. // Wait for messageComplete so any trailers can be sent as special content - boolean delayed = _delayedForContent; - _delayedForContent = false; - return delayed; + return false; } @Override diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java index 0be149211a69..b93a6daa100f 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java @@ -35,7 +35,6 @@ import java.util.stream.IntStream; import java.util.stream.Stream; -import org.awaitility.Awaitility; import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpParser; @@ -51,7 +50,6 @@ import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.NanoTime; -import org.eclipse.jetty.util.statistic.CounterStatistic; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -1127,86 +1125,6 @@ public void testConnection() throws Exception } } - /** - * Ensure that excessively large hexadecimal chunk body length is parsed properly. - */ - @Test - public void testDelayedDispatch() throws Exception - { - _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setDelayDispatchUntilContent(true); - _server.start(); - try (LocalConnector.LocalEndPoint connection = _connector.connect()) - { - CounterStatistic dumpCounter = _server.getBean(DumpHandler.class).getHandledCounter(); - - // Dispatch with content - connection.addInput(""" - POST /test HTTP/1.1\r - Host: localhost\r - Content-Length: 5\r - Content-Type: text/plain; charset=utf-8\r - \r - 12345 - """ - ); - - Awaitility.waitAtMost(1, TimeUnit.SECONDS).until(() -> dumpCounter.getTotal() == 1L); - Awaitility.waitAtMost(1, TimeUnit.SECONDS).until(() -> dumpCounter.getCurrent() == 0L); - - String raw = connection.getResponse(); - assertThat(raw, containsString("200 OK")); - - // Dispatch delayed for content - dumpCounter.reset(); - connection.addInput(""" - POST /test HTTP/1.1\r - Host: localhost\r - Content-Length: 5\r - Content-Type: text/plain; charset=utf-8\r - \r - """ - ); - - Thread.sleep(10); - assertThat(dumpCounter.getTotal(), is(0L)); - assertThat(dumpCounter.getCurrent(), is(0L)); - - connection.addInput("12345"); - Awaitility.waitAtMost(1, TimeUnit.SECONDS).until(() -> dumpCounter.getTotal() == 1L); - Awaitility.waitAtMost(1, TimeUnit.SECONDS).until(() -> dumpCounter.getCurrent() == 0L); - - raw = connection.getResponse(); - assertThat(raw, containsString("200 OK")); - - // Dispatch delayed for chunked content - dumpCounter.reset(); - connection.addInput(""" - POST /test HTTP/1.1\r - Host: localhost\r - Transfer-Encoding: chunked\r - Content-Type: text/plain; charset=utf-8\r - \r - """ - ); - - Thread.sleep(10); - assertThat(dumpCounter.getTotal(), is(0L)); - assertThat(dumpCounter.getCurrent(), is(0L)); - - connection.addInput(""" - 5;\r - 12345\r - 0;\r - \r - """); - Awaitility.waitAtMost(1, TimeUnit.SECONDS).until(() -> dumpCounter.getTotal() == 1L); - Awaitility.waitAtMost(1, TimeUnit.SECONDS).until(() -> dumpCounter.getCurrent() == 0L); - - raw = connection.getResponse(); - assertThat(raw, containsString("200 OK")); - } - } - /** * Creates a request header over 1k in size, by creating a single header entry with an huge value. * diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java index 0c561d9e7d9c..3d4c4ce68c1d 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java @@ -47,6 +47,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.sameInstance; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -168,6 +170,124 @@ public boolean handle(Request request, Response response, Callback callback) thr } } + @Test + public void testDelayedUntilContent() throws Exception + { + DelayedHandler delayedHandler = new DelayedHandler(); + + _server.setHandler(delayedHandler); + CountDownLatch processing = new CountDownLatch(1); + delayedHandler.setHandler(new HelloHandler() + { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Check that we are not called via any demand callback + ByteArrayOutputStream out = new ByteArrayOutputStream(8192); + new Throwable().printStackTrace(new PrintStream(out)); + String stack = out.toString(StandardCharsets.ISO_8859_1); + assertThat(stack, not(containsString("DemandContentCallback.succeeded"))); + assertThat(stack, not(containsString("%s.%s".formatted( + DelayedHandler.UntilContentDelayedProcess.class.getSimpleName(), + DelayedHandler.UntilContentDelayedProcess.class.getMethod("onContent").getName())))); + + processing.countDown(); + return super.handle(request, response, callback); + } + }); + _server.start(); + + try (Socket socket = new Socket("localhost", _connector.getLocalPort())) + { + String request = """ + POST / HTTP/1.1\r + Host: localhost\r + Content-Length: 10\r + \r + """; + OutputStream output = socket.getOutputStream(); + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + assertFalse(processing.await(250, TimeUnit.MILLISECONDS)); + + output.write("01234567\r\n".getBytes(StandardCharsets.UTF_8)); + output.flush(); + + assertTrue(processing.await(10, TimeUnit.SECONDS)); + + HttpTester.Input input = HttpTester.from(socket.getInputStream()); + HttpTester.Response response = HttpTester.parseResponse(input); + assertNotNull(response); + assertEquals(HttpStatus.OK_200, response.getStatus()); + String content = new String(response.getContentBytes(), StandardCharsets.UTF_8); + assertThat(content, containsString("Hello")); + } + } + + @Test + public void testDelayedUntilContentInContext() throws Exception + { + ContextHandler context = new ContextHandler(); + _server.setHandler(context); + DelayedHandler delayedHandler = new DelayedHandler(); + context.setHandler(delayedHandler); + + CountDownLatch processing = new CountDownLatch(1); + delayedHandler.setHandler(new HelloHandler() + { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Check that we are not called via any demand callback + ByteArrayOutputStream out = new ByteArrayOutputStream(8192); + new Throwable().printStackTrace(new PrintStream(out)); + String stack = out.toString(StandardCharsets.ISO_8859_1); + assertThat(stack, not(containsString("DemandContentCallback.succeeded"))); + assertThat(stack, not(containsString("%s.%s".formatted( + DelayedHandler.UntilContentDelayedProcess.class.getSimpleName(), + DelayedHandler.UntilContentDelayedProcess.class.getMethod("onContent").getName())))); + + // Check the thread is in the context + assertThat(ContextHandler.getCurrentContext(), sameInstance(context.getContext())); + + // Check the request is wrapped in the context + assertThat(request.getContext(), sameInstance(context.getContext())); + + processing.countDown(); + return super.handle(request, response, callback); + } + }); + _server.start(); + + try (Socket socket = new Socket("localhost", _connector.getLocalPort())) + { + String request = """ + POST / HTTP/1.1\r + Host: localhost\r + Content-Length: 10\r + \r + """; + OutputStream output = socket.getOutputStream(); + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + assertFalse(processing.await(250, TimeUnit.MILLISECONDS)); + + output.write("01234567\r\n".getBytes(StandardCharsets.UTF_8)); + output.flush(); + + assertTrue(processing.await(10, TimeUnit.SECONDS)); + + HttpTester.Input input = HttpTester.from(socket.getInputStream()); + HttpTester.Response response = HttpTester.parseResponse(input); + assertNotNull(response); + assertEquals(HttpStatus.OK_200, response.getStatus()); + String content = new String(response.getContentBytes(), StandardCharsets.UTF_8); + assertThat(content, containsString("Hello")); + } + } + @Test public void testNoDelayWithContent() throws Exception { diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ThreadLimitHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ThreadLimitHandlerTest.java index 060b80a149da..dd7d58152365 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ThreadLimitHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ThreadLimitHandlerTest.java @@ -42,7 +42,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertTrue; public class ThreadLimitHandlerTest @@ -281,16 +280,6 @@ public boolean handle(Request request, Response response, Callback callback) thr @Override public void run() { - // Read the first byte we know is there. This is to get around any delayed dispatch - if (read.get() == 0) - { - Content.Chunk chunk = request.read(); - assertThat(chunk, notNullValue()); - assertThat(chunk.remaining(), is(1)); - read.incrementAndGet(); - request.demand(this); - return; - } count.incrementAndGet(); try { @@ -342,7 +331,7 @@ public void run() for (int i = 0; i < client.length; i++) { client[i] = new Socket("127.0.0.1", _connector.getLocalPort()); - client[i].getOutputStream().write(("POST /" + i + " HTTP/1.0\r\nForwarded: for=1.2.3.4\r\nContent-Length: 3\r\n\r\nX").getBytes()); + client[i].getOutputStream().write(("POST /" + i + " HTTP/1.0\r\nForwarded: for=1.2.3.4\r\nContent-Length: 2\r\n\r\n").getBytes()); client[i].getOutputStream().flush(); } @@ -355,7 +344,7 @@ public void run() // Send some content for the clients for (Socket socket : client) { - socket.getOutputStream().write('Y'); + socket.getOutputStream().write('X'); socket.getOutputStream().flush(); } @@ -375,7 +364,7 @@ public void run() // Send the rest of the content for the clients for (Socket socket : client) { - socket.getOutputStream().write('Z'); + socket.getOutputStream().write('Y'); socket.getOutputStream().flush(); } @@ -384,7 +373,7 @@ public void run() { response = IO.toString(socket.getInputStream()); assertThat(response, containsString(" 200 OK")); - assertThat(response, containsString(" read 3")); + assertThat(response, containsString(" read 2")); } await().atMost(5, TimeUnit.SECONDS).until(handler::getRemoteCount, is(0)); diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/RequestReaderTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/RequestReaderTest.java index 9b281e0a70b2..576c585f2fdf 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/RequestReaderTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/RequestReaderTest.java @@ -137,7 +137,6 @@ public void failed(Throwable x) } }); server.start(); - connector.setIdleTimeout(1000); AtomicReference resultRef = new AtomicReference<>(); try (AsyncRequestContent content = new AsyncRequestContent()) diff --git a/jetty-ee11/jetty-ee11-tests/jetty-ee11-test-client-transports/src/test/java/org/eclipse/jetty/ee11/test/client/transport/RequestReaderTest.java b/jetty-ee11/jetty-ee11-tests/jetty-ee11-test-client-transports/src/test/java/org/eclipse/jetty/ee11/test/client/transport/RequestReaderTest.java index 737032090c8f..6267b08c84ca 100644 --- a/jetty-ee11/jetty-ee11-tests/jetty-ee11-test-client-transports/src/test/java/org/eclipse/jetty/ee11/test/client/transport/RequestReaderTest.java +++ b/jetty-ee11/jetty-ee11-tests/jetty-ee11-test-client-transports/src/test/java/org/eclipse/jetty/ee11/test/client/transport/RequestReaderTest.java @@ -137,7 +137,6 @@ public void failed(Throwable x) } }); server.start(); - connector.setIdleTimeout(1000); AtomicReference resultRef = new AtomicReference<>(); try (AsyncRequestContent content = new AsyncRequestContent()) From 8c33aa498375200acf5c1b7a9a73768619308cc5 Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 4 Oct 2024 09:20:40 +1000 Subject: [PATCH 16/61] Use lowercase for charsets #11741 Fix #11741 as per the WhatTFWG recommendations, use lower case for charset names. Took the opportunity for some minor optimizations: + use the already made HttpField instance in MimeTypes.Type rather than create a new one in the HttpParser.CACHE + keep the MimeType.Type associated with the pre encoded Content-Type fields --- .../migration/ServletToHandlerDocs.java | 4 + .../org/eclipse/jetty/http/HttpParser.java | 24 ++--- .../org/eclipse/jetty/http/MimeTypes.java | 97 +++++++++++++++---- .../eclipse/jetty/http/HttpParserTest.java | 20 +++- .../org/eclipse/jetty/server/FormFields.java | 11 ++- .../org/eclipse/jetty/server/Request.java | 20 +++- .../jetty/server/ErrorHandlerTest.java | 28 +++--- .../jetty/server/HttpConnectionTest.java | 4 +- .../jetty/server/HttpServerTestBase.java | 4 +- .../server/handler/DefaultHandlerTest.java | 4 +- .../jetty/ee10/servlet/ServletApiRequest.java | 10 +- .../ee10/servlet/CharacterEncodingTest.java | 2 +- .../ee10/servlet/DefaultServletTest.java | 2 +- .../ee10/servlet/ResourceServletTest.java | 2 +- .../ee10/servlet/ResponseHeadersTest.java | 4 +- .../jetty/ee11/servlet/ServletApiRequest.java | 10 +- .../ee11/servlet/CharacterEncodingTest.java | 2 +- .../ee11/servlet/DefaultServletTest.java | 2 +- .../ee11/servlet/ResourceServletTest.java | 2 +- .../ee11/servlet/ResponseHeadersTest.java | 4 +- 20 files changed, 187 insertions(+), 69 deletions(-) diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java index fb64158148f2..d6a0538641fc 100644 --- a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java @@ -15,6 +15,7 @@ import java.io.InputStream; import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.time.Duration; import java.util.List; import java.util.Locale; @@ -28,6 +29,7 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.Trailers; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.server.Context; @@ -118,6 +120,8 @@ public boolean handle(Request request, Response response, Callback callback) thr // Gets the request Content-Type. // Replaces: // - servletRequest.getContentType() + MimeTypes.Type mimeType = Request.getContentMimeType(request); + Charset charset = Request.getCharset(request); String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE); // Gets the request Content-Length. diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java index 7ba4c573f347..37109020bc04 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java @@ -19,7 +19,6 @@ import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import org.eclipse.jetty.http.HttpTokens.EndOfContent; @@ -138,25 +137,16 @@ public class HttpParser .with(new HttpField(HttpHeader.EXPIRES, "Fri, 01 Jan 1990 00:00:00 GMT")) .withAll(() -> { - Map map = new LinkedHashMap<>(); // Add common Content types as fields - for (String type : new String[]{ - "text/plain", "text/html", "text/xml", "text/json", "application/json", "application/x-www-form-urlencoded" - }) + Map map = new LinkedHashMap<>(); + for (MimeTypes.Type mimetype : MimeTypes.Type.values()) { - HttpField field = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type); - map.put(field.toString(), field); - - for (String charset : new String[]{"utf-8", "iso-8859-1"}) + MimeTypes.ContentTypeField contentTypeField = mimetype.getContentTypeField(); + map.put(contentTypeField.toString(), contentTypeField); + if (contentTypeField.getValue().contains(";charset=")) { - PreEncodedHttpField field1 = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + ";charset=" + charset); - map.put(field1.toString(), field1); - PreEncodedHttpField field2 = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + "; charset=" + charset); - map.put(field2.toString(), field2); - PreEncodedHttpField field3 = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + ";charset=" + charset.toUpperCase(Locale.ENGLISH)); - map.put(field3.toString(), field3); - PreEncodedHttpField field4 = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + "; charset=" + charset.toUpperCase(Locale.ENGLISH)); - map.put(field4.toString(), field4); + HttpField contentTypeFieldWithSpace = new MimeTypes.ContentTypeField(contentTypeField.getMimeType(), contentTypeField.getValue().replace(";charset=", "; charset=")); + map.put(contentTypeFieldWithSpace.toString(), contentTypeFieldWithSpace); } } return map; diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java index 12eecc601452..f7232f42adb9 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java @@ -124,7 +124,7 @@ public HttpField getContentTypeField(Charset charset) private final Charset _charset; private final String _charsetString; private final boolean _assumedCharset; - private final HttpField _field; + private final ContentTypeField _field; Type(String name) { @@ -133,18 +133,18 @@ public HttpField getContentTypeField(Charset charset) _charset = null; _charsetString = null; _assumedCharset = false; - _field = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, _string); + _field = new ContentTypeField(this); } Type(String name, Type base) { _string = name; - _base = base; + _base = Objects.requireNonNull(base); int i = name.indexOf(";charset="); _charset = Charset.forName(name.substring(i + 9)); _charsetString = _charset.toString().toLowerCase(Locale.ENGLISH); _assumedCharset = false; - _field = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, _string); + _field = new ContentTypeField(this); } Type(String name, Charset cs) @@ -154,9 +154,12 @@ public HttpField getContentTypeField(Charset charset) _charset = cs; _charsetString = _charset == null ? null : _charset.toString().toLowerCase(Locale.ENGLISH); _assumedCharset = true; - _field = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, _string); + _field = new ContentTypeField(this); } + /** + * @return The {@link Charset} for this type or {@code null} if it is not known + */ public Charset getCharset() { return _charset; @@ -167,6 +170,11 @@ public String getCharsetString() return _charsetString; } + /** + * Check if this type is equal to the type passed as a string + * @param type The type to compare to + * @return {@code true} if this is the same type + */ public boolean is(String type) { return _string.equalsIgnoreCase(type); @@ -183,12 +191,15 @@ public String toString() return _string; } + /** + * @return {@code true} If the {@link Charset} for this type is assumed rather than being explicitly declared. + */ public boolean isCharsetAssumed() { return _assumedCharset; } - public HttpField getContentTypeField() + public ContentTypeField getContentTypeField() { return _field; } @@ -200,6 +211,10 @@ public HttpField getContentTypeField(Charset charset) return new HttpField(HttpHeader.CONTENT_TYPE, getContentTypeWithoutCharset(_string) + ";charset=" + charset.name()); } + /** + * Get the base type of this type, which is the type without a charset specified + * @return The base type or this type if it is a base type + */ public Type getBaseType() { return _base; @@ -227,23 +242,34 @@ public Type getBaseType() }) .build(); + /** + * Get the base value, stripped of any parameters + * @param value The value + * @return A string with any semicolon separated parameters removed + */ + public static String getBase(String value) + { + int index = value.indexOf(';'); + return index == -1 ? value : value.substring(0, index); + } + + /** + * Get the base type of this type, which is the type without a charset specified + * @param contentType The mimetype as a string + * @return The base type or this type if it is a base type + */ public static Type getBaseType(String contentType) { if (StringUtil.isEmpty(contentType)) return null; Type type = CACHE.getBest(contentType); if (type == null) - return null; - if (type.asString().length() == contentType.length()) - return type.getBaseType(); - if (contentType.charAt(type.asString().length()) == ';') - return type.getBaseType(); - contentType = contentType.replace(" ", ""); - if (type.asString().length() == contentType.length()) - return type.getBaseType(); - if (contentType.charAt(type.asString().length()) == ';') - return type.getBaseType(); - return null; + { + type = CACHE.get(getBase(contentType)); + if (type == null) + return null; + } + return type.getBaseType(); } public static boolean isKnownLocale(Locale locale) @@ -326,6 +352,23 @@ public MimeTypes(MimeTypes defaults) } } + /** + * Get the explicit, assumed, or inferred Charset for a HttpField containing a mime type value + * @param field HttpField with a mime type value (e.g. Content-Type) + * @return A {@link Charset} or null; + * @throws IllegalCharsetNameException + * If the given charset name is illegal + * @throws UnsupportedCharsetException + * If no support for the named charset is available + * in this instance of the Java virtual machine + */ + public Charset getCharset(HttpField field) throws IllegalCharsetNameException, UnsupportedCharsetException + { + if (field instanceof ContentTypeField contentTypeField) + return contentTypeField.getMimeType().getCharset(); + return getCharset(field.getValue()); + } + /** * Get the explicit, assumed, or inferred Charset for a mime type * @param mimeType String form or a mimeType @@ -876,4 +919,24 @@ else if (' ' != b) return value; return builder.toString(); } + + public static class ContentTypeField extends PreEncodedHttpField + { + private final Type _type; + public ContentTypeField(MimeTypes.Type type) + { + this(type, type.toString()); + } + + public ContentTypeField(MimeTypes.Type type, String value) + { + super(HttpHeader.CONTENT_TYPE, value); + _type = type; + } + + public Type getMimeType() + { + return _type; + } + } } diff --git a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java index 603c314d7684..faef0134ea63 100644 --- a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java +++ b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java @@ -2601,7 +2601,25 @@ public void testRequestMaxHeaderBytesCumulative(String eoln) @ParameterizedTest @ValueSource(strings = {"\r\n", "\n"}) @SuppressWarnings("ReferenceEquality") - public void testCachedField(String eoln) + public void testInsensitiveCachedField(String eoln) + { + ByteBuffer buffer = BufferUtil.toBuffer( + "GET / HTTP/1.1" + eoln + + "Content-Type: text/plain;Charset=UTF-8" + eoln + + eoln); + + HttpParser.RequestHandler handler = new Handler(); + HttpParser parser = new HttpParser(handler); + parseAll(parser, buffer); + + HttpField field = _fields.get(0); + assertThat(field.getValue(), is("text/plain;charset=utf-8")); + } + + @ParameterizedTest + @ValueSource(strings = {"\r\n", "\n"}) + @SuppressWarnings("ReferenceEquality") + public void testDynamicCachedField(String eoln) { ByteBuffer buffer = BufferUtil.toBuffer( "GET / HTTP/1.1" + eoln + diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java index a3558caf89c3..cd719ff21e60 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java @@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.util.concurrent.CompletableFuture; +import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.io.Content; @@ -51,7 +52,15 @@ public static Charset getFormEncodedCharset(Request request) if (!config.getFormEncodedMethods().contains(request.getMethod())) return null; - String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE); + HttpField contentTypeField= request.getHeaders().getField(HttpHeader.CONTENT_TYPE); + if (contentTypeField instanceof MimeTypes.ContentTypeField contentMimeTypeField) + { + MimeTypes.Type type = contentMimeTypeField.getMimeType(); + if (type != null && type.getCharset() != null) + return type.getCharset(); + } + + String contentType = contentTypeField.getValue(); if (request.getLength() == 0 || StringUtil.isBlank(contentType)) return null; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 84fbc3cd2385..0bdfa5c35ecc 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -37,6 +37,7 @@ import org.eclipse.jetty.http.ComplianceViolation; import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; @@ -535,6 +536,21 @@ static InputStream asInputStream(Request request) return Content.Source.asInputStream(request); } + /** + * Get a known {@link MimeTypes.Type} from the request {@link HttpHeader#CONTENT_TYPE}, if any. + * @param request The request. + * @return A {@link MimeTypes} or {@code null} if the {@code Content-Type} is not set or not known. + */ + static MimeTypes.Type getContentMimeType(Request request) + { + HttpField contentType= request.getHeaders().getField(HttpHeader.CONTENT_TYPE); + if (contentType instanceof MimeTypes.ContentTypeField contentTypeField) + return contentTypeField.getMimeType(); + if (contentType == null) + return null; + return MimeTypes.CACHE.get(contentType.getValue()); + } + /** * Get a {@link Charset} from the request {@link HttpHeader#CONTENT_TYPE}, if any. * @param request The request. @@ -544,7 +560,9 @@ static InputStream asInputStream(Request request) */ static Charset getCharset(Request request) throws IllegalCharsetNameException, UnsupportedCharsetException { - String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE); + HttpField contentType= request.getHeaders().getField(HttpHeader.CONTENT_TYPE); + if (contentType == null) + return null; return Objects.requireNonNullElse(request.getContext().getMimeTypes(), MimeTypes.DEFAULTS).getCharset(contentType); } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java index f06ab857f392..cca4ca9720fa 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ErrorHandlerTest.java @@ -153,7 +153,7 @@ public void test404NoAccept() throws Exception assertThat("Response status code", response.getStatus(), is(404)); assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0)); - assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1")); + assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=iso-8859-1")); assertThat(response.get(HttpHeader.DATE), notNullValue()); assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\"")); @@ -227,7 +227,7 @@ public void test404AllAccept() throws Exception assertThat("Response status code", response.getStatus(), is(404)); assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0)); - assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1")); + assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=iso-8859-1")); assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\"")); assertContent(response); } @@ -245,7 +245,7 @@ public void test404HtmlAccept() throws Exception assertThat("Response status code", response.getStatus(), is(404)); assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0)); - assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1")); + assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=iso-8859-1")); assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\"")); assertContent(response); } @@ -266,7 +266,7 @@ public void test404PostHttp10() throws Exception assertThat(response.getStatus(), is(404)); assertThat(response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0)); - assertThat(response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1")); + assertThat(response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=iso-8859-1")); assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\"")); assertThat(response.get(HttpHeader.CONNECTION), is("keep-alive")); assertContent(response); @@ -288,7 +288,7 @@ public void test404PostHttp11() throws Exception assertThat(response.getStatus(), is(404)); assertThat(response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0)); - assertThat(response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1")); + assertThat(response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=iso-8859-1")); assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\"")); assertThat(response.getField(HttpHeader.CONNECTION), nullValue()); assertContent(response); @@ -307,7 +307,7 @@ public void testMoreSpecificAccept() throws Exception assertThat("Response status code", response.getStatus(), is(404)); assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0)); - assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1")); + assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=iso-8859-1")); assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\"")); assertContent(response); @@ -327,7 +327,7 @@ public void test404HtmlAcceptAnyCharset() throws Exception assertThat("Response status code", response.getStatus(), is(404)); assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0)); - assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=UTF-8")); + assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=utf-8")); assertThat(response.getContent(), containsString("content=\"text/html;charset=UTF-8\"")); assertContent(response); @@ -347,7 +347,7 @@ public void test404HtmlAcceptUtf8Charset() throws Exception assertThat("Response status code", response.getStatus(), is(404)); assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0)); - assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=UTF-8")); + assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=utf-8")); assertThat(response.getContent(), containsString("content=\"text/html;charset=UTF-8\"")); assertContent(response); @@ -400,7 +400,7 @@ public void test404HtmlAcceptUnknownUtf8Charset() throws Exception assertThat("Response status code", response.getStatus(), is(404)); assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0)); - assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=UTF-8")); + assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=utf-8")); assertThat(response.getContent(), containsString("content=\"text/html;charset=UTF-8\"")); assertContent(response); @@ -420,7 +420,7 @@ public void test404PreferHtml() throws Exception assertThat("Response status code", response.getStatus(), is(404)); assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0)); - assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=UTF-8")); + assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=utf-8")); assertThat(response.getContent(), containsString("Error 404 Not Found")); assertContent(response); @@ -457,7 +457,7 @@ public void testThrowBadMessage() throws Exception assertThat("Response status code", response.getStatus(), is(444)); assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0)); - assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1")); + assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=iso-8859-1")); assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\"")); assertContent(response); @@ -475,7 +475,7 @@ public void testBadMessage() throws Exception assertThat("Response status code", response.getStatus(), is(400)); assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0)); - assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1")); + assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=iso-8859-1")); assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\"")); assertContent(response); @@ -601,7 +601,7 @@ public void testComplexCauseMessageNoAcceptHeader(String path) throws Exception assertThat("Response status code", response.getStatus(), is(500)); assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0)); - assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1")); + assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=iso-8859-1")); assertThat(response.getContent(), containsString("content=\"text/html;charset=ISO-8859-1\"")); String content = assertContent(response); @@ -633,7 +633,7 @@ public void testComplexCauseMessageAcceptUtf8Header(String path) throws Exceptio assertThat("Response status code", response.getStatus(), is(500)); assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0)); - assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=UTF-8")); + assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=utf-8")); assertThat(response.getContent(), containsString("content=\"text/html;charset=UTF-8\"")); String content = assertContent(response); diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java index b93a6daa100f..a151e9990bea 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java @@ -965,7 +965,7 @@ public void testUnconsumed() throws Exception checkNotContained(response, offset, "56789"); offset = checkContains(response, offset, "HTTP/1.1 200"); offset = checkContains(response, offset, "pathInContext=/R2"); - offset = checkContains(response, offset, "charset=UTF-8"); + offset = checkContains(response, offset, "charset=utf-8"); checkContains(response, offset, "abcdefghij"); } @@ -1061,7 +1061,7 @@ public void testUnconsumedErrorStream() throws Exception offset = checkContains(response, offset, "HTTP/1.1 599"); offset = checkContains(response, offset, "HTTP/1.1 200"); offset = checkContains(response, offset, "/R2"); - offset = checkContains(response, offset, "text/plain; charset=UTF-8"); + offset = checkContains(response, offset, "text/plain; charset=utf-8"); checkContains(response, offset, "abcdefghij"); } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java index 6cf5573bc50a..14abf2d42383 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java @@ -111,7 +111,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture protected static final String REQUEST2_HEADER = "POST / HTTP/1.0\n" + "Host: localhost\n" + - "Content-Type: text/xml; charset=ISO-8859-1\n" + + "Content-Type: text/xml; charset=iso-8859-1\n" + "Content-Length: "; protected static final String REQUEST2_CONTENT = "\n" + @@ -127,7 +127,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture protected static final String RESPONSE2 = "HTTP/1.1 200 OK\n" + - "Content-Type: text/xml; charset=ISO-8859-1\n" + + "Content-Type: text/xml; charset=iso-8859-1\n" + "Content-Length: " + REQUEST2_CONTENT.getBytes().length + "\n" + "\n" + REQUEST2_CONTENT; diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DefaultHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DefaultHandlerTest.java index 1e5996cc601c..692ef04ac642 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DefaultHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DefaultHandlerTest.java @@ -79,7 +79,7 @@ public void testRoot() throws Exception HttpTester.Response response = HttpTester.parseResponse(input); assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); - assertEquals("text/html;charset=UTF-8", response.get(HttpHeader.CONTENT_TYPE)); + assertEquals("text/html;charset=utf-8", response.get(HttpHeader.CONTENT_TYPE)); String content = new String(response.getContentBytes(), StandardCharsets.UTF_8); assertThat(content, containsString("Contexts known to this server are:")); @@ -105,7 +105,7 @@ public void testSomePath() throws Exception HttpTester.Response response = HttpTester.parseResponse(input); assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); - assertEquals("text/html;charset=ISO-8859-1", response.get(HttpHeader.CONTENT_TYPE)); + assertEquals("text/html;charset=iso-8859-1", response.get(HttpHeader.CONTENT_TYPE)); String content = new String(response.getContentBytes(), StandardCharsets.ISO_8859_1); assertThat(content, not(containsString("Contexts known to this server are:"))); diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java index cd5504e9c120..41326c4a0d53 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java @@ -1183,7 +1183,15 @@ public long getContentLengthLong() public String getContentType() { if (_contentType == null) - _contentType = getFields().get(HttpHeader.CONTENT_TYPE); + { + HttpField contentType = getFields().getField(HttpHeader.CONTENT_TYPE); + if (contentType != null) + { + _contentType = contentType.getValue(); + if (_charset == null && contentType instanceof MimeTypes.ContentTypeField contentTypeField) + _charset = contentTypeField.getMimeType().getCharset(); + } + } return _contentType; } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/CharacterEncodingTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/CharacterEncodingTest.java index f4ad0dfd1dd1..094b827e2787 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/CharacterEncodingTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/CharacterEncodingTest.java @@ -134,7 +134,7 @@ public void testCharacterEncodingSetTwice() throws Exception // Now test for properly formatted HTTP Response Headers. assertThat("Response Code", response.getStatus(), is(200)); - assertThat(response.get(HttpHeader.CONTENT_TYPE), is("text/plain;charset=UTF-8")); + assertThat(response.get(HttpHeader.CONTENT_TYPE), is("text/plain;charset=utf-8")); } } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java index 41845f7dfd75..ba25912b6289 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java @@ -494,7 +494,7 @@ public void testSimpleListing() throws Exception HttpTester.Response response = HttpTester.parseResponse(rawResponse); assertThat(response.getStatus(), is(200)); - assertThat(response.getField("content-type").getValue(), is("text/html;charset=UTF-8")); + assertThat(response.getField("content-type").getValue(), is("text/html;charset=utf-8")); String body = response.getContent(); assertThat(body, containsString("")); } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ResourceServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ResourceServletTest.java index 7d470cbbe618..89828695530b 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ResourceServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ResourceServletTest.java @@ -557,7 +557,7 @@ public void testSimpleListing() throws Exception HttpTester.Response response = HttpTester.parseResponse(rawResponse); assertThat(response.getStatus(), is(200)); - assertThat(response.getField("content-type").getValue(), is("text/html;charset=UTF-8")); + assertThat(response.getField("content-type").getValue(), is("text/html;charset=utf-8")); String body = response.getContent(); assertThat(body, containsString("")); } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ResponseHeadersTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ResponseHeadersTest.java index fd5848e70d0d..939357f5dfc2 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ResponseHeadersTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ResponseHeadersTest.java @@ -146,7 +146,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse response) throw // Now test for properly formatted HTTP Response Headers. assertThat("Response Code", response.getStatus(), is(200)); - assertThat("Response Header Content-Type", response.get("Content-Type"), is("text/plain;charset=UTF-8")); + assertThat("Response Header Content-Type", response.get("Content-Type"), is("text/plain;charset=utf-8")); String expected = StringUtil.replace(actualPathInfo, "%0A", " "); // replace OBS fold with space expected = URLDecoder.decode(expected, StandardCharsets.UTF_8); // decode the rest @@ -189,7 +189,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t // Now test for properly formatted HTTP Response Headers. assertThat("Response Code", response.getStatus(), is(200)); // The Content-Type should not have a charset= portion - assertThat("Response Header Content-Type", response.get("Content-Type"), is("text/html;charset=UTF-8")); + assertThat("Response Header Content-Type", response.get("Content-Type"), is("text/html;charset=utf-8")); } @Test diff --git a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java index 5a7203b31009..4aaa4ced8cb3 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java @@ -1192,7 +1192,15 @@ public long getContentLengthLong() public String getContentType() { if (_contentType == null) - _contentType = getFields().get(HttpHeader.CONTENT_TYPE); + { + HttpField contentType = getFields().getField(HttpHeader.CONTENT_TYPE); + if (contentType != null) + { + _contentType = contentType.getValue(); + if (_charset == null && contentType instanceof MimeTypes.ContentTypeField contentTypeField) + _charset = contentTypeField.getMimeType().getCharset(); + } + } return _contentType; } diff --git a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/CharacterEncodingTest.java b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/CharacterEncodingTest.java index 79ec8f1d0a75..c7b414c14e28 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/CharacterEncodingTest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/CharacterEncodingTest.java @@ -134,7 +134,7 @@ public void testCharacterEncodingSetTwice() throws Exception // Now test for properly formatted HTTP Response Headers. assertThat("Response Code", response.getStatus(), is(200)); - assertThat(response.get(HttpHeader.CONTENT_TYPE), is("text/plain;charset=UTF-8")); + assertThat(response.get(HttpHeader.CONTENT_TYPE), is("text/plain;charset=utf-8")); } } diff --git a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/DefaultServletTest.java b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/DefaultServletTest.java index b2618dd5f397..ed8653dd4638 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/DefaultServletTest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/DefaultServletTest.java @@ -494,7 +494,7 @@ public void testSimpleListing() throws Exception HttpTester.Response response = HttpTester.parseResponse(rawResponse); assertThat(response.getStatus(), is(200)); - assertThat(response.getField("content-type").getValue(), is("text/html;charset=UTF-8")); + assertThat(response.getField("content-type").getValue(), is("text/html;charset=utf-8")); String body = response.getContent(); assertThat(body, containsString("")); } diff --git a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResourceServletTest.java b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResourceServletTest.java index 0100fe971f38..b87c4967188b 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResourceServletTest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResourceServletTest.java @@ -555,7 +555,7 @@ public void testSimpleListing() throws Exception HttpTester.Response response = HttpTester.parseResponse(rawResponse); assertThat(response.getStatus(), is(200)); - assertThat(response.getField("content-type").getValue(), is("text/html;charset=UTF-8")); + assertThat(response.getField("content-type").getValue(), is("text/html;charset=utf-8")); String body = response.getContent(); assertThat(body, containsString("")); } diff --git a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResponseHeadersTest.java b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResponseHeadersTest.java index 77d43ffa9278..33b079575f7b 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResponseHeadersTest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResponseHeadersTest.java @@ -147,7 +147,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse response) throw // Now test for properly formatted HTTP Response Headers. assertThat("Response Code", response.getStatus(), is(200)); - assertThat("Response Header Content-Type", response.get("Content-Type"), is("text/plain;charset=UTF-8")); + assertThat("Response Header Content-Type", response.get("Content-Type"), is("text/plain;charset=utf-8")); String expected = StringUtil.replace(actualPathInfo, "%0A", " "); // replace OBS fold with space expected = URLDecoder.decode(expected, StandardCharsets.UTF_8); // decode the rest @@ -190,7 +190,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t // Now test for properly formatted HTTP Response Headers. assertThat("Response Code", response.getStatus(), is(200)); // The Content-Type should not have a charset= portion - assertThat("Response Header Content-Type", response.get("Content-Type"), is("text/html;charset=UTF-8")); + assertThat("Response Header Content-Type", response.get("Content-Type"), is("text/html;charset=utf-8")); } @Test From ccec3e4176d62023bc5b9ff84ad8243eeb732931 Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 4 Oct 2024 11:21:28 +1000 Subject: [PATCH 17/61] Use lowercase for charsets #11741 Fix #11741 as per the WhatTFWG recommendations, use lower case for charset names. Took the opportunity for some minor optimizations: + use the already made HttpField instance in MimeTypes.Type rather than create a new one in the HttpParser.CACHE + keep the MimeType.Type associated with the pre encoded Content-Type fields --- .../src/main/java/org/eclipse/jetty/http/MimeTypes.java | 1 + 1 file changed, 1 insertion(+) diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java index f7232f42adb9..4b299e2c0ee4 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java @@ -923,6 +923,7 @@ else if (' ' != b) public static class ContentTypeField extends PreEncodedHttpField { private final Type _type; + public ContentTypeField(MimeTypes.Type type) { this(type, type.toString()); From 1001c318b10c8ec1b526ddb339b49922892c5401 Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 4 Oct 2024 11:24:16 +1000 Subject: [PATCH 18/61] Use lowercase for charsets #11741 Fix #11741 as per the WhatTFWG recommendations, use lower case for charset names. Took the opportunity for some minor optimizations: + use the already made HttpField instance in MimeTypes.Type rather than create a new one in the HttpParser.CACHE + keep the MimeType.Type associated with the pre encoded Content-Type fields --- .../src/main/java/org/eclipse/jetty/server/FormFields.java | 2 +- .../src/main/java/org/eclipse/jetty/server/Request.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java index cd719ff21e60..9f15a7943772 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java @@ -52,7 +52,7 @@ public static Charset getFormEncodedCharset(Request request) if (!config.getFormEncodedMethods().contains(request.getMethod())) return null; - HttpField contentTypeField= request.getHeaders().getField(HttpHeader.CONTENT_TYPE); + HttpField contentTypeField = request.getHeaders().getField(HttpHeader.CONTENT_TYPE); if (contentTypeField instanceof MimeTypes.ContentTypeField contentMimeTypeField) { MimeTypes.Type type = contentMimeTypeField.getMimeType(); diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 0bdfa5c35ecc..5583af0d4944 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -543,7 +543,7 @@ static InputStream asInputStream(Request request) */ static MimeTypes.Type getContentMimeType(Request request) { - HttpField contentType= request.getHeaders().getField(HttpHeader.CONTENT_TYPE); + HttpField contentType = request.getHeaders().getField(HttpHeader.CONTENT_TYPE); if (contentType instanceof MimeTypes.ContentTypeField contentTypeField) return contentTypeField.getMimeType(); if (contentType == null) @@ -560,7 +560,7 @@ static MimeTypes.Type getContentMimeType(Request request) */ static Charset getCharset(Request request) throws IllegalCharsetNameException, UnsupportedCharsetException { - HttpField contentType= request.getHeaders().getField(HttpHeader.CONTENT_TYPE); + HttpField contentType = request.getHeaders().getField(HttpHeader.CONTENT_TYPE); if (contentType == null) return null; return Objects.requireNonNullElse(request.getContext().getMimeTypes(), MimeTypes.DEFAULTS).getCharset(contentType); From 8c25183f9d52cd9dae887c09598c46296af2c38b Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 4 Oct 2024 15:19:41 +1000 Subject: [PATCH 19/61] Use lowercase for charsets #11741 Fix #11741 as per the WhatTFWG recommendations, use lower case for charset names. Took the opportunity for some minor optimizations: + use the already made HttpField instance in MimeTypes.Type rather than create a new one in the HttpParser.CACHE + keep the MimeType.Type associated with the pre encoded Content-Type fields --- .../src/main/java/org/eclipse/jetty/server/FormFields.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java index 9f15a7943772..d75ec94980dd 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java @@ -53,6 +53,9 @@ public static Charset getFormEncodedCharset(Request request) return null; HttpField contentTypeField = request.getHeaders().getField(HttpHeader.CONTENT_TYPE); + if (contentTypeField == null) + return null; + if (contentTypeField instanceof MimeTypes.ContentTypeField contentMimeTypeField) { MimeTypes.Type type = contentMimeTypeField.getMimeType(); From 3d7f5da03cfc1b3c6270d75decfc3a910f4d0aae Mon Sep 17 00:00:00 2001 From: gregw Date: Sun, 6 Oct 2024 11:28:21 +1100 Subject: [PATCH 20/61] Use lowercase for charsets #11741 Fix #11741 as per the WhatTFWG recommendations, use lower case for charset names. Took the opportunity for some minor optimizations: + use the already made HttpField instance in MimeTypes.Type rather than create a new one in the HttpParser.CACHE + keep the MimeType.Type associated with the pre encoded Content-Type fields --- .../eclipse/jetty/client/ContentResponseTest.java | 6 ++++-- .../client/util/TypedContentProviderTest.java | 3 ++- .../ee10/test/HttpInputTransientErrorTest.java | 7 ++++--- .../eclipse/jetty/ee11/servlet/ResponseTest.java | 3 ++- .../ee11/test/HttpInputTransientErrorTest.java | 15 ++++++++------- .../ee9/test/HttpInputTransientErrorTest.java | 2 +- 6 files changed, 21 insertions(+), 15 deletions(-) diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ContentResponseTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ContentResponseTest.java index 0258053a5d31..a3715ff3bda0 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ContentResponseTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ContentResponseTest.java @@ -23,9 +23,11 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.Callback; +import org.hamcrest.Matchers; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -114,7 +116,7 @@ public boolean handle(Request request, Response response, Callback callback) thr assertEquals(200, response.getStatus()); assertEquals(content, response.getContentAsString()); assertEquals(mediaType, response.getMediaType()); - assertEquals(encoding, response.getEncoding()); + assertThat(response.getEncoding(), Matchers.equalToIgnoringCase(encoding)); } @ParameterizedTest @@ -144,6 +146,6 @@ public boolean handle(Request request, Response response, Callback callback) thr assertEquals(200, response.getStatus()); assertEquals(content, response.getContentAsString()); assertEquals(mediaType, response.getMediaType()); - assertEquals(encoding, response.getEncoding()); + assertThat(response.getEncoding(), Matchers.equalToIgnoringCase(encoding)); } } diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/util/TypedContentProviderTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/util/TypedContentProviderTest.java index 72515336bbf6..1057d69585bf 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/util/TypedContentProviderTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/util/TypedContentProviderTest.java @@ -29,6 +29,7 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.Fields; +import org.hamcrest.Matchers; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; @@ -96,7 +97,7 @@ public void testFormContentProviderWithDifferentContentType(Scenario scenario) t protected void service(Request request, Response response) throws Throwable { assertEquals("POST", request.getMethod()); - assertEquals(contentType, request.getHeaders().get(HttpHeader.CONTENT_TYPE)); + assertThat(request.getHeaders().get(HttpHeader.CONTENT_TYPE), Matchers.equalToIgnoringCase(contentType)); assertEquals(content, Content.Source.asString(request)); } }); diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputTransientErrorTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputTransientErrorTest.java index a767e2edea3b..57fdb3f1697c 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputTransientErrorTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputTransientErrorTest.java @@ -46,6 +46,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -181,7 +182,7 @@ public void onError(Throwable t) assertThat("Unexpected response status\n" + response + response.getContent(), response.getStatus(), is(HttpStatus.OK_200)); assertThat(response.get(HttpHeader.CONNECTION), nullValue()); - assertThat(response.get(HttpHeader.CONTENT_TYPE), is("text/plain;charset=UTF-8")); + assertThat(response.get(HttpHeader.CONTENT_TYPE), equalToIgnoringCase("text/plain;charset=utf-8")); assertThat(response.getContent(), containsString("read=10")); assertInstanceOf(TimeoutException.class, failure.get()); assertThat(events, contains("onError", "onAllDataRead")); @@ -273,7 +274,7 @@ public void onError(Throwable t) HttpTester.Response response = HttpTester.parseResponse(localEndPoint.getResponse(false, 5, TimeUnit.SECONDS)); assertThat("Unexpected response status\n" + response + response.getContent(), response.getStatus(), is(HttpStatus.OK_200)); - assertThat(response.get(HttpHeader.CONTENT_TYPE), is("text/plain;charset=UTF-8")); + assertThat(response.get(HttpHeader.CONTENT_TYPE), equalToIgnoringCase("text/plain;charset=utf-8")); assertThat(response.getContent(), containsString("read=10")); assertThat(failure.get(), nullValue()); } @@ -382,7 +383,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I HttpTester.Response response = HttpTester.parseResponse(localEndPoint.getResponse(false, 5, TimeUnit.SECONDS)); assertThat("Unexpected response status\n" + response + response.getContent(), response.getStatus(), is(HttpStatus.OK_200)); - assertThat(response.get(HttpHeader.CONTENT_TYPE), is("text/plain;charset=UTF-8")); + assertThat(response.get(HttpHeader.CONTENT_TYPE), equalToIgnoringCase("text/plain;charset=utf-8")); assertThat(response.getContent(), containsString("read=10")); assertInstanceOf(IOException.class, failure.get()); assertInstanceOf(TimeoutException.class, failure.get().getCause()); diff --git a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResponseTest.java b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResponseTest.java index 4823fe15ae5a..b29a5b81f8f9 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResponseTest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResponseTest.java @@ -47,6 +47,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; @@ -140,7 +141,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t HttpTester.Response response = HttpTester.parseResponse(responseBuffer); assertThat(response.getStatus(), is(410)); - assertThat(response.get("Content-Type"), is("text/html;charset=ISO-8859-1")); + assertThat(response.get("Content-Type"), equalToIgnoringCase("text/html;charset=iso-8859-1")); assertThat(response.getContent(), containsString("The content is gone.")); } diff --git a/jetty-ee11/jetty-ee11-tests/jetty-ee11-test-integration/src/test/java/org/eclipse/jetty/ee11/test/HttpInputTransientErrorTest.java b/jetty-ee11/jetty-ee11-tests/jetty-ee11-test-integration/src/test/java/org/eclipse/jetty/ee11/test/HttpInputTransientErrorTest.java index ff99bb3d7379..1fb31407f909 100644 --- a/jetty-ee11/jetty-ee11-tests/jetty-ee11-test-integration/src/test/java/org/eclipse/jetty/ee11/test/HttpInputTransientErrorTest.java +++ b/jetty-ee11/jetty-ee11-tests/jetty-ee11-test-integration/src/test/java/org/eclipse/jetty/ee11/test/HttpInputTransientErrorTest.java @@ -45,6 +45,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -101,7 +102,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I { AsyncContext asyncContext = req.startAsync(req, resp); asyncContext.setTimeout(0); - resp.setContentType("text/plain;charset=UTF-8"); + resp.setContentType("text/plain;charset=utf-8"); // Since the client sends a request with a content-length header, but sends // the content only after idle timeout expired, this ReadListener will have @@ -132,7 +133,7 @@ public void onAllDataRead() throws IOException { events.add("onAllDataRead"); resp.setStatus(HttpStatus.OK_200); - resp.setContentType("text/plain;charset=UTF-8"); + resp.setContentType("text/plain;charset=utf-8"); resp.getWriter().println("read=" + counter.get()); asyncContext.complete(); } @@ -180,7 +181,7 @@ public void onError(Throwable t) assertThat("Unexpected response status\n" + response + response.getContent(), response.getStatus(), is(HttpStatus.OK_200)); assertThat(response.get(HttpHeader.CONNECTION), nullValue()); - assertThat(response.get(HttpHeader.CONTENT_TYPE), is("text/plain;charset=UTF-8")); + assertThat(response.get(HttpHeader.CONTENT_TYPE), equalToIgnoringCase("text/plain;charset=utf-8")); assertThat(response.getContent(), containsString("read=10")); assertInstanceOf(TimeoutException.class, failure.get()); assertThat(events, contains("onError", "onAllDataRead")); @@ -200,7 +201,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) { AsyncContext asyncContext = req.startAsync(req, resp); asyncContext.setTimeout(0); - resp.setContentType("text/plain;charset=UTF-8"); + resp.setContentType("text/plain;charset=utf-8"); // Not calling setReadListener will make Jetty set the ServletChannelState // in state WAITING upon doPost return, so idle timeouts are ignored. @@ -272,7 +273,7 @@ public void onError(Throwable t) HttpTester.Response response = HttpTester.parseResponse(localEndPoint.getResponse(false, 5, TimeUnit.SECONDS)); assertThat("Unexpected response status\n" + response + response.getContent(), response.getStatus(), is(HttpStatus.OK_200)); - assertThat(response.get(HttpHeader.CONTENT_TYPE), is("text/plain;charset=UTF-8")); + assertThat(response.get(HttpHeader.CONTENT_TYPE), equalToIgnoringCase("text/plain;charset=utf-8")); assertThat(response.getContent(), containsString("read=10")); assertThat(failure.get(), nullValue()); } @@ -362,7 +363,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String content = IO.toString(req.getInputStream()); resp.setStatus(HttpStatus.OK_200); - resp.setContentType("text/plain;charset=UTF-8"); + resp.setContentType("text/plain;charset=utf-8"); resp.getWriter().println("read=" + content.length()); } }); @@ -381,7 +382,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I HttpTester.Response response = HttpTester.parseResponse(localEndPoint.getResponse(false, 5, TimeUnit.SECONDS)); assertThat("Unexpected response status\n" + response + response.getContent(), response.getStatus(), is(HttpStatus.OK_200)); - assertThat(response.get(HttpHeader.CONTENT_TYPE), is("text/plain;charset=UTF-8")); + assertThat(response.get(HttpHeader.CONTENT_TYPE), equalToIgnoringCase("text/plain;charset=utf-8")); assertThat(response.getContent(), containsString("read=10")); assertInstanceOf(IOException.class, failure.get()); assertInstanceOf(TimeoutException.class, failure.get().getCause()); diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-integration/src/test/java/org/eclipse/jetty/ee9/test/HttpInputTransientErrorTest.java b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-integration/src/test/java/org/eclipse/jetty/ee9/test/HttpInputTransientErrorTest.java index 2d040c460bfa..1f5fceb0f4dc 100644 --- a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-integration/src/test/java/org/eclipse/jetty/ee9/test/HttpInputTransientErrorTest.java +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-integration/src/test/java/org/eclipse/jetty/ee9/test/HttpInputTransientErrorTest.java @@ -94,7 +94,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I { AsyncContext asyncContext = req.startAsync(req, resp); asyncContext.setTimeout(0); - resp.setContentType("text/plain;charset=UTF-8"); + resp.setContentType("text/plain;charset=utf-8"); req.getInputStream().setReadListener(new ReadListener() { From 6c0b6f94cb3a63ea98afee5a54bd62d02600a284 Mon Sep 17 00:00:00 2001 From: gregw Date: Mon, 7 Oct 2024 08:40:45 +1100 Subject: [PATCH 21/61] Use lowercase for charsets #11741 Fix #11741 as per the WhatTFWG recommendations, use lower case for charset names. Took the opportunity for some minor optimizations: + use the already made HttpField instance in MimeTypes.Type rather than create a new one in the HttpParser.CACHE + keep the MimeType.Type associated with the pre encoded Content-Type fields --- .../org/eclipse/jetty/ee10/servlet/ResponseHeadersTest.java | 3 ++- .../org/eclipse/jetty/ee11/servlet/ResponseHeadersTest.java | 3 ++- .../org/eclipse/jetty/ee9/servlet/ResponseHeadersTest.java | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ResponseHeadersTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ResponseHeadersTest.java index 939357f5dfc2..3100f32c5cab 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ResponseHeadersTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ResponseHeadersTest.java @@ -40,6 +40,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; @@ -146,7 +147,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse response) throw // Now test for properly formatted HTTP Response Headers. assertThat("Response Code", response.getStatus(), is(200)); - assertThat("Response Header Content-Type", response.get("Content-Type"), is("text/plain;charset=utf-8")); + assertThat("Response Header Content-Type", response.get("Content-Type"), equalToIgnoringCase("text/plain;charset=utf-8")); String expected = StringUtil.replace(actualPathInfo, "%0A", " "); // replace OBS fold with space expected = URLDecoder.decode(expected, StandardCharsets.UTF_8); // decode the rest diff --git a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResponseHeadersTest.java b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResponseHeadersTest.java index 33b079575f7b..3b8c67f0ce32 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResponseHeadersTest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResponseHeadersTest.java @@ -41,6 +41,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; @@ -147,7 +148,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse response) throw // Now test for properly formatted HTTP Response Headers. assertThat("Response Code", response.getStatus(), is(200)); - assertThat("Response Header Content-Type", response.get("Content-Type"), is("text/plain;charset=utf-8")); + assertThat("Response Header Content-Type", response.get("Content-Type"), equalToIgnoringCase("text/plain;charset=utf-8")); String expected = StringUtil.replace(actualPathInfo, "%0A", " "); // replace OBS fold with space expected = URLDecoder.decode(expected, StandardCharsets.UTF_8); // decode the rest diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ResponseHeadersTest.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ResponseHeadersTest.java index f24a77fa5e5b..43f8edf09192 100644 --- a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ResponseHeadersTest.java +++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ResponseHeadersTest.java @@ -44,6 +44,7 @@ import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -242,7 +243,7 @@ public void testMultilineResponseHeaderValue() throws Exception // Now test for properly formatted HTTP Response Headers. assertThat("Response Code", response.getStatus(), is(200)); - assertThat("Response Header Content-Type", response.get("Content-Type"), is("text/plain;charset=UTF-8")); + assertThat("Response Header Content-Type", response.get("Content-Type"), equalToIgnoringCase("text/plain;charset=utf-8")); String expected = StringUtil.replace(actualPathInfo, "%0a", " "); // replace OBS fold with space expected = StringUtil.replace(expected, "%0A", " "); // replace OBS fold with space From c87adb64dbb68212217e06e94154da2b998dbf97 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 10 Oct 2024 08:35:40 +1100 Subject: [PATCH 22/61] javadoc --- .../src/main/java/org/eclipse/jetty/http/MimeTypes.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java index 4b299e2c0ee4..82c7c7035946 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java @@ -920,6 +920,10 @@ else if (' ' != b) return builder.toString(); } + /** + * A {@link PreEncodedHttpField} for `Content-Type` that can hold a {@link MimeTypes.Type} field + * for later recovery. + */ public static class ContentTypeField extends PreEncodedHttpField { private final Type _type; From 1172d595b181f654b2be1d2fd56971f371cb7fd4 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 15 Oct 2024 08:53:19 +1100 Subject: [PATCH 23/61] updates from review --- .../migration/ServletToHandlerDocs.java | 16 ++++-- .../org/eclipse/jetty/http/HttpParser.java | 5 +- .../org/eclipse/jetty/http/MimeTypes.java | 49 ++++++++++++++++++- .../org/eclipse/jetty/server/FormFields.java | 35 ++----------- .../org/eclipse/jetty/server/Request.java | 15 ------ .../jetty/ee10/servlet/ServletApiRequest.java | 4 +- .../jetty/ee11/servlet/ServletApiRequest.java | 4 +- 7 files changed, 71 insertions(+), 57 deletions(-) diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java index d6a0538641fc..151e4049abe6 100644 --- a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java @@ -25,6 +25,7 @@ import java.util.function.Supplier; import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; @@ -120,9 +121,18 @@ public boolean handle(Request request, Response response, Callback callback) thr // Gets the request Content-Type. // Replaces: // - servletRequest.getContentType() - MimeTypes.Type mimeType = Request.getContentMimeType(request); - Charset charset = Request.getCharset(request); - String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE); + // - servletRequest.getCharacterEncoding() + HttpField contentTypeField = request.getHeaders().getField(HttpHeader.CONTENT_TYPE); + MimeTypes.Type knownType = MimeTypes.getMimeTypeFromContentType(contentTypeField); + if (knownType != null) + { + Charset charset = knownType.getCharset(); + } + else + { + String contentType = contentTypeField.getValue(); + String charset = MimeTypes.getCharsetFromContentType(contentType); + } // Gets the request Content-Length. // Replaces: diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java index 37109020bc04..a323efcbb774 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java @@ -141,11 +141,12 @@ public class HttpParser Map map = new LinkedHashMap<>(); for (MimeTypes.Type mimetype : MimeTypes.Type.values()) { - MimeTypes.ContentTypeField contentTypeField = mimetype.getContentTypeField(); + HttpField contentTypeField = mimetype.getContentTypeField(); map.put(contentTypeField.toString(), contentTypeField); if (contentTypeField.getValue().contains(";charset=")) { - HttpField contentTypeFieldWithSpace = new MimeTypes.ContentTypeField(contentTypeField.getMimeType(), contentTypeField.getValue().replace(";charset=", "; charset=")); + HttpField contentTypeFieldWithSpace = + new MimeTypes.ContentTypeField(MimeTypes.getMimeTypeFromContentType(contentTypeField), contentTypeField.getValue().replace(";charset=", "; charset=")); map.put(contentTypeFieldWithSpace.toString(), contentTypeFieldWithSpace); } } diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java index 82c7c7035946..119931fa2572 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java @@ -199,7 +199,7 @@ public boolean isCharsetAssumed() return _assumedCharset; } - public ContentTypeField getContentTypeField() + public HttpField getContentTypeField() { return _field; } @@ -681,6 +681,46 @@ private static String normalizeMimeType(String type) return StringUtil.asciiToLowerCase(type); } + public static MimeTypes.Type getMimeTypeFromContentType(HttpField field) + { + if (field == null) + return null; + + assert field.getHeader() == HttpHeader.CONTENT_TYPE; + + if (field instanceof MimeTypes.ContentTypeField contentTypeField) + return contentTypeField.getMimeType(); + + return MimeTypes.CACHE.get(field.getValue()); + } + + /** + * Efficiently extract the charset value from a {@code Content-Type} {@link HttpField}. + * @param field A {@code Content-Type} field. + * @return The {@link Charset} + */ + public static Charset getCharsetFromContentType(HttpField field) + { + if (field == null) + return null; + + assert field.getHeader() == HttpHeader.CONTENT_TYPE; + + if (field instanceof ContentTypeField contentTypeField) + return contentTypeField._type.getCharset(); + + String charset = getCharsetFromContentType(field.getValue()); + if (charset == null) + return null; + + return Charset.forName(charset); + } + + /** + * Efficiently extract the charset value from a {@code Content-Type} string + * @param value A content-type value (e.g. {@code text/plain; charset=utf8}). + * @return The charset value (e.g. {@code utf-8}). + */ public static String getCharsetFromContentType(String value) { if (value == null) @@ -794,6 +834,11 @@ else if (' ' != b) return null; } + /** + * Efficiently extract the base mime-type from a content-type value + * @param value A content-type value (e.g. {@code text/plain; charset=utf8}). + * @return The base mime-type value (e.g. {@code text/plain}). + */ public static String getContentTypeWithoutCharset(String value) { int end = value.length(); @@ -924,7 +969,7 @@ else if (' ' != b) * A {@link PreEncodedHttpField} for `Content-Type` that can hold a {@link MimeTypes.Type} field * for later recovery. */ - public static class ContentTypeField extends PreEncodedHttpField + static class ContentTypeField extends PreEncodedHttpField { private final Type _type; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java index d75ec94980dd..e8c22f7daec7 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java @@ -28,7 +28,6 @@ import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.CharsetStringBuilder; import org.eclipse.jetty.util.Fields; -import org.eclipse.jetty.util.StringUtil; import static org.eclipse.jetty.util.UrlEncoded.decodeHexByte; @@ -56,37 +55,11 @@ public static Charset getFormEncodedCharset(Request request) if (contentTypeField == null) return null; - if (contentTypeField instanceof MimeTypes.ContentTypeField contentMimeTypeField) - { - MimeTypes.Type type = contentMimeTypeField.getMimeType(); - if (type != null && type.getCharset() != null) - return type.getCharset(); - } - - String contentType = contentTypeField.getValue(); - if (request.getLength() == 0 || StringUtil.isBlank(contentType)) - return null; - - String contentTypeWithoutCharset = MimeTypes.getContentTypeWithoutCharset(contentType); - MimeTypes.Type type = MimeTypes.CACHE.get(contentTypeWithoutCharset); - if (type != null) - { - if (type != MimeTypes.Type.FORM_ENCODED) - return null; - } - else - { - // Could be a non-cached Content-Type with other parameters such as "application/x-www-form-urlencoded; p=v". - // Verify that it is actually application/x-www-form-urlencoded. - int semi = contentTypeWithoutCharset.indexOf(';'); - if (semi > 0) - contentTypeWithoutCharset = contentTypeWithoutCharset.substring(0, semi); - if (!MimeTypes.Type.FORM_ENCODED.is(contentTypeWithoutCharset.trim())) - return null; - } + Charset charset = MimeTypes.getCharsetFromContentType(contentTypeField); + if (charset != null) + return charset; - String cs = MimeTypes.getCharsetFromContentType(contentType); - return StringUtil.isEmpty(cs) ? StandardCharsets.UTF_8 : Charset.forName(cs); + return StandardCharsets.UTF_8; } /** diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 5583af0d4944..757df3c518c5 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -536,21 +536,6 @@ static InputStream asInputStream(Request request) return Content.Source.asInputStream(request); } - /** - * Get a known {@link MimeTypes.Type} from the request {@link HttpHeader#CONTENT_TYPE}, if any. - * @param request The request. - * @return A {@link MimeTypes} or {@code null} if the {@code Content-Type} is not set or not known. - */ - static MimeTypes.Type getContentMimeType(Request request) - { - HttpField contentType = request.getHeaders().getField(HttpHeader.CONTENT_TYPE); - if (contentType instanceof MimeTypes.ContentTypeField contentTypeField) - return contentTypeField.getMimeType(); - if (contentType == null) - return null; - return MimeTypes.CACHE.get(contentType.getValue()); - } - /** * Get a {@link Charset} from the request {@link HttpHeader#CONTENT_TYPE}, if any. * @param request The request. diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java index 41326c4a0d53..e120c88b722e 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java @@ -1188,8 +1188,8 @@ public String getContentType() if (contentType != null) { _contentType = contentType.getValue(); - if (_charset == null && contentType instanceof MimeTypes.ContentTypeField contentTypeField) - _charset = contentTypeField.getMimeType().getCharset(); + if (_charset == null) + _charset = MimeTypes.getCharsetFromContentType(contentType); } } return _contentType; diff --git a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java index 4aaa4ced8cb3..cd5f487b2533 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java @@ -1197,8 +1197,8 @@ public String getContentType() if (contentType != null) { _contentType = contentType.getValue(); - if (_charset == null && contentType instanceof MimeTypes.ContentTypeField contentTypeField) - _charset = contentTypeField.getMimeType().getCharset(); + if (_charset == null) + _charset = MimeTypes.getCharsetFromContentType(contentType); } } return _contentType; From 7913cbbb103db7e2c4086c0e501f80c0a90ddf2b Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 15 Oct 2024 09:29:36 +1100 Subject: [PATCH 24/61] updates from review --- .../migration/ServletToHandlerDocs.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java index 151e4049abe6..186e87788bdd 100644 --- a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java @@ -121,18 +121,16 @@ public boolean handle(Request request, Response response, Callback callback) thr // Gets the request Content-Type. // Replaces: // - servletRequest.getContentType() - // - servletRequest.getCharacterEncoding() HttpField contentTypeField = request.getHeaders().getField(HttpHeader.CONTENT_TYPE); + String contentType = contentTypeField.getValue(); MimeTypes.Type knownType = MimeTypes.getMimeTypeFromContentType(contentTypeField); - if (knownType != null) - { - Charset charset = knownType.getCharset(); - } - else - { - String contentType = contentTypeField.getValue(); - String charset = MimeTypes.getCharsetFromContentType(contentType); - } + + // Gets the request Character Encoding. + // Replaces: + // - servletRequest.getCharacterEncoding() + Charset charset = knownType == null + ? MimeTypes.getCharsetFromContentType(contentTypeField) + : knownType.getCharset(); // Gets the request Content-Length. // Replaces: From 4c5be88666b795aaa07d18bbf4637e25e8006144 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 15 Oct 2024 11:47:55 +1100 Subject: [PATCH 25/61] WIP --- .../java/org/eclipse/jetty/maven/ServerSupport.java | 1 + .../jetty/server/handler/DelayedHandler.java | 13 +++++++++---- .../jetty/server/internal/HttpConnection.java | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/jetty-core/jetty-maven/src/main/java/org/eclipse/jetty/maven/ServerSupport.java b/jetty-core/jetty-maven/src/main/java/org/eclipse/jetty/maven/ServerSupport.java index f06de7a3d5eb..99178482737e 100644 --- a/jetty-core/jetty-maven/src/main/java/org/eclipse/jetty/maven/ServerSupport.java +++ b/jetty-core/jetty-maven/src/main/java/org/eclipse/jetty/maven/ServerSupport.java @@ -21,6 +21,7 @@ import org.eclipse.jetty.security.LoginService; import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.RequestLog; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java index d5d2ed5e03fb..fb86299d1b20 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -56,6 +56,7 @@ public boolean handle(Request request, Response response, Callback callback) thr boolean contentExpected = false; String contentType = null; + MimeTypes.Type mimeType = null; loop: for (HttpField field : request.getHeaders()) { HttpHeader header = field.getHeader(); @@ -65,6 +66,7 @@ public boolean handle(Request request, Response response, Callback callback) thr { case CONTENT_TYPE: contentType = field.getValue(); + mimeType = MimeTypes.getMimeTypeFromContentType(field); break; case CONTENT_LENGTH: @@ -87,7 +89,6 @@ public boolean handle(Request request, Response response, Callback callback) thr } } - MimeTypes.Type mimeType = MimeTypes.getBaseType(contentType); DelayedProcess delayed = newDelayedProcess(contentExpected, contentType, mimeType, next, request, response, callback); if (delayed == null) return next.handle(request, response, callback); @@ -102,9 +103,12 @@ protected DelayedProcess newDelayedProcess(boolean contentExpected, String conte if (!contentExpected) return null; - // if no mimeType, then no delay + // are we configured to delay dispatch until content? + boolean delayDispatchUntilContent = request.getConnectionMetaData().getHttpConfiguration().isDelayDispatchUntilContent(); + + // if no known mimeType, then only delay until content if configured if (mimeType == null) - return new UntilContentDelayedProcess(handler, request, response, callback); + return delayDispatchUntilContent ? new UntilContentDelayedProcess(handler, request, response, callback) : null; // Otherwise, delay until a known content type is fully read; or if the type is not known then until the content is available return switch (mimeType) @@ -122,7 +126,8 @@ else if (getHandler().getServer().getAttribute(MultiPartConfig.class.getName()) yield new UntilMultipartDelayedProcess(handler, request, response, callback, contentType, config); } - default -> new UntilContentDelayedProcess(handler, request, response, callback); + // if other mimeType, then only delay until content if configured + default -> delayDispatchUntilContent ? new UntilContentDelayedProcess(handler, request, response, callback) : null; }; } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index d70ae3016344..0f739d54333c 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -1401,6 +1401,7 @@ public void send(MetaData.Request request, MetaData.Response response, boolean l else if (_generator.isCommitted()) { callback.failed(new IllegalStateException("Committed")); + return; } else { From bb37636381f1bb035b9f3f8696b4e4012c949f40 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 15 Oct 2024 17:36:45 +1100 Subject: [PATCH 26/61] Experiment to reuse buffer in HttpConnection to make retaining chunks more efficient --- .../jetty/server/internal/HttpConnection.java | 42 ++++++-- .../jetty/server/HttpConnectionTest.java | 97 +++++++++++++++++++ 2 files changed, 133 insertions(+), 6 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index 0f739d54333c..dfd839883195 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -502,18 +502,44 @@ void parseAndFillForContent() assert !_requestBuffer.hasRemaining(); + int filled; if (_requestBuffer.isRetained()) { - // The application has retained the content chunks, - // reacquire the buffer to avoid overwriting the content. - releaseRequestBuffer(); - ensureRequestBuffer(); + // The application has retained the content chunks, we must be careful to not overwrite content. + + // If there is space, we can top up the buffer + ByteBuffer backing = _requestBuffer.getByteBuffer(); + if (backing.limit() < backing.capacity() / 8) + { + int padding = backing.position(); + backing.position(0); + try + { + filled = doFillRequestBuffer(); + } + finally + { + backing.position(padding); + } + } + else + { + // otherwise reacquire the buffer and fill into the new buffer. + releaseRequestBuffer(); + ensureRequestBuffer(); + filled = fillRequestBuffer(); + } + } + else + { + filled = fillRequestBuffer(); } - int filled = fillRequestBuffer(); if (filled <= 0) { - releaseRequestBuffer(); + // Keep the buffer if it is retained + if (filled < 0 || !_requestBuffer.isRetained()) + releaseRequestBuffer(); break; } } @@ -523,7 +549,11 @@ private int fillRequestBuffer() { if (!isRequestBufferEmpty()) return _requestBuffer.remaining(); + return doFillRequestBuffer(); + } + private int doFillRequestBuffer() + { try { ByteBuffer requestBuffer = _requestBuffer.getByteBuffer(); diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java index a151e9990bea..b313f7722831 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java @@ -28,7 +28,9 @@ import java.util.EnumSet; import java.util.HashSet; import java.util.List; +import java.util.Queue; import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -70,6 +72,7 @@ import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; public class HttpConnectionTest @@ -1728,4 +1731,98 @@ public boolean handle(Request request, Response response, Callback callback) thr assertThat(actual.remove(0), is(expected)); } } + + @Test + public void testRetainedChunks() throws Exception + { + Queue chunks = new ConcurrentLinkedQueue<>(); + CountDownLatch blocked = new CountDownLatch(1); + + _server.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + while (true) + { + Content.Chunk chunk = request.read(); + if (chunk == null) + { + try + { + blocked.countDown(); + CountDownLatch blocker = new CountDownLatch(1); + request.demand(blocker::countDown); + blocker.await(); + } + catch (InterruptedException e) + { + // ignored + } + continue; + } + + chunks.add(chunk); + if (chunk.isLast()) + break; + } + callback.succeeded(); + return true; + } + }); + _server.start(); + + LocalConnector.LocalEndPoint localEndPoint = _connector.executeRequest(""" + POST / HTTP/1.1\r + Host: localhost\r + Transfer-Encoding: chunked\r + \r + 3;\r + one\r + 3;\r + two\r + 5;\r + """); + + // Wait for the server to block on the read(). + blocked.await(5, TimeUnit.SECONDS); + + // Send more content. + localEndPoint.addInput(""" + three\r + 4;\r + four\r + 4;\r + five\r + 3;\r + si"""); + + // Send more content. + localEndPoint.addInput(""" + x\r + 5;\r + seven\r + 5;\r + eight\r + 0;\r + \r + """); + + String rawResponse = localEndPoint.getResponse(); + // System.err.println(rawResponse); + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + assertEquals(response.getStatus(), HttpStatus.OK_200); + localEndPoint.close(); + + assertThat(chunks.size(), greaterThan(8)); + // chunks.forEach(System.err::println); + + // test all chunks are backed by the same buffer + Content.Chunk firstChunk = chunks.peek(); + assertNotNull(firstChunk); + String backing = firstChunk.toString().replaceFirst("WithRetainable.*ReservedBuffer", "ReservedBuffer").replaceFirst("\\[.*", ""); + for (Content.Chunk chunk : chunks) + if (chunk.hasRemaining()) + assertThat(chunk.toString(), containsString(backing)); + } } From af819749806939bb39b9720457fb2b218973283e Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 15 Oct 2024 21:32:56 +1100 Subject: [PATCH 27/61] Experiment to reuse buffer in HttpConnection to make retaining chunks more efficient --- .../jetty/server/internal/HttpConnection.java | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index dfd839883195..cbb2dab4e32b 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -358,12 +358,20 @@ public void onFillable() // Note that the endpoint might already be closed in some special circumstances. while (true) { - int filled = fillRequestBuffer(); - if (LOG.isDebugEnabled()) - LOG.debug("onFillable filled {} {} {} {}", filled, _httpChannel, _requestBuffer, this); + int filled; + if (isRequestBufferEmpty()) + { + filled = fillRequestBuffer(); + if (LOG.isDebugEnabled()) + LOG.debug("onFillable filled {} {} {} {}", filled, _httpChannel, _requestBuffer, this); - if (filled < 0 && getEndPoint().isOutputShutdown()) - close(); + if (filled < 0 && getEndPoint().isOutputShutdown()) + close(); + } + else + { + filled = 0; + } boolean handle = parseRequestBuffer(); @@ -503,19 +511,22 @@ void parseAndFillForContent() assert !_requestBuffer.hasRemaining(); int filled; + + // The application has retained the content chunks if (_requestBuffer.isRetained()) { - // The application has retained the content chunks, we must be careful to not overwrite content. + // then we must be careful to not overwrite content. // If there is space, we can top up the buffer ByteBuffer backing = _requestBuffer.getByteBuffer(); if (backing.limit() < backing.capacity() / 8) { + // pad the buffer so retained content is not overwritten int padding = backing.position(); backing.position(0); try { - filled = doFillRequestBuffer(); + filled = fillRequestBuffer(); } finally { @@ -546,13 +557,6 @@ void parseAndFillForContent() } private int fillRequestBuffer() - { - if (!isRequestBufferEmpty()) - return _requestBuffer.remaining(); - return doFillRequestBuffer(); - } - - private int doFillRequestBuffer() { try { From b48cfdaba0eda3113fae12dad41b815878d6d8ef Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 16 Oct 2024 18:02:41 +1100 Subject: [PATCH 28/61] fixed mimetype lookup --- .../src/main/java/org/eclipse/jetty/http/MimeTypes.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java index 119931fa2572..49cc1d535611 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java @@ -691,7 +691,12 @@ public static MimeTypes.Type getMimeTypeFromContentType(HttpField field) if (field instanceof MimeTypes.ContentTypeField contentTypeField) return contentTypeField.getMimeType(); - return MimeTypes.CACHE.get(field.getValue()); + String contentType = field.getValue(); + int semicolon = contentType.indexOf(';'); + if (semicolon >= 0) + contentType = contentType.substring(0, semicolon).trim(); + + return MimeTypes.CACHE.get(contentType); } /** From ba985438375f677422de16b834cb93bf0c0cb28a Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 22 Oct 2024 11:48:36 +1100 Subject: [PATCH 29/61] Delay content until 75% of an input buffer is read. --- .../jetty/io/RetainableByteBuffer.java | 4 + .../jetty/server/handler/DelayedHandler.java | 115 ++++++++++++++---- .../server/handler/DelayedHandlerTest.java | 72 ++++++++++- 3 files changed, 162 insertions(+), 29 deletions(-) diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java index f109cfe801c9..0726e4a5384d 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java @@ -2101,6 +2101,10 @@ private boolean shouldAggregate(RetainableByteBuffer buffer, long size) if (_minRetainSize == -1) { + // If it is a chunk, then retain + if (buffer instanceof Content.Chunk) + return false; + // If we are already aggregating and the size is small if (_aggregate != null && size < 128) return true; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java index fb86299d1b20..7f8dca0893bc 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -15,9 +15,10 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.Objects; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; @@ -29,6 +30,7 @@ import org.eclipse.jetty.io.Content; import org.eclipse.jetty.server.FormFields; import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.Callback; @@ -182,60 +184,121 @@ protected void process() protected abstract void delay() throws Exception; } - protected static class UntilContentDelayedProcess extends DelayedProcess + /** + * Delay dispatch until all content or 75% of an input buffer is received. + */ + protected static class UntilContentDelayedProcess extends DelayedProcess implements Runnable { + private final Deque _chunks = new ArrayDeque<>(); + private final int _maxBuffered; + private int _size; + public UntilContentDelayedProcess(Handler handler, Request request, Response response, Callback callback) { super(handler, request, response, callback); + _maxBuffered = 3 * request.getConnectionMetaData().getConnector().getConnectionFactory(HttpConnectionFactory.class).getInputBufferSize() / 4; } @Override protected void delay() { - Content.Chunk chunk = super.getRequest().read(); - if (chunk == null) - { - getRequest().demand(this::onContent); - } - else + read(false); + } + + protected void onContentAvailable() + { + read(true); + } + + protected void read(boolean execute) + { + while (true) { - RewindChunkRequest request = new RewindChunkRequest(getRequest(), chunk); - try + Content.Chunk chunk = super.getRequest().read(); + if (chunk == null) { - getHandler().handle(request, getResponse(), getCallback()); + getRequest().demand(this::onContentAvailable); + break; + } + + if (!_chunks.add(chunk)) + { + getCallback().failed(new IllegalStateException()); + break; } - catch (Throwable x) + + _size += chunk.remaining(); + + if (chunk.isLast() || _size >= _maxBuffered) { - // Use the wrapped request so that the error handling can - // consume the request content and release the already read chunk. - Response.writeError(request, getResponse(), getCallback(), x); + if (execute) + getRequest().getContext().execute(this); + else + run(); + break; } } } - public void onContent() + /** + * This is run when enough content has been received to dispatch to the next handler. + */ + public void run() { - // We must execute here, because demand callbacks are serialized and process may block on a demand callback - getRequest().getContext().execute(this::process); + RewindChunksRequest request = new RewindChunksRequest(getRequest(), getCallback(), _chunks); + try + { + if (!getHandler().handle(request, getResponse(), request)) + { + request.release(); + Response.writeError(getRequest(), getResponse(), getCallback(), HttpStatus.NOT_FOUND_404); + } + } + catch (Throwable t) + { + request.release(); + Response.writeError(getRequest(), getResponse(), getCallback(), t); + } } - private static class RewindChunkRequest extends Request.Wrapper + private static class RewindChunksRequest extends Request.Wrapper implements Callback { - private final AtomicReference _chunk; + private final Deque _chunks; + private final Callback _callback; - public RewindChunkRequest(Request wrapped, Content.Chunk chunk) + public RewindChunksRequest(Request wrapped, Callback callback, Deque chunks) { super(wrapped); - _chunk = new AtomicReference<>(chunk); + _chunks = chunks; + _callback = callback; } @Override public Content.Chunk read() { - Content.Chunk chunk = _chunk.getAndSet(null); - if (chunk != null) - return chunk; - return super.read(); + if (_chunks.isEmpty()) + return super.read(); + return _chunks.removeFirst(); + } + + private void release() + { + _chunks.forEach(Content.Chunk::release); + _chunks.clear(); + } + + @Override + public void fail(Throwable failure, boolean last) + { + release(); + _callback.failed(failure); + } + + @Override + public void succeeded() + { + release(); + _callback.succeeded(); } } } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java index 3d4c4ce68c1d..ee6ba22bd4b1 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java @@ -47,6 +47,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.sameInstance; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -189,7 +190,7 @@ public boolean handle(Request request, Response response, Callback callback) thr assertThat(stack, not(containsString("DemandContentCallback.succeeded"))); assertThat(stack, not(containsString("%s.%s".formatted( DelayedHandler.UntilContentDelayedProcess.class.getSimpleName(), - DelayedHandler.UntilContentDelayedProcess.class.getMethod("onContent").getName())))); + DelayedHandler.UntilContentDelayedProcess.class.getDeclaredMethod("onContentAvailable").getName())))); processing.countDown(); return super.handle(request, response, callback); @@ -246,7 +247,11 @@ public boolean handle(Request request, Response response, Callback callback) thr assertThat(stack, not(containsString("DemandContentCallback.succeeded"))); assertThat(stack, not(containsString("%s.%s".formatted( DelayedHandler.UntilContentDelayedProcess.class.getSimpleName(), - DelayedHandler.UntilContentDelayedProcess.class.getMethod("onContent").getName())))); + DelayedHandler.UntilContentDelayedProcess.class.getDeclaredMethod("onContentAvailable").getName())))); + + // Check content + String body = Content.Source.asString(request, StandardCharsets.ISO_8859_1); + assertThat(body, is("0123456789")); // Check the thread is in the context assertThat(ContextHandler.getCurrentContext(), sameInstance(context.getContext())); @@ -274,7 +279,12 @@ public boolean handle(Request request, Response response, Callback callback) thr assertFalse(processing.await(250, TimeUnit.MILLISECONDS)); - output.write("01234567\r\n".getBytes(StandardCharsets.UTF_8)); + output.write("0123456".getBytes(StandardCharsets.UTF_8)); + output.flush(); + + assertFalse(processing.await(250, TimeUnit.MILLISECONDS)); + + output.write("789".getBytes(StandardCharsets.UTF_8)); output.flush(); assertTrue(processing.await(10, TimeUnit.SECONDS)); @@ -337,6 +347,62 @@ public boolean handle(Request request, Response response, Callback callback) thr } } + @Test + public void testNoDelayWithChunkedContent() throws Exception + { + DelayedHandler delayedHandler = new DelayedHandler(); + + _server.setHandler(delayedHandler); + delayedHandler.setHandler(new HelloHandler() + { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Check that we are called directly from HttpConnection.onFillable + ByteArrayOutputStream out = new ByteArrayOutputStream(8192); + new Throwable().printStackTrace(new PrintStream(out)); + String stack = out.toString(StandardCharsets.ISO_8859_1); + assertThat(stack, containsString("org.eclipse.jetty.server.internal.HttpConnection.onFillable")); + assertThat(stack, containsString("org.eclipse.jetty.server.handler.DelayedHandler.handle")); + + // Check the content is available + String content = Content.Source.asString(request); + assertThat(content, equalTo("1234567890")); + + return super.handle(request, response, callback); + } + }); + _server.start(); + + try (Socket socket = new Socket("localhost", _connector.getLocalPort())) + { + String request = """ + POST / HTTP/1.1\r + Host: localhost\r + Transfer-Encoding: chunked\r + \r + 3;\r + 123\r + 4;\r + 4567\r + 3;\r + 890\r + 0;\r + \r + """; + OutputStream output = socket.getOutputStream(); + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + HttpTester.Input input = HttpTester.from(socket.getInputStream()); + HttpTester.Response response = HttpTester.parseResponse(input); + assertNotNull(response); + assertEquals(HttpStatus.OK_200, response.getStatus()); + String content = new String(response.getContentBytes(), StandardCharsets.UTF_8); + assertThat(content, containsString("Hello")); + } + } + @Test public void testDelayed404() throws Exception { From 472233b39e071bb84b887fa6fc78cef72ee768ea Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 22 Oct 2024 12:00:04 +1100 Subject: [PATCH 30/61] Delay content until 75% of an input buffer is read. --- .../config/modules/delay-until-content.mod | 6 ++--- .../jetty/server/handler/DelayedHandler.java | 25 ++++++++----------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod b/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod index ae8699bb26f6..75132432d889 100644 --- a/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod @@ -1,10 +1,10 @@ [description] Applies DelayedHandler to entire server. -Delays request handling until any body content has arrived, to minimize blocking. +Delays request handling until body content has arrived, to minimize blocking. For form data and multipart, the handling is delayed until the entire request body has -been asynchronously read. For all other content types, the delay is until the first byte -has arrived. +been asynchronously read. For all other content types, the delay is for a maximum of 75% +of an input buffer. [tags] server diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java index 7f8dca0893bc..2cc1bec21aa4 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -169,16 +169,24 @@ protected Callback getCallback() } protected void process() + { + process(getRequest(), getResponse(), getCallback()); + } + + protected boolean process(Request request, Response response, Callback callback) { try { - if (!getHandler().handle(getRequest(), getResponse(), getCallback())) - Response.writeError(getRequest(), getResponse(), getCallback(), HttpStatus.NOT_FOUND_404); + if (getHandler().handle(request, response, callback)) + return true; + + Response.writeError(getRequest(), getResponse(), getCallback(), HttpStatus.NOT_FOUND_404); } catch (Throwable t) { Response.writeError(getRequest(), getResponse(), getCallback(), t); } + return false; } protected abstract void delay() throws Exception; @@ -246,19 +254,8 @@ protected void read(boolean execute) public void run() { RewindChunksRequest request = new RewindChunksRequest(getRequest(), getCallback(), _chunks); - try - { - if (!getHandler().handle(request, getResponse(), request)) - { - request.release(); - Response.writeError(getRequest(), getResponse(), getCallback(), HttpStatus.NOT_FOUND_404); - } - } - catch (Throwable t) - { + if (!process(request, getResponse(), request)) request.release(); - Response.writeError(getRequest(), getResponse(), getCallback(), t); - } } private static class RewindChunksRequest extends Request.Wrapper implements Callback From 0dd3279e2ed8bccbd752fb7c24628445bb008f1a Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 23 Oct 2024 15:22:49 +1100 Subject: [PATCH 31/61] updates from review --- .../org/eclipse/jetty/io/RetainableByteBuffer.java | 4 ---- .../eclipse/jetty/server/handler/DelayedHandler.java | 11 ++++++----- .../eclipse/jetty/server/internal/HttpConnection.java | 11 ++++++----- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java index 0726e4a5384d..f109cfe801c9 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java @@ -2101,10 +2101,6 @@ private boolean shouldAggregate(RetainableByteBuffer buffer, long size) if (_minRetainSize == -1) { - // If it is a chunk, then retain - if (buffer instanceof Content.Chunk) - return false; - // If we are already aggregating and the size is small if (_aggregate != null && size < 128) return true; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java index 2cc1bec21aa4..0d00ed2c6942 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -198,13 +198,12 @@ protected boolean process(Request request, Response response, Callback callback) protected static class UntilContentDelayedProcess extends DelayedProcess implements Runnable { private final Deque _chunks = new ArrayDeque<>(); - private final int _maxBuffered; - private int _size; + private int _space; public UntilContentDelayedProcess(Handler handler, Request request, Response response, Callback callback) { super(handler, request, response, callback); - _maxBuffered = 3 * request.getConnectionMetaData().getConnector().getConnectionFactory(HttpConnectionFactory.class).getInputBufferSize() / 4; + _space = request.getConnectionMetaData().getConnector().getConnectionFactory(HttpConnectionFactory.class).getInputBufferSize(); } @Override @@ -229,15 +228,17 @@ protected void read(boolean execute) break; } + // retain the chunk in the queue if (!_chunks.add(chunk)) { getCallback().failed(new IllegalStateException()); break; } - _size += chunk.remaining(); + // reduce the buffer space by a guessed 8 byte framing overhead and the chunk size + _space -= 8 + chunk.remaining(); - if (chunk.isLast() || _size >= _maxBuffered) + if (chunk.isLast() || _space <= 0) { if (execute) getRequest().getContext().execute(this); diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index 35131a17f5fb..0a1414d94eb7 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -518,12 +518,12 @@ void parseAndFillForContent() { // then we must be careful to not overwrite content. - // If there is space, we can top up the buffer + // If there is more than 1K space available, we can top up the buffer rather than allocate a new one ByteBuffer backing = _requestBuffer.getByteBuffer(); - if (backing.limit() < backing.capacity() / 8) + int limit = backing.limit(); + if (backing.capacity() - limit < 1024) { - // pad the buffer so retained content is not overwritten - int padding = backing.position(); + // Move the position back to 0, leaving limit to cover the retained content and prevent it be overwritten backing.position(0); try { @@ -531,7 +531,8 @@ void parseAndFillForContent() } finally { - backing.position(padding); + // revert the position to the original limit to reconsume the retained content + backing.position(limit); } } else From 346adea2816615cf1b2cc7d69b828e51fd0c1574 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 23 Oct 2024 17:02:28 +1100 Subject: [PATCH 32/61] updates from review --- .../java/org/eclipse/jetty/server/internal/HttpConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index 0a1414d94eb7..2fcf2b3f830f 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -521,7 +521,7 @@ void parseAndFillForContent() // If there is more than 1K space available, we can top up the buffer rather than allocate a new one ByteBuffer backing = _requestBuffer.getByteBuffer(); int limit = backing.limit(); - if (backing.capacity() - limit < 1024) + if (backing.capacity() - limit >= 1024) { // Move the position back to 0, leaving limit to cover the retained content and prevent it be overwritten backing.position(0); From da8e70fe3d1fa9b001c9ccb019448349ceac24eb Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 23 Oct 2024 17:40:24 +1100 Subject: [PATCH 33/61] updates from review --- .../src/main/config/modules/delay-until-content.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod b/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod index 75132432d889..d06c59f4fa80 100644 --- a/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod @@ -3,8 +3,8 @@ Applies DelayedHandler to entire server. Delays request handling until body content has arrived, to minimize blocking. For form data and multipart, the handling is delayed until the entire request body has -been asynchronously read. For all other content types, the delay is for a maximum of 75% -of an input buffer. +been asynchronously read. For all other content types, the delay is for up to one +input buffer of content. [tags] server From a9b1578b0705a42486b4943b1d5d1b9decee5d0f Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 25 Oct 2024 10:37:56 +1100 Subject: [PATCH 34/61] updates from review --- .../java/org/eclipse/jetty/server/handler/DelayedHandler.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java index 0d00ed2c6942..2fefcc9290d6 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -180,12 +180,15 @@ protected boolean process(Request request, Response response, Callback callback) if (getHandler().handle(request, response, callback)) return true; + // The handle was rejected, so write the error using the original potentially unwrapped request/response/callback Response.writeError(getRequest(), getResponse(), getCallback(), HttpStatus.NOT_FOUND_404); } catch (Throwable t) { + // The handle failed, so write the error using the original potentially unwrapped request/response/callback Response.writeError(getRequest(), getResponse(), getCallback(), t); } + // return false to indicate the passed request/response/callback were not used. return false; } From 2fc4aa1780a6a6fcf542b3c80befa5dadda4bf0b Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 29 Oct 2024 17:14:56 +1100 Subject: [PATCH 35/61] configurable space --- .../jetty-server/src/main/config/etc/jetty.xml | 1 + .../src/main/config/modules/server.mod | 4 ++++ .../jetty/server/HttpConfiguration.java | 18 ++++++++++++++++++ .../jetty/server/internal/HttpConnection.java | 6 ++---- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty.xml b/jetty-core/jetty-server/src/main/config/etc/jetty.xml index 3796c3c29afb..8348155b7865 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty.xml @@ -70,6 +70,7 @@ + diff --git a/jetty-core/jetty-server/src/main/config/modules/server.mod b/jetty-core/jetty-server/src/main/config/modules/server.mod index 42166f77e001..1bcb7a071e06 100644 --- a/jetty-core/jetty-server/src/main/config/modules/server.mod +++ b/jetty-core/jetty-server/src/main/config/modules/server.mod @@ -71,6 +71,9 @@ etc/jetty.xml ## Whether to use direct ByteBuffers for reading or writing # jetty.httpConfig.useInputDirectByteBuffers=true # jetty.httpConfig.useOutputDirectByteBuffers=true + +## The minimum space available in a retained input buffer before allocating a new one. +# jetty.httpConfig.minInputBufferSpace=1024 # end::documentation-http-config[] # tag::documentation-server-compliance[] @@ -126,3 +129,4 @@ etc/jetty.xml ## Should the DefaultHandler show a list of known contexts in a root 404 response. # jetty.server.default.showContexts=true + diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java index e638065b9de7..97e8de89df7d 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java @@ -89,6 +89,7 @@ public class HttpConfiguration implements Dumpable private HostPort _serverAuthority; private SocketAddress _localAddress; private int _maxUnconsumedRequestContentReads = 16; + private int _minInputBufferSpace = 1024; /** *

An interface that allows a request object to be customized @@ -166,6 +167,7 @@ public HttpConfiguration(HttpConfiguration config) _serverAuthority = config._serverAuthority; _localAddress = config._localAddress; _maxUnconsumedRequestContentReads = config._maxUnconsumedRequestContentReads; + _minInputBufferSpace = config._minInputBufferSpace; } /** @@ -550,6 +552,22 @@ public void setMaxErrorDispatches(int max) _maxErrorDispatches = max; } + /** + * @return The minimum space available in a retained input buffer before allocating a new one. + */ + public int getMinInputBufferSpace() + { + return _minInputBufferSpace; + } + + /** + * @param minInputBufferSpace The minimum space available in a retained input buffer before allocating a new one. + */ + public void setMinInputBufferSpace(int minInputBufferSpace) + { + _minInputBufferSpace = minInputBufferSpace; + } + /** * @return The minimum request data rate in bytes per second; or <=0 for no limit */ diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index 50dced5fe681..d27b5c052fb5 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -513,15 +513,13 @@ void parseAndFillForContent() int filled; - // The application has retained the content chunks + // The application has retained the content chunks then we must not overwrite content. if (_requestBuffer.isRetained()) { - // then we must be careful to not overwrite content. - // If there is more than 1K space available, we can top up the buffer rather than allocate a new one ByteBuffer backing = _requestBuffer.getByteBuffer(); int limit = backing.limit(); - if (backing.capacity() - limit >= 1024) + if (backing.capacity() - limit >= getHttpConfiguration().getMinInputBufferSpace()) { // Move the position back to 0, leaving limit to cover the retained content and prevent it be overwritten backing.position(0); From f400278cf8c76aa4d230df7aff005ba23618a04d Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 30 Oct 2024 14:51:30 +1100 Subject: [PATCH 36/61] update javadoc and updates from review --- .../jetty/server/handler/DelayedHandler.java | 41 +++++++++++++++---- .../jetty/server/internal/HttpConnection.java | 24 +++++------ .../jetty/server/HttpConnectionTest.java | 11 ++--- 3 files changed, 50 insertions(+), 26 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java index 2fefcc9290d6..3e57085b9bc5 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -37,6 +37,15 @@ import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.StringUtil; +/** + * A {@link Handler.Wrapper} that can delay calling {@link Handler#handle(Request, Response, Callback)} on the + * {@link #getHandler() next Handler} until content is available, either entirely or in part. This handler is fully + * asynchronous and will not block waiting for content. Furthermore, for known content types, the content may be + * parsed into {@link FormFields} or {@link MultiPartFormData.Parts} prior to handling. + *

+ * This handler can allow a blocking application to run without blocking on input, as the content is asynchronously + * read before the application is called. + */ public class DelayedHandler extends Handler.Wrapper { public DelayedHandler() @@ -110,7 +119,7 @@ protected DelayedProcess newDelayedProcess(boolean contentExpected, String conte // if no known mimeType, then only delay until content if configured if (mimeType == null) - return delayDispatchUntilContent ? new UntilContentDelayedProcess(handler, request, response, callback) : null; + return delayDispatchUntilContent ? newUntilContentDelayedProcess(handler, request, response, callback) : null; // Otherwise, delay until a known content type is fully read; or if the type is not known then until the content is available return switch (mimeType) @@ -129,10 +138,15 @@ else if (getHandler().getServer().getAttribute(MultiPartConfig.class.getName()) yield new UntilMultipartDelayedProcess(handler, request, response, callback, contentType, config); } // if other mimeType, then only delay until content if configured - default -> delayDispatchUntilContent ? new UntilContentDelayedProcess(handler, request, response, callback) : null; + default -> delayDispatchUntilContent ? newUntilContentDelayedProcess(handler, request, response, callback) : null; }; } + protected DelayedProcess newUntilContentDelayedProcess(Handler handler, Request request, Response response, Callback callback) + { + return new UntilContentDelayedProcess(handler, request, response, callback, -1); + } + protected abstract static class DelayedProcess { private final Handler _handler; @@ -196,17 +210,26 @@ protected boolean process(Request request, Response response, Callback callback) } /** - * Delay dispatch until all content or 75% of an input buffer is received. + * Delay dispatch until all content or an effective buffer size is reached */ protected static class UntilContentDelayedProcess extends DelayedProcess implements Runnable { private final Deque _chunks = new ArrayDeque<>(); - private int _space; + private final int _maxSize; + private int _estimatedSize; - public UntilContentDelayedProcess(Handler handler, Request request, Response response, Callback callback) + /** + * @param handler The next handler + * @param request The delayed request + * @param response The delayed response + * @param callback The delayed callback + * @param maxSize The maximum size to buffer before dispatching to the next handler; + * or -1 to use {@link HttpConnectionFactory#getInputBufferSize()} + */ + public UntilContentDelayedProcess(Handler handler, Request request, Response response, Callback callback, int maxSize) { super(handler, request, response, callback); - _space = request.getConnectionMetaData().getConnector().getConnectionFactory(HttpConnectionFactory.class).getInputBufferSize(); + _maxSize = maxSize < 0 ? request.getConnectionMetaData().getConnector().getConnectionFactory(HttpConnectionFactory.class).getInputBufferSize() : maxSize; } @Override @@ -238,10 +261,10 @@ protected void read(boolean execute) break; } - // reduce the buffer space by a guessed 8 byte framing overhead and the chunk size - _space -= 8 + chunk.remaining(); + // Estimated size is 8 byte framing overhead per chunk plus the chunk size + _estimatedSize += 8 + chunk.remaining(); - if (chunk.isLast() || _space <= 0) + if (chunk.isLast() || _estimatedSize >= _maxSize) { if (execute) getRequest().getContext().execute(this); diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index d27b5c052fb5..a0679e03600f 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -104,10 +104,10 @@ public class HttpConnection extends AbstractMetaDataConnection implements Runnab private volatile RetainableByteBuffer _requestBuffer; private HttpFields.Mutable _trailers; private Runnable _onRequest; - private long _requests; - private long _responses; - private long _bytesIn; - private long _bytesOut; + private final AtomicLong _requests = new AtomicLong(); + private final AtomicLong _responses = new AtomicLong(); + private final AtomicLong _bytesIn = new AtomicLong(); + private final AtomicLong _bytesOut = new AtomicLong(); /** * Get the current connection that this thread is dispatched to. @@ -264,13 +264,13 @@ public void clearAttributes() @Override public long getMessagesIn() { - return _requests; + return _requests.get(); } @Override public long getMessagesOut() { - return _responses; + return _responses.get(); } public boolean isUseInputDirectByteBuffers() @@ -569,7 +569,7 @@ private int fillRequestBuffer() LOG.debug("filled {} {} {}", filled, _requestBuffer, this); if (filled > 0) - _bytesIn += filled; + _bytesIn.addAndGet(filled); else if (filled < 0) _parser.atEOF(); @@ -661,13 +661,13 @@ public void run() @Override public long getBytesIn() { - return _bytesIn; + return _bytesIn.get(); } @Override public long getBytesOut() { - return _bytesOut; + return _bytesOut.get(); } @Override @@ -834,7 +834,7 @@ public Action process() throws Exception gatherWrite += 1; bytes += _content.remaining(); } - HttpConnection.this._bytesOut += bytes; + _bytesOut.addAndGet(bytes); switch (gatherWrite) { case 7: @@ -1270,7 +1270,7 @@ public boolean is100ContinueExpected() }; Runnable handle = _httpChannel.onRequest(_request); - _requests++; + _requests.incrementAndGet(); Request request = _httpChannel.getRequest(); getHttpChannel().getComplianceViolationListener().onRequestBegin(request); @@ -1434,7 +1434,7 @@ else if (_generator.isCommitted()) } else { - _responses++; + _responses.incrementAndGet(); if (_expects100Continue) { if (response.getStatus() == HttpStatus.CONTINUE_100) diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java index a31e71280e9a..f5bae7c65013 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.server; import java.io.BufferedReader; +import java.io.IOException; import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -49,6 +50,7 @@ import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.server.handler.DumpHandler; import org.eclipse.jetty.server.internal.HttpConnection; +import org.eclipse.jetty.util.Blocker; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.NanoTime; @@ -1744,14 +1746,13 @@ public boolean handle(Request request, Response response, Callback callback) Content.Chunk chunk = request.read(); if (chunk == null) { - try + try (Blocker.Runnable blocker = Blocker.runnable()) { blocked.countDown(); - CountDownLatch blocker = new CountDownLatch(1); - request.demand(blocker::countDown); - blocker.await(); + request.demand(blocker); + blocker.block(); } - catch (InterruptedException e) + catch (IOException e) { // ignored } From 0543ae0dd5bbecceb4903e450eac1109774c160b Mon Sep 17 00:00:00 2001 From: gregw Date: Mon, 4 Nov 2024 13:55:07 +1100 Subject: [PATCH 37/61] Implement non-compact algorithm in HTTP and HTTP/2 --- .../eclipse/jetty/http2/HTTP2Connection.java | 32 ++++++++++--- .../jetty/server/internal/HttpConnection.java | 46 +++++++++---------- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java index be9a018dc0f9..a9507014d422 100644 --- a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java +++ b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java @@ -157,12 +157,20 @@ public void onFillable() produce(); } - private int fill(EndPoint endPoint, ByteBuffer buffer) + private int fill(EndPoint endPoint, ByteBuffer buffer, boolean compact) { + int padding = 0; try { if (endPoint.isInputShutdown()) return -1; + + if (!compact) + { + // Add padding content to avoid compaction + padding = buffer.limit(); + buffer.position(0); + } return endPoint.fill(buffer); } catch (IOException x) @@ -171,6 +179,11 @@ private int fill(EndPoint endPoint, ByteBuffer buffer) LOG.debug("Could not read from {}", endPoint, x); return -1; } + finally + { + if (!compact && padding > 0) + buffer.position(padding); + } } @Override @@ -335,6 +348,7 @@ public Runnable produce() while (true) { + boolean compact = true; if (parse) { while (networkBuffer.hasRemaining()) @@ -350,14 +364,20 @@ public Runnable produce() if (task != null) return task; - // If more references than 1 (ie not just us), don't refill into buffer and risk compaction. + // If the application has retained the content chunks then we must not overwrite content. if (networkBuffer.isRetained()) - reacquireNetworkBuffer(); + { + // If there is sufficient space available, we can top up the buffer rather than allocate a new one + if (BufferUtil.space(networkBuffer.getByteBuffer()) >= 1024) // TODO getHttpConfiguration().getMinInputBufferSpace() + // do not compact the buffer + compact = false; + else + // otherwise reacquire the buffer and fill into the new buffer. + reacquireNetworkBuffer(); + } } - // Here we know that this.networkBuffer is not retained by - // application code: either it has been released, or it's a new one. - int filled = fill(getEndPoint(), networkBuffer.getByteBuffer()); + int filled = fill(getEndPoint(), networkBuffer.getByteBuffer(), compact); if (LOG.isDebugEnabled()) LOG.debug("Filled {} bytes in {}", filled, networkBuffer); diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index 9e5230bcdc3d..443b3c82b76d 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -365,7 +365,7 @@ public void onFillable() int filled; if (isRequestBufferEmpty()) { - filled = fillRequestBuffer(); + filled = fillRequestBuffer(true); if (LOG.isDebugEnabled()) LOG.debug("onFillable filled {} {} {} {}", filled, _httpChannel, _requestBuffer, this); @@ -515,39 +515,27 @@ void parseAndFillForContent() assert !_requestBuffer.hasRemaining(); int filled; + boolean compact = true; - // The application has retained the content chunks then we must not overwrite content. + // If the application has retained the content chunks then we must not overwrite content. if (_requestBuffer.isRetained()) { - // If there is more than 1K space available, we can top up the buffer rather than allocate a new one + // If there is sufficient space available, we can top up the buffer rather than allocate a new one ByteBuffer backing = _requestBuffer.getByteBuffer(); - int limit = backing.limit(); - if (backing.capacity() - limit >= getHttpConfiguration().getMinInputBufferSpace()) + if (BufferUtil.space(backing) >= getHttpConfiguration().getMinInputBufferSpace()) { - // Move the position back to 0, leaving limit to cover the retained content and prevent it be overwritten - backing.position(0); - try - { - filled = fillRequestBuffer(); - } - finally - { - // revert the position to the original limit to reconsume the retained content - backing.position(limit); - } + // do not compact the buffer + compact = false; } else { // otherwise reacquire the buffer and fill into the new buffer. releaseRequestBuffer(); ensureRequestBuffer(); - filled = fillRequestBuffer(); } } - else - { - filled = fillRequestBuffer(); - } + + filled = fillRequestBuffer(compact); if (filled <= 0) { @@ -559,11 +547,18 @@ void parseAndFillForContent() } } - private int fillRequestBuffer() + private int fillRequestBuffer(boolean compact) { + int padding = 0; + ByteBuffer requestBuffer = _requestBuffer.getByteBuffer(); try { - ByteBuffer requestBuffer = _requestBuffer.getByteBuffer(); + if (!compact) + { + // Add padding content to avoid compaction + padding = requestBuffer.limit(); + requestBuffer.position(0); + } int filled = getEndPoint().fill(requestBuffer); if (filled == 0) // Do a retry on fill 0 (optimization for SSL connections) filled = getEndPoint().fill(requestBuffer); @@ -585,6 +580,11 @@ else if (filled < 0) _parser.atEOF(); return -1; } + finally + { + if (!compact && padding > 0) + requestBuffer.position(padding); + } } private boolean parseRequestBuffer() From 687a7bcca323dcc643ce6e9d545bfc2012c5c5ac Mon Sep 17 00:00:00 2001 From: gregw Date: Mon, 4 Nov 2024 14:13:53 +1100 Subject: [PATCH 38/61] Implement non-compact algorithm in HTTP and HTTP/2 --- .../http2/client/HTTP2ClientConnectionFactory.java | 2 +- .../java/org/eclipse/jetty/http2/HTTP2Connection.java | 10 ++++++++-- .../http2/server/internal/HTTP2ServerConnection.java | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java b/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java index dc2469e2d897..1c53f6096ad4 100644 --- a/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java +++ b/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java @@ -83,7 +83,7 @@ private static class HTTP2ClientConnection extends HTTP2Connection implements Ca private HTTP2ClientConnection(HTTP2Client client, EndPoint endpoint, HTTP2ClientSession session, Promise sessionPromise, Session.Listener listener) { - super(client.getByteBufferPool(), client.getExecutor(), endpoint, session, client.getInputBufferSize()); + super(client.getByteBufferPool(), client.getExecutor(), endpoint, session, client.getInputBufferSize(), -1); this.client = client; this.promise = sessionPromise; this.listener = listener; diff --git a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java index a9507014d422..6a51afb3c552 100644 --- a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java +++ b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java @@ -58,16 +58,23 @@ public class HTTP2Connection extends AbstractConnection implements Parser.Listen private final ByteBufferPool bufferPool; private final HTTP2Session session; private final int bufferSize; + private final int minBufferSpace; private final ExecutionStrategy strategy; private boolean useInputDirectByteBuffers; private boolean useOutputDirectByteBuffers; protected HTTP2Connection(ByteBufferPool bufferPool, Executor executor, EndPoint endPoint, HTTP2Session session, int bufferSize) + { + this(bufferPool, executor, endPoint, session, bufferSize, -1); + } + + protected HTTP2Connection(ByteBufferPool bufferPool, Executor executor, EndPoint endPoint, HTTP2Session session, int bufferSize, int minBufferSpace) { super(endPoint, executor); this.bufferPool = bufferPool; this.session = session; this.bufferSize = bufferSize; + this.minBufferSpace = minBufferSpace; this.strategy = new AdaptiveExecutionStrategy(producer, executor); LifeCycle.start(strategy); } @@ -368,8 +375,7 @@ public Runnable produce() if (networkBuffer.isRetained()) { // If there is sufficient space available, we can top up the buffer rather than allocate a new one - if (BufferUtil.space(networkBuffer.getByteBuffer()) >= 1024) // TODO getHttpConfiguration().getMinInputBufferSpace() - // do not compact the buffer + if (minBufferSpace > 0 && BufferUtil.space(networkBuffer.getByteBuffer()) >= minBufferSpace) // do not compact the buffer compact = false; else // otherwise reacquire the buffer and fill into the new buffer. diff --git a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HTTP2ServerConnection.java b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HTTP2ServerConnection.java index 7a2a9299636b..396f3e1712ad 100644 --- a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HTTP2ServerConnection.java +++ b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HTTP2ServerConnection.java @@ -76,7 +76,7 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection public HTTP2ServerConnection(Connector connector, EndPoint endPoint, HttpConfiguration httpConfig, HTTP2ServerSession session, int inputBufferSize, ServerSessionListener listener) { - super(connector.getByteBufferPool(), connector.getExecutor(), endPoint, session, inputBufferSize); + super(connector.getByteBufferPool(), connector.getExecutor(), endPoint, session, inputBufferSize, httpConfig.getMinInputBufferSpace()); this.connector = connector; this.listener = listener; this.httpConfig = httpConfig; From 1657145924f2fe18c97ae172ee28dac3401898e3 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 5 Nov 2024 14:35:25 +1100 Subject: [PATCH 39/61] fixed comment --- .../src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java index 6a51afb3c552..2c2772d93a21 100644 --- a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java +++ b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java @@ -375,7 +375,8 @@ public Runnable produce() if (networkBuffer.isRetained()) { // If there is sufficient space available, we can top up the buffer rather than allocate a new one - if (minBufferSpace > 0 && BufferUtil.space(networkBuffer.getByteBuffer()) >= minBufferSpace) // do not compact the buffer + if (minBufferSpace > 0 && BufferUtil.space(networkBuffer.getByteBuffer()) >= minBufferSpace) + // do not compact the buffer compact = false; else // otherwise reacquire the buffer and fill into the new buffer. From acfba417b9bdd54833b32d6da0abc86ec12e724d Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 6 Nov 2024 08:30:47 +1100 Subject: [PATCH 40/61] improved comments --- .../org/eclipse/jetty/server/handler/DelayedHandler.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java index 949018615c4b..9cc476517e4e 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -347,12 +347,13 @@ protected void delay() @Override public void failed(Throwable x) { - Response.writeError(getRequest(), getResponse(), getCallback(), x); + succeeded(null); } @Override public void succeeded(Fields result) { + // If the handling thread has already exited, we must process without blocking from this callback if (done.decrementAndGet() == 0) invocationType.runWithoutBlocking(this::doProcess, getRequest().getContext()); } @@ -369,6 +370,7 @@ public InvocationType getInvocationType() } }; + // If the fields are already available, we can process from this handling thread FormFields.onFields(getRequest(), _charset, onFields); if (done.decrementAndGet() == 0) process(); @@ -405,6 +407,7 @@ public void failed(Throwable x) @Override public void succeeded(MultiPartFormData.Parts result) { + // If the handling thread has already exited, we must process without blocking from this callback if (done.decrementAndGet() == 0) invocationType.runWithoutBlocking(this::doProcess, getRequest().getContext()); } @@ -422,6 +425,8 @@ public InvocationType getInvocationType() }; MultiPartFormData.onParts(request, request, _contentType, _config, onParts); + + // If the parts are already available, we can process from this handling thread if (done.decrementAndGet() == 0) process(); } From 1e838f7f75d2629047f8a8c9089d4c7f4be48030 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 6 Nov 2024 16:43:29 +1100 Subject: [PATCH 41/61] improved comments --- .../jetty/server/handler/DelayedHandler.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java index 9cc476517e4e..00424a82bffa 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -28,11 +28,14 @@ import org.eclipse.jetty.http.MultiPartConfig; import org.eclipse.jetty.http.MultiPartFormData; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.RetainableByteBuffer; import org.eclipse.jetty.server.FormFields; import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.Promise; @@ -42,10 +45,23 @@ * A {@link Handler.Wrapper} that can delay calling {@link Handler#handle(Request, Response, Callback)} on the * {@link #getHandler() next Handler} until content is available, either entirely or in part. This handler is fully * asynchronous and will not block waiting for content. Furthermore, for known content types, the content may be - * parsed into {@link FormFields} or {@link MultiPartFormData.Parts} prior to handling. - *

- * This handler can allow a blocking application to run without blocking on input, as the content is asynchronously - * read before the application is called. + * parsed into {@link FormFields} or {@link MultiPartFormData.Parts} prior to handling. Thus, this handler can allow a + * blocking application to run without blocking on input, as the content is asynchronously read before the application + * is called. + *

+ *

To delay for {@link FormFields}, the request content must be {@link org.eclipse.jetty.http.MimeTypes.Type#FORM_ENCODED}. + * Once read by this handler, the fields are available via {@link FormFields#getFields(Request)}. + *

+ *

To delay for {@link MultiPartFormData} content, a {@link org.eclipse.jetty.http.MultiPartConfig} instance must be set as + * a {@link org.eclipse.jetty.server.Context} or {@link org.eclipse.jetty.server.Server} attribute with the class name + * as they attribute name. Once read by this handler, the parts are available via + * {@link MultiPartFormData#getParts(Attributes)}, passing in the {@link Request} as the {@link Attributes} instance. + *

+ *

To delay for arbitrary content, the {@link HttpConfiguration#isDelayDispatchUntilContent()} configuration must + * be {@code true} and up to {@link HttpConfiguration#getInputBufferSize()} of data may be + * {@link RetainableByteBuffer#retain() retained}. Once read, the data is made available via the standard + * {@link Request#read()} API. + *

*/ public class DelayedHandler extends Handler.Wrapper { From a23c8f41d3b0287a4fe5b145082aa8383873ea11 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 7 Nov 2024 14:06:33 +1100 Subject: [PATCH 42/61] updates from review --- .../client/HTTP2ClientConnectionFactory.java | 2 +- .../eclipse/jetty/http2/HTTP2Connection.java | 34 ++-- .../eclipse/jetty/io/ArrayByteBufferPool.java | 10 ++ .../src/main/config/etc/jetty-delayed.xml | 1 + .../config/modules/delay-until-content.mod | 9 +- .../jetty/server/HttpConfiguration.java | 9 +- .../jetty/server/handler/DelayedHandler.java | 64 +++++--- .../jetty/server/internal/HttpConnection.java | 10 +- .../server/handler/DelayedHandlerTest.java | 4 +- .../transport/ServerRetainContentTest.java | 147 ++++++++++++++++++ .../ee10/test/HttpInputIntegrationTest.java | 31 +++- 11 files changed, 265 insertions(+), 56 deletions(-) create mode 100644 jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerRetainContentTest.java diff --git a/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java b/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java index 1c53f6096ad4..920f69d3bc2a 100644 --- a/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java +++ b/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java @@ -83,7 +83,7 @@ private static class HTTP2ClientConnection extends HTTP2Connection implements Ca private HTTP2ClientConnection(HTTP2Client client, EndPoint endpoint, HTTP2ClientSession session, Promise sessionPromise, Session.Listener listener) { - super(client.getByteBufferPool(), client.getExecutor(), endpoint, session, client.getInputBufferSize(), -1); + super(client.getByteBufferPool(), client.getExecutor(), endpoint, session, client.getInputBufferSize(), 0); this.client = client; this.promise = sessionPromise; this.listener = listener; diff --git a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java index 2c2772d93a21..a8ceda87191a 100644 --- a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java +++ b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java @@ -74,7 +74,7 @@ protected HTTP2Connection(ByteBufferPool bufferPool, Executor executor, EndPoint this.bufferPool = bufferPool; this.session = session; this.bufferSize = bufferSize; - this.minBufferSpace = minBufferSpace; + this.minBufferSpace = minBufferSpace < 0 ? Math.min(1024, bufferSize) : minBufferSpace; this.strategy = new AdaptiveExecutionStrategy(producer, executor); LifeCycle.start(strategy); } @@ -349,6 +349,7 @@ public Runnable produce() boolean interested = false; acquireNetworkBuffer(); + int filled = 0; try { boolean parse = networkBuffer.hasRemaining(); @@ -370,23 +371,23 @@ public Runnable produce() LOG.debug("Dequeued new task {}", task); if (task != null) return task; + } - // If the application has retained the content chunks then we must not overwrite content. - if (networkBuffer.isRetained()) - { - // If there is sufficient space available, we can top up the buffer rather than allocate a new one - if (minBufferSpace > 0 && BufferUtil.space(networkBuffer.getByteBuffer()) >= minBufferSpace) - // do not compact the buffer - compact = false; - else - // otherwise reacquire the buffer and fill into the new buffer. - reacquireNetworkBuffer(); - } + // If the application has retained the content chunks then we must not overwrite content. + if (networkBuffer.isRetained()) + { + // If there is sufficient space available, we can top up the buffer rather than allocate a new one + if (minBufferSpace > 0 && BufferUtil.space(networkBuffer.getByteBuffer()) >= minBufferSpace) + // do not compact the buffer + compact = false; + else + // otherwise reacquire the buffer and fill into the new buffer. + reacquireNetworkBuffer(); } - int filled = fill(getEndPoint(), networkBuffer.getByteBuffer(), compact); + filled = fill(getEndPoint(), networkBuffer.getByteBuffer(), compact); if (LOG.isDebugEnabled()) - LOG.debug("Filled {} bytes in {}", filled, networkBuffer); + LOG.debug("Filled {} bytes compacted {} in {}", filled, compact, networkBuffer); if (filled > 0) { @@ -408,7 +409,8 @@ else if (filled == 0) } finally { - releaseNetworkBuffer(); + if (filled < 0 || !networkBuffer.isRetained() || shutdown) + releaseNetworkBuffer(); if (interested) getEndPoint().fillInterested(fillableCallback); } @@ -443,7 +445,7 @@ private void releaseNetworkBuffer() { RetainableByteBuffer.Mutable currentBuffer = networkBuffer; if (currentBuffer == null) - throw new IllegalStateException(); + return; if (currentBuffer.hasRemaining() && !shutdown && !failed) throw new IllegalStateException(); diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayByteBufferPool.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayByteBufferPool.java index 38f168180061..1f1d5fbd4ba6 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayByteBufferPool.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayByteBufferPool.java @@ -26,6 +26,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.LongAdder; import java.util.function.IntUnaryOperator; import java.util.stream.Collectors; @@ -66,6 +67,7 @@ public class ArrayByteBufferPool implements ByteBufferPool, Dumpable private final long _maxDirectMemory; private final IntUnaryOperator _bucketIndexFor; private final AtomicBoolean _evictor = new AtomicBoolean(false); + private final AtomicLong _reserved = new AtomicLong(); private boolean _statisticsEnabled; /** @@ -175,6 +177,12 @@ private long maxMemory(long maxMemory) return maxMemory; } + @ManagedAttribute("The current number of allocated bytes reserved to be added to the pool once released") + public long getReserved() + { + return _reserved.get(); + } + @ManagedAttribute("Whether statistics are enabled") public boolean isStatisticsEnabled() { @@ -214,6 +222,7 @@ public RetainableByteBuffer.Mutable acquire(int size, boolean direct) if (entry == null) { ByteBuffer buffer = BufferUtil.allocate(bucket.getCapacity(), direct); + _reserved.addAndGet(buffer.capacity()); return new ReservedBuffer(buffer, bucket); } @@ -249,6 +258,7 @@ public boolean releaseAndRemove(RetainableByteBuffer buffer) private void reserve(RetainedBucket bucket, ByteBuffer byteBuffer) { + _reserved.addAndGet(-byteBuffer.capacity()); bucket.recordRelease(); // Try to reserve an entry to put the buffer into the pool. diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-delayed.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-delayed.xml index d1f1fc41ded9..6db65645493e 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-delayed.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-delayed.xml @@ -9,6 +9,7 @@ + diff --git a/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod b/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod index d06c59f4fa80..b4e9663f669f 100644 --- a/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod @@ -3,8 +3,8 @@ Applies DelayedHandler to entire server. Delays request handling until body content has arrived, to minimize blocking. For form data and multipart, the handling is delayed until the entire request body has -been asynchronously read. For all other content types, the delay is for up to one -input buffer of content. +been asynchronously read. For all other content types, the delay is for up to a limited size, +which by default is one input buffer. [tags] server @@ -18,3 +18,8 @@ threadlimit [xml] etc/jetty-delayed.xml + +[ini-template] +#tag::documentation[] +## The maximum bytes to retain whilst delaying content; or 0 for no delay; or -1 (default) for a default value. +# jetty.delayed.maxRetainedContent=-1 diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java index f9cd8f4c590f..7d7c1041559b 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java @@ -355,13 +355,15 @@ public boolean getSendDateHeader() /** * Set if true, delays the application dispatch until content is available (defaults to true). * @param delay if true, delays the application dispatch until content is available (defaults to true) + * @deprecated Use the DelayedHandler instead. */ + @Deprecated (forRemoval = true, since = "12.1.0") public void setDelayDispatchUntilContent(boolean delay) { _delayDispatchUntilContent = delay; } - @ManagedAttribute("Whether to delay the application dispatch until content is available") + @Deprecated (forRemoval = true, since = "12.1.0") public boolean isDelayDispatchUntilContent() { return _delayDispatchUntilContent; @@ -572,13 +574,16 @@ public void setMaxErrorDispatches(int max) /** * @return The minimum space available in a retained input buffer before allocating a new one. */ + @ManagedAttribute("The minimum space available in a retained input buffer before allocating a new one") public int getMinInputBufferSpace() { return _minInputBufferSpace; } /** - * @param minInputBufferSpace The minimum space available in a retained input buffer before allocating a new one. + * @param minInputBufferSpace The minimum space available in a retained input buffer before allocating a new one; + * 0 to always allocate a new buffer; + * -1 for a default value */ public void setMinInputBufferSpace(int minInputBufferSpace) { diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java index 00424a82bffa..1f29ca0bfea5 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -31,7 +31,6 @@ import org.eclipse.jetty.io.RetainableByteBuffer; import org.eclipse.jetty.server.FormFields; import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; @@ -40,9 +39,10 @@ import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.thread.Invocable; /** - * A {@link Handler.Wrapper} that can delay calling {@link Handler#handle(Request, Response, Callback)} on the + *

A {@link Handler.Wrapper} that can delay calling {@link Handler#handle(Request, Response, Callback)} on the * {@link #getHandler() next Handler} until content is available, either entirely or in part. This handler is fully * asynchronous and will not block waiting for content. Furthermore, for known content types, the content may be * parsed into {@link FormFields} or {@link MultiPartFormData.Parts} prior to handling. Thus, this handler can allow a @@ -57,14 +57,14 @@ * as they attribute name. Once read by this handler, the parts are available via * {@link MultiPartFormData#getParts(Attributes)}, passing in the {@link Request} as the {@link Attributes} instance. *

- *

To delay for arbitrary content, the {@link HttpConfiguration#isDelayDispatchUntilContent()} configuration must - * be {@code true} and up to {@link HttpConfiguration#getInputBufferSize()} of data may be - * {@link RetainableByteBuffer#retain() retained}. Once read, the data is made available via the standard - * {@link Request#read()} API. + *

To delay for arbitrary content, the {@link #setMaxRetainedContent(int)} configuration must + * be non zero. Once read, the data is made available via the standard {@link Request#read()} API. *

*/ public class DelayedHandler extends Handler.Wrapper { + private int _maxRetainedContent = -1; + public DelayedHandler() { this(null); @@ -75,6 +75,21 @@ public DelayedHandler(Handler handler) super(handler); } + public int getMaxRetainedContent() + { + return _maxRetainedContent; + } + + /** + * @param maxRetainedContent The maximum bytes to {@link RetainableByteBuffer#retain() retain} whilst delaying content; + * or 0 to never delay for content; + * or -1 (default) for a heuristic value. + */ + public void setMaxRetainedContent(int maxRetainedContent) + { + _maxRetainedContent = maxRetainedContent; + } + @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { @@ -131,12 +146,9 @@ protected DelayedProcess newDelayedProcess(boolean contentExpected, String conte if (!contentExpected) return null; - // are we configured to delay dispatch until content? - boolean delayDispatchUntilContent = request.getConnectionMetaData().getHttpConfiguration().isDelayDispatchUntilContent(); - // if no known mimeType, then only delay until content if configured if (mimeType == null) - return delayDispatchUntilContent ? newUntilContentDelayedProcess(handler, request, response, callback) : null; + return _maxRetainedContent != 0 ? new UntilContentDelayedProcess(handler, request, response, callback, _maxRetainedContent) : null; // Otherwise, delay until a known content type is fully read; or if the type is not known then until the content is available return switch (mimeType) @@ -151,16 +163,12 @@ protected DelayedProcess newDelayedProcess(boolean contentExpected, String conte yield null; } // if other mimeType, then only delay until content if configured - default -> delayDispatchUntilContent ? newUntilContentDelayedProcess(handler, request, response, callback) : null; + default -> + _maxRetainedContent != 0 ? new UntilContentDelayedProcess(handler, request, response, callback, _maxRetainedContent) : null; }; } - protected DelayedProcess newUntilContentDelayedProcess(Handler handler, Request request, Response response, Callback callback) - { - return new UntilContentDelayedProcess(handler, request, response, callback, -1); - } - protected abstract static class DelayedProcess { private final Handler _handler; @@ -226,7 +234,7 @@ protected boolean process(Request request, Response response, Callback callback) /** * Delay dispatch until all content or an effective buffer size is reached */ - protected static class UntilContentDelayedProcess extends DelayedProcess implements Runnable + protected static class UntilContentDelayedProcess extends DelayedProcess implements Invocable.Task { private final Deque _chunks = new ArrayDeque<>(); private final int _maxSize; @@ -252,11 +260,6 @@ protected void delay() read(false); } - protected void onContentAvailable() - { - read(true); - } - protected void read(boolean execute) { while (true) @@ -264,7 +267,7 @@ protected void read(boolean execute) Content.Chunk chunk = super.getRequest().read(); if (chunk == null) { - getRequest().demand(org.eclipse.jetty.util.thread.Invocable.from(InvocationType.NON_BLOCKING, this::onContentAvailable)); + getRequest().demand(this); break; } @@ -281,18 +284,29 @@ protected void read(boolean execute) if (chunk.isLast() || _estimatedSize >= _maxSize) { if (execute) - getRequest().getContext().execute(this); + getRequest().getContext().execute(this::doProcess); else - run(); + doProcess(); break; } } } + @Override + public InvocationType getInvocationType() + { + return InvocationType.NON_BLOCKING; + } + /** * This is run when enough content has been received to dispatch to the next handler. */ public void run() + { + read(true); + } + + private void doProcess() { RewindChunksRequest request = new RewindChunksRequest(getRequest(), getCallback(), _chunks); if (!process(request, getResponse(), request)) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index 9431f1ba0927..4180cf26cb21 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -102,6 +102,7 @@ public class HttpConnection extends AbstractMetaDataConnection implements Runnab private final SendCallback _sendCallback = new SendCallback(); private final AtomicBoolean _handling = new AtomicBoolean(false); private final HttpFields.Mutable _headerBuilder = HttpFields.build(); + private final int _minBufferSpace; private volatile RetainableByteBuffer _requestBuffer; private HttpFields.Mutable _trailers; private Runnable _onRequest; @@ -139,6 +140,8 @@ public HttpConnection(HttpConfiguration configuration, Connector connector, EndP _httpChannel = newHttpChannel(connector.getServer(), configuration); _requestHandler = newRequestHandler(); _parser = newHttpParser(configuration.getHttpCompliance()); + _minBufferSpace = configuration.getMinInputBufferSpace() < 0 ? Math.min(1024, configuration.getInputBufferSize()) : configuration.getMinInputBufferSpace(); + if (LOG.isDebugEnabled()) LOG.debug("New HTTP Connection {}", this); } @@ -327,9 +330,10 @@ void releaseRequestBuffer() { if (LOG.isDebugEnabled()) LOG.debug("releasing request buffer {} {}", _requestBuffer, this); - if (_requestBuffer != null) - _requestBuffer.release(); + RetainableByteBuffer buffer = _requestBuffer; _requestBuffer = null; + if (buffer != null) + buffer.release(); } private void ensureRequestBuffer() @@ -522,7 +526,7 @@ void parseAndFillForContent() { // If there is sufficient space available, we can top up the buffer rather than allocate a new one ByteBuffer backing = _requestBuffer.getByteBuffer(); - if (BufferUtil.space(backing) >= getHttpConfiguration().getMinInputBufferSpace()) + if (_minBufferSpace > 0 && BufferUtil.space(backing) >= _minBufferSpace) { // do not compact the buffer compact = false; diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java index 1996835cf669..14946293daf6 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java @@ -189,7 +189,7 @@ public boolean handle(Request request, Response response, Callback callback) thr assertThat(stack, not(containsString("DemandContentCallback.succeeded"))); assertThat(stack, not(containsString("%s.%s".formatted( DelayedHandler.UntilContentDelayedProcess.class.getSimpleName(), - DelayedHandler.UntilContentDelayedProcess.class.getDeclaredMethod("onContentAvailable").getName())))); + DelayedHandler.UntilContentDelayedProcess.class.getDeclaredMethod("run").getName())))); processing.countDown(); return super.handle(request, response, callback); @@ -246,7 +246,7 @@ public boolean handle(Request request, Response response, Callback callback) thr assertThat(stack, not(containsString("DemandContentCallback.succeeded"))); assertThat(stack, not(containsString("%s.%s".formatted( DelayedHandler.UntilContentDelayedProcess.class.getSimpleName(), - DelayedHandler.UntilContentDelayedProcess.class.getDeclaredMethod("onContentAvailable").getName())))); + DelayedHandler.UntilContentDelayedProcess.class.getDeclaredMethod("run").getName())))); // Check content String body = Content.Source.asString(request, StandardCharsets.ISO_8859_1); diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerRetainContentTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerRetainContentTest.java new file mode 100644 index 000000000000..7849a7542842 --- /dev/null +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerRetainContentTest.java @@ -0,0 +1,147 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.test.client.transport; + +import java.io.IOException; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.AsyncRequestContent; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.ArrayByteBufferPool; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Blocker; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +public class ServerRetainContentTest extends AbstractTest +{ + + @Override + protected void prepareServer(Transport transport, Handler handler) throws Exception + { + super.prepareServer(transport, handler); + } + + @ParameterizedTest + @MethodSource("transports") + public void testRetainPOST(Transport transport) throws Exception + { + assumeTrue(transport != Transport.FCGI); + assumeTrue(transport != Transport.H3); + + Queue chunks = new ConcurrentLinkedQueue<>(); + CountDownLatch blocked = new CountDownLatch(1); + + start(transport, new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + while (true) + { + Content.Chunk chunk = request.read(); + if (chunk == null) + { + try (Blocker.Runnable blocker = Blocker.runnable()) + { + blocked.countDown(); + request.demand(blocker); + blocker.block(); + } + catch (IOException e) + { + // ignored + } + continue; + } + + chunks.add(chunk); + if (chunk.isLast()) + break; + } + callback.succeeded(); + return true; + } + }); + AsyncRequestContent content = new AsyncRequestContent(); + + Callback.Completable one = new Callback.Completable(); + content.write(false, BufferUtil.toBuffer("1"), one); + + ArrayByteBufferPool byteBufferPool = (ArrayByteBufferPool)server.getByteBufferPool(); + + long baseMemory = byteBufferPool.getDirectMemory() + byteBufferPool.getHeapMemory() + byteBufferPool.getReserved(); + + CountDownLatch latch = new CountDownLatch(1); + client.newRequest(newURI(transport)) + .method("POST") + .body(content) + .send(result -> + { + assertThat(result.getResponse().getStatus(), is(HttpStatus.OK_200)); + latch.countDown(); + }); + + Callback.Completable two = new Callback.Completable(); + content.write(false, BufferUtil.toBuffer("2"), two); + content.flush(); + + assertTrue(blocked.await(5, TimeUnit.SECONDS)); + one.get(5, TimeUnit.SECONDS); + two.get(5, TimeUnit.SECONDS); + + final int CHUNKS = 1000; + for (int i = 3; i < CHUNKS; i++) + { + Callback.Completable complete = new Callback.Completable(); + content.write(false, BufferUtil.toBuffer(Integer.toString(i)), complete); + content.flush(); + complete.get(5, TimeUnit.SECONDS); + } + + Callback.Completable end = new Callback.Completable(); + content.write(true, BufferUtil.toBuffer("x"), end); + content.close(); + end.get(5, TimeUnit.SECONDS); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + long finalMemory = byteBufferPool.getDirectMemory() + byteBufferPool.getHeapMemory() + byteBufferPool.getReserved(); + + long totalData = 0; + for (Content.Chunk chunk : chunks) + { + chunk.release(); + if (chunk.hasRemaining()) + totalData += chunk.remaining(); + } + + assertThat(finalMemory - baseMemory, lessThanOrEqualTo((transport.isSecure() ? 100 : 32) * 1024L)); + + client.close(); + } +} diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java index a26a99ffac7d..770c53c9cad0 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import javax.net.ssl.SSLSocket; @@ -48,11 +49,15 @@ import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.LocalConnector.LocalEndPoint; import org.eclipse.jetty.server.NetworkConnector; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.handler.DelayedHandler; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -82,6 +87,7 @@ enum Mode private static HttpConfiguration __config; private static SslContextFactory.Server __sslContextFactory; private static ArrayByteBufferPool.Tracking __bufferPool; + private static final AtomicBoolean __delayHandler = new AtomicBoolean(); @BeforeAll public static void beforeClass() throws Exception @@ -127,8 +133,20 @@ public static void beforeClass() throws Exception http2.setIdleTimeout(5000); __server.addConnector(http2); + DelayedHandler delayedHandler = new DelayedHandler() + { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + if (__delayHandler.get()) + return super.handle(request, response, callback); + return getHandler().handle(request, response, callback); + } + }; + ServletContextHandler context = new ServletContextHandler("/ctx"); - __server.setHandler(context); + __server.setHandler(delayedHandler); + delayedHandler.setHandler(context); ServletHolder holder = new ServletHolder(new TestServlet()); holder.setAsyncSupported(true); context.addServlet(holder, "/*"); @@ -286,6 +304,7 @@ private static void runMode(Mode mode, ServletContextRequest request, Runnable t @MethodSource("scenarios") public void testOne(Scenario scenario) throws Exception { + __delayHandler.set(scenario._delayHandler); TestClient client = scenario._client.getDeclaredConstructor().newInstance(); String response = client.send("/ctx/test?mode=" + scenario._mode, 50, scenario._delay, scenario._length, scenario._send); @@ -309,6 +328,7 @@ public void testOne(Scenario scenario) throws Exception @MethodSource("scenarios") public void testStress(Scenario scenario) throws Exception { + __delayHandler.set(scenario._delayHandler); int sum = 0; for (String s : scenario._send) { @@ -664,17 +684,18 @@ public static class Scenario { private final Class _client; private final Mode _mode; + private final boolean _delayHandler; private final Boolean _delay; private final int _status; private final int _read; private final int _length; private final List _send; - public Scenario(Class client, Mode mode, boolean dispatch, Boolean delay, int status, int read, int length, String... send) + public Scenario(Class client, Mode mode, boolean delayHandler, Boolean delay, int status, int read, int length, String... send) { _client = client; _mode = mode; - __config.setDelayDispatchUntilContent(dispatch); + _delayHandler = delayHandler; _delay = delay; _status = status; _read = read; @@ -685,8 +706,8 @@ public Scenario(Class client, Mode mode, boolean dispatch, @Override public String toString() { - return String.format("c=%s, m=%s, delayDispatch=%b delayInFrame=%s content-length:%d expect=%d read=%d content:%s%n", - _client.getSimpleName(), _mode, __config.isDelayDispatchUntilContent(), _delay, _length, _status, _read, _send); + return String.format("c=%s, m=%s, delayInFrame=%s content-length:%d expect=%d read=%d content:%s%n", + _client.getSimpleName(), _mode, _delay, _length, _status, _read, _send); } } } From 1f89ff55ebd68c9627f319ae9b14be4879a23ba3 Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 8 Nov 2024 08:53:02 +1100 Subject: [PATCH 43/61] Fix test leaks --- .../eclipse/jetty/http2/HTTP2Connection.java | 96 ++++++++++++------- .../jetty/http2/tests/AsyncIOTest.java | 3 + .../eclipse/jetty/http2/tests/HTTP2Test.java | 3 + .../HttpClientTransportOverHTTP2Test.java | 3 + .../jetty/http2/tests/StreamResetTest.java | 3 + 5 files changed, 72 insertions(+), 36 deletions(-) diff --git a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java index a8ceda87191a..dce96fb6da33 100644 --- a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java +++ b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java @@ -20,6 +20,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.http2.api.Stream; import org.eclipse.jetty.http2.frames.DataFrame; @@ -154,6 +155,7 @@ public void onClose(Throwable cause) super.onClose(cause); LifeCycle.stop(strategy); + producer.stop(); } @Override @@ -323,16 +325,19 @@ public void onConnectionFailure(int error, String reason) protected class HTTP2Producer implements ExecutionStrategy.Producer { + private static final RetainableByteBuffer.Mutable STOPPED = new RetainableByteBuffer.NonRetainableByteBuffer(BufferUtil.EMPTY_BUFFER); private final Callback fillableCallback = new FillableCallback(); + private final AtomicReference savedBuffer = new AtomicReference<>(); private RetainableByteBuffer.Mutable networkBuffer; private boolean shutdown; private boolean failed; private void setInputBuffer(ByteBuffer byteBuffer) { - acquireNetworkBuffer(); + RetainableByteBuffer.Mutable networkBuffer = acquireBuffer(); if (!networkBuffer.append(byteBuffer)) LOG.warn("overflow"); + saveBuffer(networkBuffer); } @Override @@ -348,8 +353,8 @@ public Runnable produce() return null; boolean interested = false; - acquireNetworkBuffer(); int filled = 0; + networkBuffer = acquireBuffer(); try { boolean parse = networkBuffer.hasRemaining(); @@ -378,11 +383,18 @@ public Runnable produce() { // If there is sufficient space available, we can top up the buffer rather than allocate a new one if (minBufferSpace > 0 && BufferUtil.space(networkBuffer.getByteBuffer()) >= minBufferSpace) + { // do not compact the buffer compact = false; + } else + { // otherwise reacquire the buffer and fill into the new buffer. - reacquireNetworkBuffer(); + if (LOG.isDebugEnabled()) + LOG.debug("Released retained {}", networkBuffer); + networkBuffer.release(); + networkBuffer = acquireBuffer(); + } } filled = fill(getEndPoint(), networkBuffer.getByteBuffer(), compact); @@ -409,51 +421,63 @@ else if (filled == 0) } finally { - if (filled < 0 || !networkBuffer.isRetained() || shutdown) - releaseNetworkBuffer(); + if (networkBuffer.isRetained() && !shutdown) + { + saveBuffer(networkBuffer); + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("Released after process {}", networkBuffer); + networkBuffer.release(); + } + networkBuffer = null; if (interested) getEndPoint().fillInterested(fillableCallback); } } - private void acquireNetworkBuffer() + private RetainableByteBuffer.Mutable acquireBuffer() { - if (networkBuffer == null) - { - networkBuffer = bufferPool.acquire(bufferSize, isUseInputDirectByteBuffers()).asMutable(); - if (LOG.isDebugEnabled()) - LOG.debug("Acquired {}", networkBuffer); - } + RetainableByteBuffer.Mutable buffer = savedBuffer.getAndSet(null); + if (buffer == null) + buffer = bufferPool.acquire(bufferSize, isUseInputDirectByteBuffers()).asMutable(); + if (LOG.isDebugEnabled()) + LOG.debug("Acquired {}", buffer); + return buffer; } - private void reacquireNetworkBuffer() + private void saveBuffer(RetainableByteBuffer.Mutable buffer) { - RetainableByteBuffer.Mutable currentBuffer = networkBuffer; - if (currentBuffer == null) - throw new IllegalStateException(); - - if (currentBuffer.hasRemaining()) - throw new IllegalStateException(); - - currentBuffer.release(); - networkBuffer = bufferPool.acquire(bufferSize, isUseInputDirectByteBuffers()); - if (LOG.isDebugEnabled()) - LOG.debug("Reacquired {}<-{}", currentBuffer, networkBuffer); + if (savedBuffer.compareAndSet(null, buffer)) + { + if (LOG.isDebugEnabled()) + LOG.debug("Saved {}", buffer); + } + else + { + if (savedBuffer.get() == STOPPED) + { + if (LOG.isDebugEnabled()) + LOG.debug("Released in save {}", buffer); + buffer.release(); + } + else + { + throw new IllegalStateException("Buffer already saved"); + } + } } - private void releaseNetworkBuffer() + private void stop() { - RetainableByteBuffer.Mutable currentBuffer = networkBuffer; - if (currentBuffer == null) - return; - - if (currentBuffer.hasRemaining() && !shutdown && !failed) - throw new IllegalStateException(); - - currentBuffer.release(); - networkBuffer = null; - if (LOG.isDebugEnabled()) - LOG.debug("Released {}", currentBuffer); + RetainableByteBuffer.Mutable buffer = savedBuffer.getAndSet(STOPPED); + if (buffer != null) + { + if (LOG.isDebugEnabled()) + LOG.debug("Released in stop {}", buffer); + buffer.release(); + } } @Override diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java index c32a6969fcef..b7a1130e1531 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java @@ -84,6 +84,9 @@ public void onHeaders(Stream stream, HeadersFrame frame) stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(16), true), Callback.NOOP); assertTrue(latch.await(5, TimeUnit.SECONDS)); + + // Stop the client so that all connections are closed and any saved buffers are released + http2Client.stop(); } @Test diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HTTP2Test.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HTTP2Test.java index 66f02599b7eb..24e902cdbb8a 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HTTP2Test.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HTTP2Test.java @@ -248,6 +248,9 @@ public void onDataAvailable(Stream stream) .thenAccept(s -> s.data(new DataFrame(s.getId(), ByteBuffer.allocate(1024), true))); assertTrue(latch.await(5, TimeUnit.SECONDS)); + + // Stop the client so that all connections are closed and any saved buffers are released + http2Client.stop(); } @Test diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HttpClientTransportOverHTTP2Test.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HttpClientTransportOverHTTP2Test.java index 1a07fd88749d..15e590b52934 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HttpClientTransportOverHTTP2Test.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HttpClientTransportOverHTTP2Test.java @@ -890,6 +890,9 @@ public boolean handle(Request request, org.eclipse.jetty.server.Response respons assertEquals(HttpStatus.OK_200, result.getResponse().getStatus()); assertNotNull(result.getRequestFailure()); assertNotNull(result.getResponseFailure()); + + // Stop the client so that all connections are closed and any saved buffers are released + http2Client.stop(); } @Test diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/StreamResetTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/StreamResetTest.java index 611b3e06109a..68fe56f69e9b 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/StreamResetTest.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/StreamResetTest.java @@ -622,6 +622,9 @@ public void succeeded() // for the client to process the window updates. await().atMost(2 * delay, TimeUnit.MILLISECONDS) .until(() -> ((HTTP2Session)client).updateSendWindow(0), Matchers.greaterThan(0)); + + // Stop the client so that all connections are closed and any saved buffers are released + http2Client.stop(); } } From 18f655c03662399ae645fa5336f134d7d630a415 Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 8 Nov 2024 09:54:15 +1100 Subject: [PATCH 44/61] Fix test leaks --- .../test/java/org/eclipse/jetty/http2/tests/AbstractTest.java | 3 ++- .../test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java | 3 --- .../test/java/org/eclipse/jetty/http2/tests/HTTP2Test.java | 3 --- .../jetty/http2/tests/HttpClientTransportOverHTTP2Test.java | 3 --- .../java/org/eclipse/jetty/http2/tests/StreamResetTest.java | 3 --- .../org/eclipse/jetty/test/client/transport/AbstractTest.java | 4 +++- 6 files changed, 5 insertions(+), 14 deletions(-) diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractTest.java index 9a5f944f14da..36637402bc5e 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractTest.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractTest.java @@ -143,6 +143,8 @@ protected MetaData.Request newRequest(String method, String path, HttpFields fie @AfterEach public void dispose() throws Exception { + // Stop the client so that all connections are closed and any saved buffers are released + LifeCycle.stop(httpClient); try { if (serverBufferPool != null) @@ -152,7 +154,6 @@ public void dispose() throws Exception } finally { - LifeCycle.stop(httpClient); LifeCycle.stop(server); } } diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java index b7a1130e1531..c32a6969fcef 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java @@ -84,9 +84,6 @@ public void onHeaders(Stream stream, HeadersFrame frame) stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(16), true), Callback.NOOP); assertTrue(latch.await(5, TimeUnit.SECONDS)); - - // Stop the client so that all connections are closed and any saved buffers are released - http2Client.stop(); } @Test diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HTTP2Test.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HTTP2Test.java index 24e902cdbb8a..66f02599b7eb 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HTTP2Test.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HTTP2Test.java @@ -248,9 +248,6 @@ public void onDataAvailable(Stream stream) .thenAccept(s -> s.data(new DataFrame(s.getId(), ByteBuffer.allocate(1024), true))); assertTrue(latch.await(5, TimeUnit.SECONDS)); - - // Stop the client so that all connections are closed and any saved buffers are released - http2Client.stop(); } @Test diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HttpClientTransportOverHTTP2Test.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HttpClientTransportOverHTTP2Test.java index 15e590b52934..1a07fd88749d 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HttpClientTransportOverHTTP2Test.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HttpClientTransportOverHTTP2Test.java @@ -890,9 +890,6 @@ public boolean handle(Request request, org.eclipse.jetty.server.Response respons assertEquals(HttpStatus.OK_200, result.getResponse().getStatus()); assertNotNull(result.getRequestFailure()); assertNotNull(result.getResponseFailure()); - - // Stop the client so that all connections are closed and any saved buffers are released - http2Client.stop(); } @Test diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/StreamResetTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/StreamResetTest.java index 68fe56f69e9b..611b3e06109a 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/StreamResetTest.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/StreamResetTest.java @@ -622,9 +622,6 @@ public void succeeded() // for the client to process the window updates. await().atMost(2 * delay, TimeUnit.MILLISECONDS) .until(() -> ((HTTP2Session)client).updateSendWindow(0), Matchers.greaterThan(0)); - - // Stop the client so that all connections are closed and any saved buffers are released - http2Client.stop(); } } diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java index 0d61eb3cac6b..d706fe6f0df3 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java @@ -127,6 +127,8 @@ public static Collection transportsTLS() @AfterEach public void dispose(TestInfo testInfo) throws Exception { + // Stop the client so that all connections are closed and any saved buffers are released + LifeCycle.stop(client); try { if (serverBufferPool != null && !isLeakTrackingDisabled(testInfo, "server")) @@ -136,7 +138,7 @@ public void dispose(TestInfo testInfo) throws Exception } finally { - stop(); + LifeCycle.stop(server); } } From 73e21e742b78047c6fb4e3d4967b4484dc7b07b7 Mon Sep 17 00:00:00 2001 From: gregw Date: Sat, 9 Nov 2024 08:18:58 +1100 Subject: [PATCH 45/61] updates from review + rename save to hold + added "Bytes" to configuration --- .../client/HTTP2ClientConnectionFactory.java | 2 +- .../eclipse/jetty/http2/HTTP2Connection.java | 21 ++++++++-------- .../src/main/config/etc/jetty-delayed.xml | 2 +- .../config/modules/delay-until-content.mod | 6 ++--- .../jetty/server/handler/DelayedHandler.java | 24 +++++++++---------- 5 files changed, 27 insertions(+), 28 deletions(-) diff --git a/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java b/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java index 920f69d3bc2a..1c53f6096ad4 100644 --- a/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java +++ b/jetty-core/jetty-http2/jetty-http2-client/src/main/java/org/eclipse/jetty/http2/client/HTTP2ClientConnectionFactory.java @@ -83,7 +83,7 @@ private static class HTTP2ClientConnection extends HTTP2Connection implements Ca private HTTP2ClientConnection(HTTP2Client client, EndPoint endpoint, HTTP2ClientSession session, Promise sessionPromise, Session.Listener listener) { - super(client.getByteBufferPool(), client.getExecutor(), endpoint, session, client.getInputBufferSize(), 0); + super(client.getByteBufferPool(), client.getExecutor(), endpoint, session, client.getInputBufferSize(), -1); this.client = client; this.promise = sessionPromise; this.listener = listener; diff --git a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java index dce96fb6da33..8e3d2b28dc5b 100644 --- a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java +++ b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java @@ -327,7 +327,7 @@ protected class HTTP2Producer implements ExecutionStrategy.Producer { private static final RetainableByteBuffer.Mutable STOPPED = new RetainableByteBuffer.NonRetainableByteBuffer(BufferUtil.EMPTY_BUFFER); private final Callback fillableCallback = new FillableCallback(); - private final AtomicReference savedBuffer = new AtomicReference<>(); + private final AtomicReference heldBuffer = new AtomicReference<>(); private RetainableByteBuffer.Mutable networkBuffer; private boolean shutdown; private boolean failed; @@ -336,8 +336,8 @@ private void setInputBuffer(ByteBuffer byteBuffer) { RetainableByteBuffer.Mutable networkBuffer = acquireBuffer(); if (!networkBuffer.append(byteBuffer)) - LOG.warn("overflow"); - saveBuffer(networkBuffer); + throw new IllegalStateException("overflow"); + holdBuffer(networkBuffer); } @Override @@ -353,7 +353,6 @@ public Runnable produce() return null; boolean interested = false; - int filled = 0; networkBuffer = acquireBuffer(); try { @@ -397,7 +396,7 @@ public Runnable produce() } } - filled = fill(getEndPoint(), networkBuffer.getByteBuffer(), compact); + int filled = fill(getEndPoint(), networkBuffer.getByteBuffer(), compact); if (LOG.isDebugEnabled()) LOG.debug("Filled {} bytes compacted {} in {}", filled, compact, networkBuffer); @@ -423,7 +422,7 @@ else if (filled == 0) { if (networkBuffer.isRetained() && !shutdown) { - saveBuffer(networkBuffer); + holdBuffer(networkBuffer); } else { @@ -439,7 +438,7 @@ else if (filled == 0) private RetainableByteBuffer.Mutable acquireBuffer() { - RetainableByteBuffer.Mutable buffer = savedBuffer.getAndSet(null); + RetainableByteBuffer.Mutable buffer = heldBuffer.getAndSet(null); if (buffer == null) buffer = bufferPool.acquire(bufferSize, isUseInputDirectByteBuffers()).asMutable(); if (LOG.isDebugEnabled()) @@ -447,16 +446,16 @@ private RetainableByteBuffer.Mutable acquireBuffer() return buffer; } - private void saveBuffer(RetainableByteBuffer.Mutable buffer) + private void holdBuffer(RetainableByteBuffer.Mutable buffer) { - if (savedBuffer.compareAndSet(null, buffer)) + if (heldBuffer.compareAndSet(null, buffer)) { if (LOG.isDebugEnabled()) LOG.debug("Saved {}", buffer); } else { - if (savedBuffer.get() == STOPPED) + if (heldBuffer.get() == STOPPED) { if (LOG.isDebugEnabled()) LOG.debug("Released in save {}", buffer); @@ -471,7 +470,7 @@ private void saveBuffer(RetainableByteBuffer.Mutable buffer) private void stop() { - RetainableByteBuffer.Mutable buffer = savedBuffer.getAndSet(STOPPED); + RetainableByteBuffer.Mutable buffer = heldBuffer.getAndSet(STOPPED); if (buffer != null) { if (LOG.isDebugEnabled()) diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-delayed.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-delayed.xml index 6db65645493e..1ba6d7c8bcbb 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-delayed.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-delayed.xml @@ -9,7 +9,7 @@ - + diff --git a/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod b/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod index b4e9663f669f..773b46d4b7a3 100644 --- a/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod @@ -3,8 +3,8 @@ Applies DelayedHandler to entire server. Delays request handling until body content has arrived, to minimize blocking. For form data and multipart, the handling is delayed until the entire request body has -been asynchronously read. For all other content types, the delay is for up to a limited size, -which by default is one input buffer. +been asynchronously read. For all other content types, the delay is for up to a configurable +number of content bytes. [tags] server @@ -22,4 +22,4 @@ etc/jetty-delayed.xml [ini-template] #tag::documentation[] ## The maximum bytes to retain whilst delaying content; or 0 for no delay; or -1 (default) for a default value. -# jetty.delayed.maxRetainedContent=-1 +# jetty.delayed.maxRetainedContentBytes=-1 diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java index 1f29ca0bfea5..f584a507f49d 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -57,13 +57,13 @@ * as they attribute name. Once read by this handler, the parts are available via * {@link MultiPartFormData#getParts(Attributes)}, passing in the {@link Request} as the {@link Attributes} instance. *

- *

To delay for arbitrary content, the {@link #setMaxRetainedContent(int)} configuration must + *

To delay for arbitrary content, the {@link #setMaxRetainedContentBytes(long)} configuration must * be non zero. Once read, the data is made available via the standard {@link Request#read()} API. *

*/ public class DelayedHandler extends Handler.Wrapper { - private int _maxRetainedContent = -1; + private long _maxRetainedContentBytes = -1; public DelayedHandler() { @@ -75,19 +75,19 @@ public DelayedHandler(Handler handler) super(handler); } - public int getMaxRetainedContent() + public long getMaxRetainedContentBytes() { - return _maxRetainedContent; + return _maxRetainedContentBytes; } /** - * @param maxRetainedContent The maximum bytes to {@link RetainableByteBuffer#retain() retain} whilst delaying content; + * @param maxRetainedContentBytes The maximum bytes to {@link RetainableByteBuffer#retain() retain} whilst delaying content; * or 0 to never delay for content; * or -1 (default) for a heuristic value. */ - public void setMaxRetainedContent(int maxRetainedContent) + public void setMaxRetainedContentBytes(long maxRetainedContentBytes) { - _maxRetainedContent = maxRetainedContent; + _maxRetainedContentBytes = maxRetainedContentBytes; } @Override @@ -148,7 +148,7 @@ protected DelayedProcess newDelayedProcess(boolean contentExpected, String conte // if no known mimeType, then only delay until content if configured if (mimeType == null) - return _maxRetainedContent != 0 ? new UntilContentDelayedProcess(handler, request, response, callback, _maxRetainedContent) : null; + return _maxRetainedContentBytes != 0 ? new UntilContentDelayedProcess(handler, request, response, callback, _maxRetainedContentBytes) : null; // Otherwise, delay until a known content type is fully read; or if the type is not known then until the content is available return switch (mimeType) @@ -164,7 +164,7 @@ protected DelayedProcess newDelayedProcess(boolean contentExpected, String conte } // if other mimeType, then only delay until content if configured default -> - _maxRetainedContent != 0 ? new UntilContentDelayedProcess(handler, request, response, callback, _maxRetainedContent) : null; + _maxRetainedContentBytes != 0 ? new UntilContentDelayedProcess(handler, request, response, callback, _maxRetainedContentBytes) : null; }; } @@ -237,8 +237,8 @@ protected boolean process(Request request, Response response, Callback callback) protected static class UntilContentDelayedProcess extends DelayedProcess implements Invocable.Task { private final Deque _chunks = new ArrayDeque<>(); - private final int _maxSize; - private int _estimatedSize; + private final long _maxSize; + private long _estimatedSize; /** * @param handler The next handler @@ -248,7 +248,7 @@ protected static class UntilContentDelayedProcess extends DelayedProcess impleme * @param maxSize The maximum size to buffer before dispatching to the next handler; * or -1 to use {@link HttpConnectionFactory#getInputBufferSize()} */ - public UntilContentDelayedProcess(Handler handler, Request request, Response response, Callback callback, int maxSize) + public UntilContentDelayedProcess(Handler handler, Request request, Response response, Callback callback, long maxSize) { super(handler, request, response, callback); _maxSize = maxSize < 0 ? request.getConnectionMetaData().getConnector().getConnectionFactory(HttpConnectionFactory.class).getInputBufferSize() : maxSize; From 278e925a3542c0aff8561e5bada6d30e294f8a0a Mon Sep 17 00:00:00 2001 From: gregw Date: Sat, 9 Nov 2024 16:35:51 +1100 Subject: [PATCH 46/61] Implemented for H3 --- .../jetty/http3/HTTP3StreamConnection.java | 65 +++++++++++++++---- .../internal/ServerHTTP3StreamConnection.java | 2 +- .../transport/ServerRetainContentTest.java | 1 - 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java b/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java index cde4fed0c315..5cd694f9c433 100644 --- a/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java +++ b/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java @@ -30,6 +30,7 @@ import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.RetainableByteBuffer; import org.eclipse.jetty.quic.common.QuicStreamEndPoint; +import org.eclipse.jetty.util.BufferUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,14 +46,21 @@ public abstract class HTTP3StreamConnection extends AbstractConnection private boolean useInputDirectByteBuffers = true; private HTTP3Stream stream; private RetainableByteBuffer inputBuffer; + private final int minBufferSpace; private boolean remotelyClosed; public HTTP3StreamConnection(QuicStreamEndPoint endPoint, Executor executor, ByteBufferPool bufferPool, MessageParser parser) + { + this(endPoint, executor, bufferPool, parser, -1); + } + + public HTTP3StreamConnection(QuicStreamEndPoint endPoint, Executor executor, ByteBufferPool bufferPool, MessageParser parser, int minBufferSpace) { super(endPoint, executor); this.bufferPool = bufferPool; this.parser = parser; parser.init(MessageListener::new); + this.minBufferSpace = minBufferSpace < 0 ? 1024 : minBufferSpace; } public void onFailure(Throwable failure) @@ -90,6 +98,13 @@ public void onOpen() fillInterested(); } + @Override + public void onClose(Throwable cause) + { + super.onClose(cause); + tryReleaseInputBuffer(true); + } + @Override protected boolean onReadTimeout(TimeoutException timeout) { @@ -145,7 +160,7 @@ private void processDataFrames(boolean setFillInterest) } catch (Throwable x) { - tryReleaseInputBuffer(true); + tryReleaseInputBuffer(remotelyClosed); long error = HTTP3ErrorCode.REQUEST_CANCELLED_ERROR.code(); getEndPoint().close(error, x); // Notify the application that a failure happened. @@ -233,7 +248,7 @@ private void processNonDataFrames() } catch (Throwable x) { - tryReleaseInputBuffer(true); + tryReleaseInputBuffer(remotelyClosed); long error = HTTP3ErrorCode.REQUEST_CANCELLED_ERROR.code(); getEndPoint().close(error, x); // Notify the application that a failure happened. @@ -262,6 +277,9 @@ private void tryReleaseInputBuffer(boolean force) { if (inputBuffer != null) { + if (inputBuffer.isRetained() && !force) + return; + if (inputBuffer.hasRemaining() && force) inputBuffer.clear(); if (inputBuffer.isEmpty()) @@ -290,17 +308,27 @@ private MessageParser.Result parseAndFill(boolean setFillInterest) throws IOExce if (result != MessageParser.Result.NO_FRAME) return result; + boolean compact = true; if (inputBuffer.isRetained()) { - inputBuffer.release(); - RetainableByteBuffer newBuffer = bufferPool.acquire(getInputBufferSize(), isUseInputDirectByteBuffers()); - if (LOG.isDebugEnabled()) - LOG.debug("reacquired {} for retained {}", newBuffer, inputBuffer); - inputBuffer = newBuffer; - byteBuffer = inputBuffer.getByteBuffer(); + // If there is sufficient space available, we can top up the buffer rather than allocate a new one + if (minBufferSpace > 0 && BufferUtil.space(inputBuffer.getByteBuffer()) >= minBufferSpace) + { + // do not compact the buffer + compact = false; + } + else + { + inputBuffer.release(); + RetainableByteBuffer newBuffer = bufferPool.acquire(getInputBufferSize(), isUseInputDirectByteBuffers()); + if (LOG.isDebugEnabled()) + LOG.debug("reacquired {} for retained {}", newBuffer, inputBuffer); + inputBuffer = newBuffer; + byteBuffer = inputBuffer.getByteBuffer(); + } } - int filled = fill(byteBuffer); + int filled = fill(byteBuffer, compact); if (LOG.isDebugEnabled()) LOG.debug("filled {} on {} with buffer {}", filled, this, inputBuffer); @@ -335,9 +363,24 @@ private MessageParser.Result parseAndFill(boolean setFillInterest) throws IOExce } } - private int fill(ByteBuffer byteBuffer) throws IOException + private int fill(ByteBuffer buffer, boolean compact) throws IOException { - return getEndPoint().fill(byteBuffer); + int padding = 0; + try + { + if (!compact) + { + // Add padding content to avoid compaction + padding = buffer.limit(); + buffer.position(0); + } + return getEndPoint().fill(buffer); + } + finally + { + if (!compact && padding > 0) + buffer.position(padding); + } } private void processHeaders(HeadersFrame frame, boolean wasBlocked, Runnable delegate) diff --git a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/ServerHTTP3StreamConnection.java b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/ServerHTTP3StreamConnection.java index 215f95f3e527..41c8431dfba9 100644 --- a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/ServerHTTP3StreamConnection.java +++ b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/ServerHTTP3StreamConnection.java @@ -43,7 +43,7 @@ public class ServerHTTP3StreamConnection extends HTTP3StreamConnection public ServerHTTP3StreamConnection(Connector connector, HttpConfiguration httpConfiguration, QuicStreamEndPoint endPoint, ServerHTTP3Session session, MessageParser parser) { - super(endPoint, connector.getExecutor(), connector.getByteBufferPool(), parser); + super(endPoint, connector.getExecutor(), connector.getByteBufferPool(), parser, httpConfiguration.getMinInputBufferSpace()); this.connector = connector; this.httpConfiguration = httpConfiguration; this.session = session; diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerRetainContentTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerRetainContentTest.java index 7849a7542842..20842b490249 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerRetainContentTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerRetainContentTest.java @@ -52,7 +52,6 @@ protected void prepareServer(Transport transport, Handler handler) throws Except public void testRetainPOST(Transport transport) throws Exception { assumeTrue(transport != Transport.FCGI); - assumeTrue(transport != Transport.H3); Queue chunks = new ConcurrentLinkedQueue<>(); CountDownLatch blocked = new CountDownLatch(1); From 4685621cce08b75e341ee4cbabb1aa6e418756c4 Mon Sep 17 00:00:00 2001 From: gregw Date: Sun, 10 Nov 2024 08:35:13 +1100 Subject: [PATCH 47/61] Updates from review --- .../org/eclipse/jetty/http2/HTTP2Connection.java | 6 +++--- .../eclipse/jetty/http3/HTTP3StreamConnection.java | 12 ++++++------ .../jetty/server/internal/HttpConnection.java | 2 +- .../client/transport/ServerRetainContentTest.java | 12 +----------- 4 files changed, 11 insertions(+), 21 deletions(-) diff --git a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java index 8e3d2b28dc5b..8e19ad523e44 100644 --- a/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java +++ b/jetty-core/jetty-http2/jetty-http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Connection.java @@ -75,7 +75,7 @@ protected HTTP2Connection(ByteBufferPool bufferPool, Executor executor, EndPoint this.bufferPool = bufferPool; this.session = session; this.bufferSize = bufferSize; - this.minBufferSpace = minBufferSpace < 0 ? Math.min(1024, bufferSize) : minBufferSpace; + this.minBufferSpace = minBufferSpace < 0 ? Math.min(1500, bufferSize) : minBufferSpace; this.strategy = new AdaptiveExecutionStrategy(producer, executor); LifeCycle.start(strategy); } @@ -451,14 +451,14 @@ private void holdBuffer(RetainableByteBuffer.Mutable buffer) if (heldBuffer.compareAndSet(null, buffer)) { if (LOG.isDebugEnabled()) - LOG.debug("Saved {}", buffer); + LOG.debug("Held {}", buffer); } else { if (heldBuffer.get() == STOPPED) { if (LOG.isDebugEnabled()) - LOG.debug("Released in save {}", buffer); + LOG.debug("Released instead of holding {}", buffer); buffer.release(); } else diff --git a/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java b/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java index 5cd694f9c433..8e300a1bee37 100644 --- a/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java +++ b/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java @@ -42,11 +42,11 @@ public abstract class HTTP3StreamConnection extends AbstractConnection private final AtomicReference action = new AtomicReference<>(); private final ByteBufferPool bufferPool; + private final int minInputBufferSpace; private final MessageParser parser; private boolean useInputDirectByteBuffers = true; private HTTP3Stream stream; private RetainableByteBuffer inputBuffer; - private final int minBufferSpace; private boolean remotelyClosed; public HTTP3StreamConnection(QuicStreamEndPoint endPoint, Executor executor, ByteBufferPool bufferPool, MessageParser parser) @@ -54,13 +54,13 @@ public HTTP3StreamConnection(QuicStreamEndPoint endPoint, Executor executor, Byt this(endPoint, executor, bufferPool, parser, -1); } - public HTTP3StreamConnection(QuicStreamEndPoint endPoint, Executor executor, ByteBufferPool bufferPool, MessageParser parser, int minBufferSpace) + public HTTP3StreamConnection(QuicStreamEndPoint endPoint, Executor executor, ByteBufferPool bufferPool, MessageParser parser, int minInputBufferSpace) { super(endPoint, executor); this.bufferPool = bufferPool; this.parser = parser; parser.init(MessageListener::new); - this.minBufferSpace = minBufferSpace < 0 ? 1024 : minBufferSpace; + this.minInputBufferSpace = minInputBufferSpace < 0 ? 1500 : minInputBufferSpace; } public void onFailure(Throwable failure) @@ -160,7 +160,7 @@ private void processDataFrames(boolean setFillInterest) } catch (Throwable x) { - tryReleaseInputBuffer(remotelyClosed); + tryReleaseInputBuffer(true); long error = HTTP3ErrorCode.REQUEST_CANCELLED_ERROR.code(); getEndPoint().close(error, x); // Notify the application that a failure happened. @@ -248,7 +248,7 @@ private void processNonDataFrames() } catch (Throwable x) { - tryReleaseInputBuffer(remotelyClosed); + tryReleaseInputBuffer(true); long error = HTTP3ErrorCode.REQUEST_CANCELLED_ERROR.code(); getEndPoint().close(error, x); // Notify the application that a failure happened. @@ -312,7 +312,7 @@ private MessageParser.Result parseAndFill(boolean setFillInterest) throws IOExce if (inputBuffer.isRetained()) { // If there is sufficient space available, we can top up the buffer rather than allocate a new one - if (minBufferSpace > 0 && BufferUtil.space(inputBuffer.getByteBuffer()) >= minBufferSpace) + if (minInputBufferSpace > 0 && BufferUtil.space(inputBuffer.getByteBuffer()) >= minInputBufferSpace) { // do not compact the buffer compact = false; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java index 24c3578599bf..94e60222ccac 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java @@ -140,7 +140,7 @@ public HttpConnection(HttpConfiguration configuration, Connector connector, EndP _httpChannel = newHttpChannel(connector.getServer(), configuration); _requestHandler = newRequestHandler(); _parser = newHttpParser(configuration.getHttpCompliance()); - _minBufferSpace = configuration.getMinInputBufferSpace() < 0 ? Math.min(1024, configuration.getInputBufferSize()) : configuration.getMinInputBufferSpace(); + _minBufferSpace = configuration.getMinInputBufferSpace() < 0 ? Math.min(1500, configuration.getInputBufferSize()) : configuration.getMinInputBufferSpace(); if (LOG.isDebugEnabled()) LOG.debug("New HTTP Connection {}", this); diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerRetainContentTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerRetainContentTest.java index 20842b490249..17061e30ca65 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerRetainContentTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/ServerRetainContentTest.java @@ -36,23 +36,13 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeTrue; public class ServerRetainContentTest extends AbstractTest { - - @Override - protected void prepareServer(Transport transport, Handler handler) throws Exception - { - super.prepareServer(transport, handler); - } - @ParameterizedTest - @MethodSource("transports") + @MethodSource("transportsNoFCGI") public void testRetainPOST(Transport transport) throws Exception { - assumeTrue(transport != Transport.FCGI); - Queue chunks = new ConcurrentLinkedQueue<>(); CountDownLatch blocked = new CountDownLatch(1); From 4a0e1c1b31720cb8f71761374ca528d2e6b7db04 Mon Sep 17 00:00:00 2001 From: gregw Date: Mon, 11 Nov 2024 13:22:43 +1100 Subject: [PATCH 48/61] Configurable chunk overhead --- .../jetty/server/handler/DelayedHandler.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java index f584a507f49d..a2b8a3ddf7b4 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -238,6 +238,7 @@ protected static class UntilContentDelayedProcess extends DelayedProcess impleme { private final Deque _chunks = new ArrayDeque<>(); private final long _maxSize; + private final int _chunkOverhead; private long _estimatedSize; /** @@ -249,9 +250,24 @@ protected static class UntilContentDelayedProcess extends DelayedProcess impleme * or -1 to use {@link HttpConnectionFactory#getInputBufferSize()} */ public UntilContentDelayedProcess(Handler handler, Request request, Response response, Callback callback, long maxSize) + { + this(handler, request, response, callback, maxSize, -1); + } + + /** + * @param handler The next handler + * @param request The delayed request + * @param response The delayed response + * @param callback The delayed callback + * @param maxSize The maximum size to buffer before dispatching to the next handler; + * or -1 to use {@link HttpConnectionFactory#getInputBufferSize()} + * @param chunkOverhead The bytes to account for per chunk when calculating the size; or -1 for a default. + */ + public UntilContentDelayedProcess(Handler handler, Request request, Response response, Callback callback, long maxSize, int chunkOverhead) { super(handler, request, response, callback); _maxSize = maxSize < 0 ? request.getConnectionMetaData().getConnector().getConnectionFactory(HttpConnectionFactory.class).getInputBufferSize() : maxSize; + _chunkOverhead = chunkOverhead < 0 ? 8 : chunkOverhead; } @Override @@ -279,7 +295,7 @@ protected void read(boolean execute) } // Estimated size is 8 byte framing overhead per chunk plus the chunk size - _estimatedSize += 8 + chunk.remaining(); + _estimatedSize += _chunkOverhead + chunk.remaining(); if (chunk.isLast() || _estimatedSize >= _maxSize) { From 9a23ecc0c9de3386e38432b577ac88a3756d7c7e Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 12 Nov 2024 16:13:12 +1100 Subject: [PATCH 49/61] Updates after review --- .../org/eclipse/jetty/http/MimeTypes.java | 13 + .../eclipse/jetty/http/MultiPartConfig.java | 10 + .../src/main/config/etc/jetty-delayed.xml | 8 +- .../main/config/etc/jetty-eager-content.xml | 33 + .../etc/jetty-eager-multipart-content.xml | 9 + .../config/modules/delay-until-content.mod | 4 +- .../src/main/config/modules/eager-content.mod | 43 ++ .../src/main/java/module-info.java | 1 + .../org/eclipse/jetty/server/FormFields.java | 25 +- .../jetty/server/handler/DelayedHandler.java | 480 -------------- .../server/handler/EagerContentHandler.java | 618 ++++++++++++++++++ .../jetty/server/ThreadStarvationTest.java | 4 +- ...Test.java => EagerContentHandlerTest.java} | 153 ++--- .../jetty/server/jmh/HandlerBenchmark.java | 8 +- .../ee10/servlets/ThreadStarvationTest.java | 6 +- .../ee10/test/HttpInputIntegrationTest.java | 8 +- .../jetty/ee10/webapp/HugeResourceTest.java | 8 +- .../ee11/servlets/ThreadStarvationTest.java | 6 +- .../jetty/ee11/webapp/HugeResourceTest.java | 8 +- 19 files changed, 856 insertions(+), 589 deletions(-) create mode 100644 jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml create mode 100644 jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml create mode 100644 jetty-core/jetty-server/src/main/config/modules/eager-content.mod delete mode 100644 jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java create mode 100644 jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EagerContentHandler.java rename jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/{DelayedHandlerTest.java => EagerContentHandlerTest.java} (82%) diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java index 49cc1d535611..50f530284071 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java @@ -699,6 +699,19 @@ public static MimeTypes.Type getMimeTypeFromContentType(HttpField field) return MimeTypes.CACHE.get(contentType); } + public static String getMimeTypeAsStringFromContentType(HttpField field) + { + if (field == null) + return null; + + assert field.getHeader() == HttpHeader.CONTENT_TYPE; + + if (field instanceof MimeTypes.ContentTypeField contentTypeField) + return contentTypeField.getMimeType().asString(); + + return getBase(field.getValue()); + } + /** * Efficiently extract the charset value from a {@code Content-Type} {@link HttpField}. * @param field A {@code Content-Type} field. diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartConfig.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartConfig.java index 608674a69bbf..ba78ab5ca006 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartConfig.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartConfig.java @@ -18,6 +18,7 @@ import org.eclipse.jetty.io.Content; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.resource.ResourceFactory; import static org.eclipse.jetty.http.ComplianceViolation.Listener.NOOP; @@ -51,6 +52,15 @@ public Builder() { } + /** + * @param location the directory where parts will be saved as files. + */ + public Builder location(String location) + { + location(ResourceFactory.root().newResource(location).getPath()); + return this; + } + /** * @param location the directory where parts will be saved as files. */ diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-delayed.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-delayed.xml index 1ba6d7c8bcbb..dc08cd0e83c9 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-delayed.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-delayed.xml @@ -1,16 +1,10 @@ - - - - - - - + diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml new file mode 100644 index 000000000000..318481cbc3bf --- /dev/null +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml new file mode 100644 index 000000000000..33a16e8c50de --- /dev/null +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod b/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod index 773b46d4b7a3..7c40cb958539 100644 --- a/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod @@ -1,6 +1,6 @@ [description] -Applies DelayedHandler to entire server. +Applies DEPRECATED DelayedHandler to entire server. Delays request handling until body content has arrived, to minimize blocking. For form data and multipart, the handling is delayed until the entire request body has been asynchronously read. For all other content types, the delay is for up to a configurable @@ -18,8 +18,8 @@ threadlimit [xml] etc/jetty-delayed.xml - [ini-template] #tag::documentation[] ## The maximum bytes to retain whilst delaying content; or 0 for no delay; or -1 (default) for a default value. # jetty.delayed.maxRetainedContentBytes=-1 +#end::documentation[] \ No newline at end of file diff --git a/jetty-core/jetty-server/src/main/config/modules/eager-content.mod b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod new file mode 100644 index 000000000000..070f51fd3e55 --- /dev/null +++ b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod @@ -0,0 +1,43 @@ + +[description] +Applies the EagerContentHandler to the entire server +#tag::description[] +The EagerContentHandler can eagerly load content asynchronously before calling the next handler. +Typically this handler is deployed before an application that uses blocking IO to read the request body and if deployed +after this handler, the application will never (or seldom) block for request content. +This gives many of the benefits of asynchronous IO without the need to write an asynchronous application. +#end::description[] + +[tags] +server + +[depend] +server + +[before] +threadlimit + +[xml] +etc/jetty-eager-content.xml + +[ini-template] +#tag::documentation[] +## The maximum bytes to retain whilst delaying content; or 0 for no delay; or -1 (default) for a default value. +# jetty.delayed.maxRetainedContentBytes=-1 + +## The maximum number of FormFields to be eagerly loaded or -1 for a default +# jetty.eager.form.maxFields=-1 + +## The maximum size of FormFields to be eagerly loaded or -1 for a default +# jetty.eager.form.maxLength=-1 + +## The maximum bytes of retained data to be eagerly loaded or -1 for a default +# jetty.eager.retained.maxRetainedBytes=-1 + +## The frame overhead to use when calculating the retained bytes or -1 for a default +# jetty.eager.retained.frameOverhead=-1 + +## If requests should be rejected if they exceed the maxRetainedBytes +# jetty.eager.retained.reject=false + +#end::documentation[] \ No newline at end of file diff --git a/jetty-core/jetty-server/src/main/java/module-info.java b/jetty-core/jetty-server/src/main/java/module-info.java index 8f3ef3e0f50a..7a9dd70a7b55 100644 --- a/jetty-core/jetty-server/src/main/java/module-info.java +++ b/jetty-core/jetty-server/src/main/java/module-info.java @@ -18,6 +18,7 @@ // Only required if using JMX. requires static org.eclipse.jetty.jmx; + requires java.desktop; exports org.eclipse.jetty.server; exports org.eclipse.jetty.server.handler; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java index b418cb67309f..428052c640cc 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/FormFields.java @@ -195,8 +195,29 @@ public static void onFields(Request request, Promise.Invocable promise) */ public static void onFields(Request request, Charset charset, Promise.Invocable promise) { - int maxFields = getContextAttribute(request.getContext(), FormFields.MAX_FIELDS_ATTRIBUTE, FormFields.MAX_FIELDS_DEFAULT); - int maxLength = getContextAttribute(request.getContext(), FormFields.MAX_LENGTH_ATTRIBUTE, FormFields.MAX_LENGTH_DEFAULT); + onFields(request, charset, -1, -1, promise); + } + + /** + * Asynchronously read and parse FormFields from a {@link Request}. + *

+ * Calls to {@code onFields} and {@code getFields} methods are idempotent, and + * can be called multiple times, with subsequent calls returning the results of the first call. + * @param request The request to get or read the Fields from + * @param charset The {@link Charset} of the request content, if previously extracted. + * @param maxFields The maximum number of fields to be parsed; or -1 for a default + * @param maxLength The maximum total size of the fields; or -1 for a default + * @param promise The action to take when the FormFields are available. + * @see #onFields(Request, Charset, Promise.Invocable) + * @see #getFields(Request) + * @see #getFields(Request, int, int) + */ + public static void onFields(Request request, Charset charset, int maxFields, int maxLength, Promise.Invocable promise) + { + if (maxFields < 0) + maxFields = getContextAttribute(request.getContext(), FormFields.MAX_FIELDS_ATTRIBUTE, FormFields.MAX_FIELDS_DEFAULT); + if (maxLength < 0) + maxLength = getContextAttribute(request.getContext(), FormFields.MAX_LENGTH_ATTRIBUTE, FormFields.MAX_LENGTH_DEFAULT); from(request, promise.getInvocationType(), request, charset, maxFields, maxLength).whenComplete(promise); } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java deleted file mode 100644 index a2b8a3ddf7b4..000000000000 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java +++ /dev/null @@ -1,480 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.server.handler; - -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicInteger; - -import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpHeaderValue; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.http.MimeTypes; -import org.eclipse.jetty.http.MultiPartConfig; -import org.eclipse.jetty.http.MultiPartFormData; -import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.io.RetainableByteBuffer; -import org.eclipse.jetty.server.FormFields; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.HttpConnectionFactory; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.Attributes; -import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.Fields; -import org.eclipse.jetty.util.Promise; -import org.eclipse.jetty.util.StringUtil; -import org.eclipse.jetty.util.thread.Invocable; - -/** - *

A {@link Handler.Wrapper} that can delay calling {@link Handler#handle(Request, Response, Callback)} on the - * {@link #getHandler() next Handler} until content is available, either entirely or in part. This handler is fully - * asynchronous and will not block waiting for content. Furthermore, for known content types, the content may be - * parsed into {@link FormFields} or {@link MultiPartFormData.Parts} prior to handling. Thus, this handler can allow a - * blocking application to run without blocking on input, as the content is asynchronously read before the application - * is called. - *

- *

To delay for {@link FormFields}, the request content must be {@link org.eclipse.jetty.http.MimeTypes.Type#FORM_ENCODED}. - * Once read by this handler, the fields are available via {@link FormFields#getFields(Request)}. - *

- *

To delay for {@link MultiPartFormData} content, a {@link org.eclipse.jetty.http.MultiPartConfig} instance must be set as - * a {@link org.eclipse.jetty.server.Context} or {@link org.eclipse.jetty.server.Server} attribute with the class name - * as they attribute name. Once read by this handler, the parts are available via - * {@link MultiPartFormData#getParts(Attributes)}, passing in the {@link Request} as the {@link Attributes} instance. - *

- *

To delay for arbitrary content, the {@link #setMaxRetainedContentBytes(long)} configuration must - * be non zero. Once read, the data is made available via the standard {@link Request#read()} API. - *

- */ -public class DelayedHandler extends Handler.Wrapper -{ - private long _maxRetainedContentBytes = -1; - - public DelayedHandler() - { - this(null); - } - - public DelayedHandler(Handler handler) - { - super(handler); - } - - public long getMaxRetainedContentBytes() - { - return _maxRetainedContentBytes; - } - - /** - * @param maxRetainedContentBytes The maximum bytes to {@link RetainableByteBuffer#retain() retain} whilst delaying content; - * or 0 to never delay for content; - * or -1 (default) for a heuristic value. - */ - public void setMaxRetainedContentBytes(long maxRetainedContentBytes) - { - _maxRetainedContentBytes = maxRetainedContentBytes; - } - - @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception - { - Handler next = getHandler(); - if (next == null) - return false; - - boolean contentExpected = false; - String contentType = null; - MimeTypes.Type mimeType = null; - loop: for (HttpField field : request.getHeaders()) - { - HttpHeader header = field.getHeader(); - if (header == null) - continue; - switch (header) - { - case CONTENT_TYPE: - contentType = field.getValue(); - mimeType = MimeTypes.getMimeTypeFromContentType(field); - break; - - case CONTENT_LENGTH: - contentExpected = field.getLongValue() > 0; - break; - - case TRANSFER_ENCODING: - contentExpected = field.contains(HttpHeaderValue.CHUNKED.asString()); - break; - - case EXPECT: - if (field.contains(HttpHeaderValue.CONTINUE.asString())) - { - contentExpected = false; - break loop; - } - break; - default: - break; - } - } - - DelayedProcess delayed = newDelayedProcess(contentExpected, contentType, mimeType, next, request, response, callback); - if (delayed == null) - return next.handle(request, response, callback); - - delayed.delay(); - return true; - } - - protected DelayedProcess newDelayedProcess(boolean contentExpected, String contentType, MimeTypes.Type mimeType, Handler handler, Request request, Response response, Callback callback) - { - // if no content is expected, then no delay - if (!contentExpected) - return null; - - // if no known mimeType, then only delay until content if configured - if (mimeType == null) - return _maxRetainedContentBytes != 0 ? new UntilContentDelayedProcess(handler, request, response, callback, _maxRetainedContentBytes) : null; - - // Otherwise, delay until a known content type is fully read; or if the type is not known then until the content is available - return switch (mimeType) - { - case FORM_ENCODED -> new UntilFormDelayedProcess(handler, request, response, callback, contentType); - case MULTIPART_FORM_DATA -> - { - if (request.getContext().getAttribute(MultiPartConfig.class.getName()) instanceof MultiPartConfig mpc) - yield new UntilMultipartDelayedProcess(handler, request, response, callback, contentType, mpc); - if (getServer().getAttribute(MultiPartConfig.class.getName()) instanceof MultiPartConfig mpc) - yield new UntilMultipartDelayedProcess(handler, request, response, callback, contentType, mpc); - yield null; - } - // if other mimeType, then only delay until content if configured - default -> - _maxRetainedContentBytes != 0 ? new UntilContentDelayedProcess(handler, request, response, callback, _maxRetainedContentBytes) : null; - - }; - } - - protected abstract static class DelayedProcess - { - private final Handler _handler; - private final Request _request; - private final Response _response; - private final Callback _callback; - - protected DelayedProcess(Handler handler, Request request, Response response, Callback callback) - { - _handler = Objects.requireNonNull(handler); - _request = Objects.requireNonNull(request); - _response = Objects.requireNonNull(response); - _callback = Objects.requireNonNull(callback); - } - - protected Handler getHandler() - { - return _handler; - } - - protected Request getRequest() - { - return _request; - } - - protected Response getResponse() - { - return _response; - } - - protected Callback getCallback() - { - return _callback; - } - - protected void process() - { - process(getRequest(), getResponse(), getCallback()); - } - - protected boolean process(Request request, Response response, Callback callback) - { - try - { - if (getHandler().handle(request, response, callback)) - return true; - - // The handle was rejected, so write the error using the original potentially unwrapped request/response/callback - Response.writeError(getRequest(), getResponse(), getCallback(), HttpStatus.NOT_FOUND_404); - } - catch (Throwable t) - { - // The handle failed, so write the error using the original potentially unwrapped request/response/callback - Response.writeError(getRequest(), getResponse(), getCallback(), t); - } - // return false to indicate the passed request/response/callback were not used. - return false; - } - - protected abstract void delay() throws Exception; - } - - /** - * Delay dispatch until all content or an effective buffer size is reached - */ - protected static class UntilContentDelayedProcess extends DelayedProcess implements Invocable.Task - { - private final Deque _chunks = new ArrayDeque<>(); - private final long _maxSize; - private final int _chunkOverhead; - private long _estimatedSize; - - /** - * @param handler The next handler - * @param request The delayed request - * @param response The delayed response - * @param callback The delayed callback - * @param maxSize The maximum size to buffer before dispatching to the next handler; - * or -1 to use {@link HttpConnectionFactory#getInputBufferSize()} - */ - public UntilContentDelayedProcess(Handler handler, Request request, Response response, Callback callback, long maxSize) - { - this(handler, request, response, callback, maxSize, -1); - } - - /** - * @param handler The next handler - * @param request The delayed request - * @param response The delayed response - * @param callback The delayed callback - * @param maxSize The maximum size to buffer before dispatching to the next handler; - * or -1 to use {@link HttpConnectionFactory#getInputBufferSize()} - * @param chunkOverhead The bytes to account for per chunk when calculating the size; or -1 for a default. - */ - public UntilContentDelayedProcess(Handler handler, Request request, Response response, Callback callback, long maxSize, int chunkOverhead) - { - super(handler, request, response, callback); - _maxSize = maxSize < 0 ? request.getConnectionMetaData().getConnector().getConnectionFactory(HttpConnectionFactory.class).getInputBufferSize() : maxSize; - _chunkOverhead = chunkOverhead < 0 ? 8 : chunkOverhead; - } - - @Override - protected void delay() - { - read(false); - } - - protected void read(boolean execute) - { - while (true) - { - Content.Chunk chunk = super.getRequest().read(); - if (chunk == null) - { - getRequest().demand(this); - break; - } - - // retain the chunk in the queue - if (!_chunks.add(chunk)) - { - getCallback().failed(new IllegalStateException()); - break; - } - - // Estimated size is 8 byte framing overhead per chunk plus the chunk size - _estimatedSize += _chunkOverhead + chunk.remaining(); - - if (chunk.isLast() || _estimatedSize >= _maxSize) - { - if (execute) - getRequest().getContext().execute(this::doProcess); - else - doProcess(); - break; - } - } - } - - @Override - public InvocationType getInvocationType() - { - return InvocationType.NON_BLOCKING; - } - - /** - * This is run when enough content has been received to dispatch to the next handler. - */ - public void run() - { - read(true); - } - - private void doProcess() - { - RewindChunksRequest request = new RewindChunksRequest(getRequest(), getCallback(), _chunks); - if (!process(request, getResponse(), request)) - request.release(); - } - - private static class RewindChunksRequest extends Request.Wrapper implements Callback - { - private final Deque _chunks; - private final Callback _callback; - - public RewindChunksRequest(Request wrapped, Callback callback, Deque chunks) - { - super(wrapped); - _chunks = chunks; - _callback = callback; - } - - @Override - public Content.Chunk read() - { - if (_chunks.isEmpty()) - return super.read(); - return _chunks.removeFirst(); - } - - private void release() - { - _chunks.forEach(Content.Chunk::release); - _chunks.clear(); - } - - @Override - public void fail(Throwable failure, boolean last) - { - release(); - _callback.failed(failure); - } - - @Override - public void succeeded() - { - release(); - _callback.succeeded(); - } - } - } - - protected static class UntilFormDelayedProcess extends DelayedProcess - { - private final Charset _charset; - - public UntilFormDelayedProcess(Handler handler, Request request, Response response, Callback callback, String contentType) - { - super(handler, request, response, callback); - - String cs = MimeTypes.getCharsetFromContentType(contentType); - _charset = StringUtil.isEmpty(cs) ? StandardCharsets.UTF_8 : Charset.forName(cs); - } - - @Override - protected void delay() - { - InvocationType invocationType = getHandler().getInvocationType(); - AtomicInteger done = new AtomicInteger(2); - var onFields = new Promise.Invocable() - { - @Override - public void failed(Throwable x) - { - succeeded(null); - } - - @Override - public void succeeded(Fields result) - { - // If the handling thread has already exited, we must process without blocking from this callback - if (done.decrementAndGet() == 0) - invocationType.runWithoutBlocking(this::doProcess, getRequest().getContext()); - } - - private void doProcess() - { - process(); - } - - @Override - public InvocationType getInvocationType() - { - return invocationType; - } - }; - - // If the fields are already available, we can process from this handling thread - FormFields.onFields(getRequest(), _charset, onFields); - if (done.decrementAndGet() == 0) - process(); - } - } - - protected static class UntilMultipartDelayedProcess extends DelayedProcess - { - private final String _contentType; - private final MultiPartConfig _config; - - public UntilMultipartDelayedProcess(Handler handler, Request request, Response response, Callback callback, String contentType, MultiPartConfig config) - { - super(handler, request, response, callback); - _contentType = contentType; - _config = config; - } - - @Override - protected void delay() - { - Request request = getRequest(); - InvocationType invocationType = getHandler().getInvocationType(); - AtomicInteger done = new AtomicInteger(2); - - Promise.Invocable onParts = new Promise.Invocable<>() - { - @Override - public void failed(Throwable x) - { - succeeded(null); - } - - @Override - public void succeeded(MultiPartFormData.Parts result) - { - // If the handling thread has already exited, we must process without blocking from this callback - if (done.decrementAndGet() == 0) - invocationType.runWithoutBlocking(this::doProcess, getRequest().getContext()); - } - - private void doProcess() - { - process(); - } - - @Override - public InvocationType getInvocationType() - { - return invocationType; - } - }; - - MultiPartFormData.onParts(request, request, _contentType, _config, onParts); - - // If the parts are already available, we can process from this handling thread - if (done.decrementAndGet() == 0) - process(); - } - } -} diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EagerContentHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EagerContentHandler.java new file mode 100644 index 000000000000..7769dcf65b10 --- /dev/null +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EagerContentHandler.java @@ -0,0 +1,618 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.server.handler; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.http.MultiPartConfig; +import org.eclipse.jetty.http.MultiPartFormData; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.FormFields; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Attributes; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Fields; +import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.thread.Invocable; + +/** + *

A {@link ConditionalHandler} that can eagerly load content asynchronously before calling the + * {@link #getHandler() next handler}. Typically this handler is deployed before an application that uses + * blocking IO to read the request body. By using this handler, such an application can be run in a way so that it + * never (or seldom) blocks on request content. This gives many of the benefits of asynchronous IO without the + * need to write an asynchronous application. + *

+ *

The handler uses the configured {@link FormContentLoaderFactory} instances to eagerly load specific content types. + * By default, this handler supports eager loading of:

+ *
+ *
{@link FormFields}
Loaded and parsed in full by the {@link FormContentLoaderFactory}
+ *
{@link MultiPartFormData}
Loaded and parsed in full by the {@link MultiPartContentLoaderFactory}
+ *
{@link Content.Chunk}
Retained by the {@link RetainedContentLoaderFactory}
+ *
+ */ +public class EagerContentHandler extends ConditionalHandler.ElseNext +{ + private final Map _factoriesByMimeType = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + private final ContentLoaderFactory _defaultFactory; + + /** + * Construct an {@code EagerContentHandler} with default {@link ContentLoaderFactory EagerContentFactories} + */ + public EagerContentHandler() + { + this((Handler)null); + } + + /** + * Construct an {@code EagerContentHandler} with default {@link ContentLoaderFactory EagerContentFactories} + * @param handler The next handler (also can be set with {@link #setHandler(Handler)} + */ + public EagerContentHandler(Handler handler) + { + this(handler, new FormContentLoaderFactory(), new MultiPartContentLoaderFactory(), new RetainedContentLoaderFactory()); + } + + /** + * Construct an {@code EagerContentHandler} with specific {@link ContentLoaderFactory EagerContentFactories} + * @param factories The {@link ContentLoaderFactory} instances used to eagerly load content. + */ + public EagerContentHandler(ContentLoaderFactory... factories) + { + this(null, factories); + } + + /** + * Construct an {@code EagerContentHandler} with specific {@link ContentLoaderFactory EagerContentFactories} + * @param handler The next handler (also can be set with {@link #setHandler(Handler)} + * @param factories The {@link ContentLoaderFactory} instances used to eagerly load content. + */ + public EagerContentHandler(Handler handler, ContentLoaderFactory... factories) + { + super(handler); + ContentLoaderFactory dft = null; + for (ContentLoaderFactory factory : factories) + { + installBean(factory); + if (factory.getApplicableMimeType() == null) + dft = factory; + else + _factoriesByMimeType.put(factory.getApplicableMimeType(), factory); + } + _defaultFactory = dft; + } + + @Override + protected boolean onConditionsMet(Request request, Response response, Callback callback) throws Exception + { + Handler next = getHandler(); + if (next == null) + return false; + + boolean contentExpected = false; + String contentType = null; + String mimeType = null; + loop: + for (HttpField field : request.getHeaders()) + { + HttpHeader header = field.getHeader(); + if (header == null) + continue; + switch (header) + { + case CONTENT_TYPE: + contentType = field.getValue(); + mimeType = MimeTypes.getMimeTypeAsStringFromContentType(field); + break; + + case CONTENT_LENGTH: + contentExpected = field.getLongValue() > 0; + break; + + case TRANSFER_ENCODING: + contentExpected = field.contains(HttpHeaderValue.CHUNKED.asString()); + break; + + default: + break; + } + } + + if (!contentExpected) + return next.handle(request, response, callback); + + ContentLoaderFactory factory = mimeType == null ? null : _factoriesByMimeType.get(mimeType); + if (factory == null) + factory = _defaultFactory; + if (factory == null) + return next.handle(request, response, callback); + + ContentLoader process = factory.newContentLoader(contentType, mimeType, next, request, response, callback); + if (process == null) + return next.handle(request, response, callback); + + process.load(); + return true; + } + + /** + * A factory to create new {@link ContentLoader} instances for a specific mime type. + */ + public interface ContentLoaderFactory + { + /** + * @return The mimetype for which this factory is applicable to; or {@code null} if applicable to all types. + */ + String getApplicableMimeType(); + + /** + * @param contentType The content type of the request + * @param mimeType The mime type extracted from the request + * @param handler The next handler to call + * @param request The request + * @param response The response + * @param callback The callback + * @return An {@link ContentLoader} instance if the content can be loaded eagerly, else {@code null}. + */ + ContentLoader newContentLoader(String contentType, String mimeType, Handler handler, Request request, Response response, Callback callback); + } + + /** + * An Eager Content processor, created by a {@link ContentLoaderFactory} to asynchronous load content from a {@link Request} + * before calling the {@link Handler#handle(Request, Response, Callback)} method of the passed {@link Handler}. + */ + public abstract static class ContentLoader + { + private final Handler _handler; + private final Request _request; + private final Response _response; + private final Callback _callback; + + protected ContentLoader(Handler handler, Request request, Response response, Callback callback) + { + _handler = Objects.requireNonNull(handler); + _request = Objects.requireNonNull(request); + _response = Objects.requireNonNull(response); + _callback = Objects.requireNonNull(callback); + } + + protected Handler getHandler() + { + return _handler; + } + + protected Request getRequest() + { + return _request; + } + + protected Response getResponse() + { + return _response; + } + + protected Callback getCallback() + { + return _callback; + } + + protected void handle() + { + handle(getRequest(), getResponse(), getCallback()); + } + + protected boolean handle(Request request, Response response, Callback callback) + { + try + { + if (getHandler().handle(request, response, callback)) + return true; + + // The handle was rejected, so write the error using the original potentially unwrapped request/response/callback + Response.writeError(getRequest(), getResponse(), getCallback(), HttpStatus.NOT_FOUND_404); + } + catch (Throwable t) + { + // The handle failed, so write the error using the original potentially unwrapped request/response/callback + Response.writeError(getRequest(), getResponse(), getCallback(), t); + } + // return false to indicate the passed request/response/callback were not used. + return false; + } + + /** + * Called to initiate eager loading of the content. The content may be loaded within the scope + * of this method, or within the scope of a callback as a result of a {@link Request#demand(Runnable)} call made by + * this methhod. + * @throws Exception If there is a problem + */ + protected abstract void load() throws Exception; + } + + /** + * An {@link ContentLoaderFactory} for {@link MimeTypes.Type#FORM_ENCODED} content, that uses + * {@link FormFields#onFields(Request, Charset, int, int, Promise.Invocable)} to asynchronously load and parse the content. + */ + public static class FormContentLoaderFactory implements ContentLoaderFactory + { + private final int _maxFields; + private final int _maxLength; + + public FormContentLoaderFactory() + { + this(-1, -1); + } + + /** + * @param maxFields The maximum number of fields to be eagerly loaded; + * or -1 to use the default of {@link FormFields#onFields(Request, Charset, int, int, Promise.Invocable)} + * @param maxLength The maximum length of all combined fields to be eagerly loaded; + * or -1 to use the default of {@link FormFields#onFields(Request, Charset, int, int, Promise.Invocable)} + */ + public FormContentLoaderFactory(int maxFields, int maxLength) + { + _maxFields = maxFields; + _maxLength = maxLength; + } + + @Override + public String getApplicableMimeType() + { + return MimeTypes.Type.FORM_ENCODED.asString(); + } + + @Override + public ContentLoader newContentLoader(String contentType, String mimeType, Handler handler, Request request, Response response, Callback callback) + { + String cs = MimeTypes.getCharsetFromContentType(contentType); + Charset charset = StringUtil.isEmpty(cs) ? StandardCharsets.UTF_8 : Charset.forName(cs); + + return new ContentLoader(handler, request, response, callback) + { + @Override + protected void load() + { + InvocationType invocationType = getHandler().getInvocationType(); + AtomicInteger done = new AtomicInteger(2); + var onFields = new Promise.Invocable() + { + @Override + public void failed(Throwable x) + { + succeeded(null); + } + + @Override + public void succeeded(Fields result) + { + // If the handling thread has already exited, we must process without blocking from this callback + if (done.decrementAndGet() == 0) + invocationType.runWithoutBlocking(this::doProcess, getRequest().getContext()); + } + + private void doProcess() + { + handle(); + } + + @Override + public InvocationType getInvocationType() + { + return invocationType; + } + }; + + // If the fields are already available, we can process from this handling thread + FormFields.onFields(getRequest(), charset, _maxFields, _maxLength, onFields); + if (done.decrementAndGet() == 0) + handle(); + } + }; + } + } + + /** + * An {@link ContentLoaderFactory} for {@link MimeTypes.Type#MULTIPART_FORM_DATA} content, that uses + * {@link MultiPartFormData#onParts(Content.Source, Attributes, String, MultiPartConfig, Promise.Invocable)} + * to asynchronously load and parse the content. + */ + public static class MultiPartContentLoaderFactory implements ContentLoaderFactory + { + private final MultiPartConfig _multiPartConfig; + + public MultiPartContentLoaderFactory() + { + this(null); + } + + /** + * @param multiPartConfig The {@link MultiPartConfig} to use for eagerly loading content; + * or {@code null} to look for a {@link MultiPartConfig} as a + * {@link org.eclipse.jetty.server.Context} or {@link org.eclipse.jetty.server.Server} + * {@link Attributes attribute}, using the class name as the attribute name. + */ + public MultiPartContentLoaderFactory(MultiPartConfig multiPartConfig) + { + _multiPartConfig = multiPartConfig; + } + + @Override + public String getApplicableMimeType() + { + return MimeTypes.Type.MULTIPART_FORM_DATA.asString(); + } + + @Override + public ContentLoader newContentLoader(String contentType, String mimeType, Handler handler, Request request, Response response, Callback callback) + { + MultiPartConfig config = _multiPartConfig; + if (config == null && request.getContext().getAttribute(MultiPartConfig.class.getName()) instanceof MultiPartConfig mpc) + config = mpc; + if (config == null && handler.getServer().getAttribute(MultiPartConfig.class.getName()) instanceof MultiPartConfig mpc) + config = mpc; + if (config == null) + return null; + + MultiPartConfig multiPartConfig = config; + + return new ContentLoader(handler, request, response, callback) + { + @Override + protected void load() + { + Request request = getRequest(); + InvocationType invocationType = getHandler().getInvocationType(); + AtomicInteger done = new AtomicInteger(2); + + Promise.Invocable onParts = new Promise.Invocable<>() + { + @Override + public void failed(Throwable x) + { + succeeded(null); + } + + @Override + public void succeeded(MultiPartFormData.Parts result) + { + // If the handling thread has already exited, we must process without blocking from this callback + if (done.decrementAndGet() == 0) + invocationType.runWithoutBlocking(this::doProcess, getRequest().getContext()); + } + + private void doProcess() + { + handle(); + } + + @Override + public InvocationType getInvocationType() + { + return invocationType; + } + }; + + MultiPartFormData.onParts(request, request, contentType, multiPartConfig, onParts); + + // If the parts are already available, we can process from this handling thread + if (done.decrementAndGet() == 0) + handle(); + } + }; + } + } + + /** + * An {@link ContentLoaderFactory} for any content, that uses {@link Content.Chunk#retain()} to + * eagerly load content with zero copies, until all content is read or a maximum size is exceeded. + */ + public static class RetainedContentLoaderFactory implements ContentLoaderFactory + { + private final long _maxRetainedBytes; + private final int _frameOverhead; + private final boolean _reject; + + public RetainedContentLoaderFactory() + { + this(-1, -1, true); + } + + /** + * @param maxRetainedBytes the maximum number bytes to retain whilst eagerly loading, which + * includes the content bytes and any {@code frameOverhead} per chunk; + * or -1 for a heuristically determined value that will not increase memory commitment. + * @param frameOverhead the number of bytes to include in the estimated size per {@link Content.Chunk} to allow + * for framing overheads in the transport. Since the content is retained rather than copied, any + * framing data is also retained in the IO buffer. + * @param reject if {@code true}, then if {@code maxRetainBytes} is exceeded, the request is rejected with a + * {@link HttpStatus#PAYLOAD_TOO_LARGE_413} response. + */ + public RetainedContentLoaderFactory(long maxRetainedBytes, int frameOverhead, boolean reject) + { + _maxRetainedBytes = maxRetainedBytes; + _frameOverhead = frameOverhead; + _reject = reject; + } + + @Override + public String getApplicableMimeType() + { + return null; + } + + @Override + public ContentLoader newContentLoader(String contentType, String mimeType, Handler handler, Request request, Response response, Callback callback) + { + return new RetainedContentLoader(handler, request, response, callback, _maxRetainedBytes, _frameOverhead, _reject); + } + + /** + * Delay dispatch until all content or an effective buffer size is reached + */ + public static class RetainedContentLoader extends ContentLoader implements Invocable.Task + { + private final Deque _chunks = new ArrayDeque<>(); + private final long _maxRetainedBytes; + private final int _chunkOverhead; + private final boolean _reject; + private long _estimatedSize; + + /** + * @param handler The next handler + * @param request The delayed request + * @param response The delayed response + * @param callback The delayed callback + * @param maxRetainedBytes The maximum size to buffer before dispatching to the next handler; + * or -1 for a heuristically determined default + * @param frameOverhead The bytes to account for per chunk when calculating the size; or -1 for a heuristic. + * @param reject If {@code true} then requests are rejected if the content is not complete before maxRetainedBytes. + */ + public RetainedContentLoader(Handler handler, Request request, Response response, Callback callback, long maxRetainedBytes, int frameOverhead, boolean reject) + { + super(handler, request, response, callback); + _maxRetainedBytes = maxRetainedBytes < 0 + ? Math.max(1, request.getConnectionMetaData().getConnector().getConnectionFactory(HttpConnectionFactory.class).getInputBufferSize() - 1500) + : maxRetainedBytes; + _chunkOverhead = frameOverhead < 0 + ? (request.getConnectionMetaData().getHttpVersion() == HttpVersion.HTTP_2 ? 9 : 8) + : frameOverhead; + _reject = reject; + } + + @Override + protected void load() + { + read(false); + } + + protected void read(boolean execute) + { + while (true) + { + Content.Chunk chunk = super.getRequest().read(); + if (chunk == null) + { + getRequest().demand(this); + break; + } + + // retain the chunk in the queue + if (!_chunks.add(chunk)) + { + getCallback().failed(new IllegalStateException()); + break; + } + + // Estimated size is 8 byte framing overhead per chunk plus the chunk size + _estimatedSize += _chunkOverhead + chunk.remaining(); + + boolean oversize = _estimatedSize >= _maxRetainedBytes; + + if (_reject && oversize && !chunk.isLast()) + { + Response.writeError(getRequest(), getResponse(), getCallback(), HttpStatus.PAYLOAD_TOO_LARGE_413); + break; + } + + if (chunk.isLast() || oversize) + { + if (execute) + getRequest().getContext().execute(this::doHandle); + else + doHandle(); + break; + } + } + } + + @Override + public InvocationType getInvocationType() + { + return InvocationType.NON_BLOCKING; + } + + /** + * This is run when enough content has been received to dispatch to the next handler. + */ + public void run() + { + read(true); + } + + private void doHandle() + { + RewindChunksRequest request = new RewindChunksRequest(getRequest(), getCallback(), _chunks); + if (!handle(request, getResponse(), request)) + request.release(); + } + + private static class RewindChunksRequest extends Request.Wrapper implements Callback + { + private final Deque _chunks; + private final Callback _callback; + + public RewindChunksRequest(Request wrapped, Callback callback, Deque chunks) + { + super(wrapped); + _chunks = chunks; + _callback = callback; + } + + @Override + public Content.Chunk read() + { + if (_chunks.isEmpty()) + return super.read(); + return _chunks.removeFirst(); + } + + private void release() + { + _chunks.forEach(Content.Chunk::release); + _chunks.clear(); + } + + @Override + public void fail(Throwable failure, boolean last) + { + release(); + _callback.failed(failure); + } + + @Override + public void succeeded() + { + release(); + _callback.succeeded(); + } + } + } + } +} diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ThreadStarvationTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ThreadStarvationTest.java index 4f4dde261362..373c25fe579a 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ThreadStarvationTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ThreadStarvationTest.java @@ -36,7 +36,7 @@ import org.eclipse.jetty.http.MultiPartFormData; import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.server.handler.DelayedHandler; +import org.eclipse.jetty.server.handler.EagerContentHandler; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; @@ -154,7 +154,7 @@ private void prepareServer(Scenario scenario, Handler handler) if (scenario.delayed) { _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setDelayDispatchUntilContent(true); - _server.insertHandler(new DelayedHandler()); + _server.insertHandler(new EagerContentHandler()); } } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/EagerContentHandlerTest.java similarity index 82% rename from jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java rename to jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/EagerContentHandlerTest.java index 14946293daf6..959e52afa6e3 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DelayedHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/EagerContentHandlerTest.java @@ -54,7 +54,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -public class DelayedHandlerTest +public class EagerContentHandlerTest { private Server _server; private ServerConnector _connector; @@ -74,19 +74,11 @@ public void after() throws Exception } @Test - public void testNotDelayed() throws Exception + public void testNotEager() throws Exception { - DelayedHandler delayedHandler = new DelayedHandler() - { - @Override - protected DelayedProcess newDelayedProcess(boolean contentExpected, String contentType, MimeTypes.Type mimeType, Handler handler, Request request, Response response, Callback callback) - { - return null; - } - }; - - _server.setHandler(delayedHandler); - delayedHandler.setHandler(new HelloHandler()); + EagerContentHandler eagerContentHandler = new EagerContentHandler(new HelloHandler(), new EagerContentHandler.ContentLoaderFactory[]{}); + _server.setHandler(eagerContentHandler); + eagerContentHandler.setHandler(new HelloHandler()); _server.start(); try (Socket socket = new Socket("localhost", _connector.getLocalPort())) @@ -110,32 +102,40 @@ protected DelayedProcess newDelayedProcess(boolean contentExpected, String conte } @Test - public void testDelayed() throws Exception + public void testEager() throws Exception { Exchanger handleEx = new Exchanger<>(); - DelayedHandler delayedHandler = new DelayedHandler() - { - @Override - protected DelayedProcess newDelayedProcess(boolean contentExpected, String contentType, MimeTypes.Type mimeType, Handler handler, Request request, Response response, Callback callback) + EagerContentHandler eagerContentHandler = new EagerContentHandler(new HelloHandler(), + new EagerContentHandler.ContentLoaderFactory() { - return new DelayedProcess(handler, request, response, callback) + @Override + public String getApplicableMimeType() { - @Override - protected void delay() throws Exception + return null; + } + + @Override + public EagerContentHandler.ContentLoader newContentLoader(String contentType, String mimeType, Handler handler, Request request, Response response, Callback callback) + { + return new EagerContentHandler.ContentLoader(handler, request, response, callback) { - handleEx.exchange(this::process); - } - }; - } - }; + @Override + protected void load() throws Exception + { + handleEx.exchange(this::handle); + } + }; + } + }); - _server.setHandler(delayedHandler); + _server.setHandler(eagerContentHandler); CountDownLatch processing = new CountDownLatch(1); - delayedHandler.setHandler(new HelloHandler() + eagerContentHandler.setHandler(new HelloHandler() { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { + assertThat(Content.Source.asString(request), is("0123456789")); processing.countDown(); return super.handle(request, response, callback); } @@ -145,9 +145,11 @@ public boolean handle(Request request, Response response, Callback callback) thr try (Socket socket = new Socket("localhost", _connector.getLocalPort())) { String request = """ - GET / HTTP/1.1\r + POST / HTTP/1.1\r Host: localhost\r + Content-Length: 10\r \r + 0123456789\r """; OutputStream output = socket.getOutputStream(); output.write(request.getBytes(StandardCharsets.UTF_8)); @@ -171,13 +173,13 @@ public boolean handle(Request request, Response response, Callback callback) thr } @Test - public void testDelayedUntilContent() throws Exception + public void testEagerRetainedContent() throws Exception { - DelayedHandler delayedHandler = new DelayedHandler(); + EagerContentHandler eagerContentHandler = new EagerContentHandler(new EagerContentHandler.RetainedContentLoaderFactory(-1, -1, true)); - _server.setHandler(delayedHandler); + _server.setHandler(eagerContentHandler); CountDownLatch processing = new CountDownLatch(1); - delayedHandler.setHandler(new HelloHandler() + eagerContentHandler.setHandler(new HelloHandler() { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception @@ -188,8 +190,8 @@ public boolean handle(Request request, Response response, Callback callback) thr String stack = out.toString(StandardCharsets.ISO_8859_1); assertThat(stack, not(containsString("DemandContentCallback.succeeded"))); assertThat(stack, not(containsString("%s.%s".formatted( - DelayedHandler.UntilContentDelayedProcess.class.getSimpleName(), - DelayedHandler.UntilContentDelayedProcess.class.getDeclaredMethod("run").getName())))); + EagerContentHandler.RetainedContentLoaderFactory.RetainedContentLoader.class.getSimpleName(), + EagerContentHandler.RetainedContentLoaderFactory.RetainedContentLoader.class.getDeclaredMethod("run").getName())))); processing.countDown(); return super.handle(request, response, callback); @@ -226,15 +228,15 @@ public boolean handle(Request request, Response response, Callback callback) thr } @Test - public void testDelayedUntilContentInContext() throws Exception + public void testEagerContentInContext() throws Exception { ContextHandler context = new ContextHandler(); _server.setHandler(context); - DelayedHandler delayedHandler = new DelayedHandler(); - context.setHandler(delayedHandler); + EagerContentHandler eagerContentHandler = new EagerContentHandler(); + context.setHandler(eagerContentHandler); CountDownLatch processing = new CountDownLatch(1); - delayedHandler.setHandler(new HelloHandler() + eagerContentHandler.setHandler(new HelloHandler() { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception @@ -245,8 +247,8 @@ public boolean handle(Request request, Response response, Callback callback) thr String stack = out.toString(StandardCharsets.ISO_8859_1); assertThat(stack, not(containsString("DemandContentCallback.succeeded"))); assertThat(stack, not(containsString("%s.%s".formatted( - DelayedHandler.UntilContentDelayedProcess.class.getSimpleName(), - DelayedHandler.UntilContentDelayedProcess.class.getDeclaredMethod("run").getName())))); + EagerContentHandler.RetainedContentLoaderFactory.RetainedContentLoader.class.getSimpleName(), + EagerContentHandler.RetainedContentLoaderFactory.RetainedContentLoader.class.getDeclaredMethod("run").getName())))); // Check content String body = Content.Source.asString(request, StandardCharsets.ISO_8859_1); @@ -298,12 +300,12 @@ public boolean handle(Request request, Response response, Callback callback) thr } @Test - public void testNoDelayWithContent() throws Exception + public void testDirectCallWithContent() throws Exception { - DelayedHandler delayedHandler = new DelayedHandler(); + EagerContentHandler eagerContentHandler = new EagerContentHandler(); - _server.setHandler(delayedHandler); - delayedHandler.setHandler(new HelloHandler() + _server.setHandler(eagerContentHandler); + eagerContentHandler.setHandler(new HelloHandler() { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception @@ -313,7 +315,6 @@ public boolean handle(Request request, Response response, Callback callback) thr new Throwable().printStackTrace(new PrintStream(out)); String stack = out.toString(StandardCharsets.ISO_8859_1); assertThat(stack, containsString("org.eclipse.jetty.server.internal.HttpConnection.onFillable")); - assertThat(stack, containsString("org.eclipse.jetty.server.handler.DelayedHandler.handle")); // Check the content is available String content = Content.Source.asString(request); @@ -347,12 +348,12 @@ public boolean handle(Request request, Response response, Callback callback) thr } @Test - public void testNoDelayWithChunkedContent() throws Exception + public void testDirectCallWithChunkedContent() throws Exception { - DelayedHandler delayedHandler = new DelayedHandler(); + EagerContentHandler eagerContentHandler = new EagerContentHandler(); - _server.setHandler(delayedHandler); - delayedHandler.setHandler(new HelloHandler() + _server.setHandler(eagerContentHandler); + eagerContentHandler.setHandler(new HelloHandler() { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception @@ -362,7 +363,6 @@ public boolean handle(Request request, Response response, Callback callback) thr new Throwable().printStackTrace(new PrintStream(out)); String stack = out.toString(StandardCharsets.ISO_8859_1); assertThat(stack, containsString("org.eclipse.jetty.server.internal.HttpConnection.onFillable")); - assertThat(stack, containsString("org.eclipse.jetty.server.handler.DelayedHandler.handle")); // Check the content is available String content = Content.Source.asString(request); @@ -403,26 +403,32 @@ public boolean handle(Request request, Response response, Callback callback) thr } @Test - public void testDelayed404() throws Exception + public void testEager404() throws Exception { - DelayedHandler delayedHandler = new DelayedHandler() + EagerContentHandler eagerContentHandler = new EagerContentHandler(new EagerContentHandler.ContentLoaderFactory() { @Override - protected DelayedProcess newDelayedProcess(boolean contentExpected, String contentType, MimeTypes.Type mimeType, Handler handler, Request request, Response response, Callback callback) + public String getApplicableMimeType() { - return new DelayedProcess(handler, request, response, callback) + return null; + } + + @Override + public EagerContentHandler.ContentLoader newContentLoader(String contentType, String mimeType, Handler handler, Request request, Response response, Callback callback) + { + return new EagerContentHandler.ContentLoader(handler, request, response, callback) { @Override - protected void delay() + protected void load() { - getRequest().getContext().execute(this::process); + getRequest().getContext().execute(this::handle); } }; } - }; + }); - _server.setHandler(delayedHandler); - delayedHandler.setHandler(new Handler.Abstract() + _server.setHandler(eagerContentHandler); + eagerContentHandler.setHandler(new Handler.Abstract() { @Override public boolean handle(Request request, Response response, Callback callback) @@ -454,13 +460,13 @@ public boolean handle(Request request, Response response, Callback callback) } @Test - public void testDelayedFormFields() throws Exception + public void testEagerFormFields() throws Exception { - DelayedHandler delayedHandler = new DelayedHandler(); + EagerContentHandler eagerContentHandler = new EagerContentHandler(); - _server.setHandler(delayedHandler); + _server.setHandler(eagerContentHandler); CountDownLatch processing = new CountDownLatch(2); - delayedHandler.setHandler(new Handler.Abstract() + eagerContentHandler.setHandler(new Handler.Abstract() { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception @@ -525,22 +531,21 @@ public boolean handle(Request request, Response response, Callback callback) thr } @Test - public void testNoDelayFormFields() throws Exception + public void testDirectCallFormFields() throws Exception { - DelayedHandler delayedHandler = new DelayedHandler(); + EagerContentHandler eagerContentHandler = new EagerContentHandler(); - _server.setHandler(delayedHandler); - delayedHandler.setHandler(new Handler.Abstract() + _server.setHandler(eagerContentHandler); + eagerContentHandler.setHandler(new Handler.Abstract() { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - // Check that we are called directly from HttpConnection.onFillable via DelayedHandler.handle(). + // Check that we are called directly from HttpConnection.onFillable via EagerHandler.handle(). ByteArrayOutputStream out = new ByteArrayOutputStream(8192); new Throwable().printStackTrace(new PrintStream(out)); String stack = out.toString(StandardCharsets.ISO_8859_1); assertThat(stack, containsString("org.eclipse.jetty.server.internal.HttpConnection.onFillable")); - assertThat(stack, containsString("org.eclipse.jetty.server.handler.DelayedHandler.handle")); Fields fields = FormFields.getFields(request); Content.Sink.write(response, true, String.valueOf(fields), callback); @@ -575,12 +580,12 @@ public boolean handle(Request request, Response response, Callback callback) thr } @Test - public void testDelayedMultipart() throws Exception + public void testEagerMultipart() throws Exception { - DelayedHandler delayedHandler = new DelayedHandler(); + EagerContentHandler eagerContentHandler = new EagerContentHandler(); _server.setAttribute(MultiPartConfig.class.getName(), new MultiPartConfig.Builder().build()); - _server.setHandler(delayedHandler); - delayedHandler.setHandler(new Handler.Abstract() + _server.setHandler(eagerContentHandler); + eagerContentHandler.setHandler(new Handler.Abstract() { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/jmh/HandlerBenchmark.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/jmh/HandlerBenchmark.java index f193e0a597b0..05e12345f73a 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/jmh/HandlerBenchmark.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/jmh/HandlerBenchmark.java @@ -25,7 +25,7 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandlerCollection; -import org.eclipse.jetty.server.handler.DelayedHandler; +import org.eclipse.jetty.server.handler.EagerContentHandler; import org.eclipse.jetty.server.handler.EchoHandler; import org.eclipse.jetty.util.StringUtil; import org.openjdk.jmh.annotations.Benchmark; @@ -83,10 +83,10 @@ public static void setupServer() throws Exception { _server.addConnector(_connector); _connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().addCustomizer(new ForwardedRequestCustomizer()); - DelayedHandler delayedHandler = new DelayedHandler(); - _server.setHandler(delayedHandler); + EagerContentHandler eagerContentHandler = new EagerContentHandler(); + _server.setHandler(eagerContentHandler); ContextHandlerCollection contexts = new ContextHandlerCollection(); - delayedHandler.setHandler(contexts); + eagerContentHandler.setHandler(contexts); ContextHandler context = new ContextHandler("/ctx"); contexts.addHandler(context); EchoHandler echo = new EchoHandler(); diff --git a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/ThreadStarvationTest.java b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/ThreadStarvationTest.java index d7ce3f51eb3f..0b7dcffbd9a8 100644 --- a/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/ThreadStarvationTest.java +++ b/jetty-ee10/jetty-ee10-servlets/src/test/java/org/eclipse/jetty/ee10/servlets/ThreadStarvationTest.java @@ -55,7 +55,7 @@ import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.handler.DelayedHandler; +import org.eclipse.jetty.server.handler.EagerContentHandler; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; @@ -108,7 +108,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO if (delayed) { - _server.insertHandler(new DelayedHandler()); + _server.insertHandler(new EagerContentHandler()); connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setDelayDispatchUntilContent(true); } _server.start(); @@ -205,7 +205,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) if (delayed) { - _server.insertHandler(new DelayedHandler()); + _server.insertHandler(new EagerContentHandler()); connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setDelayDispatchUntilContent(true); } _server.start(); diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java index 770c53c9cad0..9829bcd809c9 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java @@ -55,7 +55,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; -import org.eclipse.jetty.server.handler.DelayedHandler; +import org.eclipse.jetty.server.handler.EagerContentHandler; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IO; @@ -133,7 +133,7 @@ public static void beforeClass() throws Exception http2.setIdleTimeout(5000); __server.addConnector(http2); - DelayedHandler delayedHandler = new DelayedHandler() + EagerContentHandler eagerContentHandler = new EagerContentHandler() { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception @@ -145,8 +145,8 @@ public boolean handle(Request request, Response response, Callback callback) thr }; ServletContextHandler context = new ServletContextHandler("/ctx"); - __server.setHandler(delayedHandler); - delayedHandler.setHandler(context); + __server.setHandler(eagerContentHandler); + eagerContentHandler.setHandler(context); ServletHolder holder = new ServletHolder(new TestServlet()); holder.setAsyncSupported(true); context.addServlet(holder, "/*"); diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/HugeResourceTest.java b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/HugeResourceTest.java index fda0e4dd1511..25af1bd0c5b3 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/HugeResourceTest.java +++ b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/HugeResourceTest.java @@ -58,7 +58,7 @@ import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.handler.DelayedHandler; +import org.eclipse.jetty.server.handler.EagerContentHandler; import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.BufferUtil; @@ -223,11 +223,11 @@ public void startServer() throws Exception ServletHolder holder = context.addServlet(MultipartServlet.class, "/multipart"); holder.getRegistration().setMultipartConfig(multipartConfig); - DelayedHandler delayedHandler = new DelayedHandler(); - server.setHandler(delayedHandler); + EagerContentHandler eagerContentHandler = new EagerContentHandler(); + server.setHandler(eagerContentHandler); httpConfig.setDelayDispatchUntilContent(false); - delayedHandler.setHandler(context); + eagerContentHandler.setHandler(context); server.start(); } diff --git a/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/ThreadStarvationTest.java b/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/ThreadStarvationTest.java index 17aa893ada53..5cf98a562546 100644 --- a/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/ThreadStarvationTest.java +++ b/jetty-ee11/jetty-ee11-servlets/src/test/java/org/eclipse/jetty/ee11/servlets/ThreadStarvationTest.java @@ -55,7 +55,7 @@ import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.handler.DelayedHandler; +import org.eclipse.jetty.server.handler.EagerContentHandler; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; @@ -108,7 +108,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO if (delayed) { - _server.insertHandler(new DelayedHandler()); + _server.insertHandler(new EagerContentHandler()); connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setDelayDispatchUntilContent(true); } _server.start(); @@ -205,7 +205,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) if (delayed) { - _server.insertHandler(new DelayedHandler()); + _server.insertHandler(new EagerContentHandler()); connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setDelayDispatchUntilContent(true); } _server.start(); diff --git a/jetty-ee11/jetty-ee11-webapp/src/test/java/org/eclipse/jetty/ee11/webapp/HugeResourceTest.java b/jetty-ee11/jetty-ee11-webapp/src/test/java/org/eclipse/jetty/ee11/webapp/HugeResourceTest.java index 97d3a92b12aa..8393885b5fee 100644 --- a/jetty-ee11/jetty-ee11-webapp/src/test/java/org/eclipse/jetty/ee11/webapp/HugeResourceTest.java +++ b/jetty-ee11/jetty-ee11-webapp/src/test/java/org/eclipse/jetty/ee11/webapp/HugeResourceTest.java @@ -58,7 +58,7 @@ import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.handler.DelayedHandler; +import org.eclipse.jetty.server.handler.EagerContentHandler; import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.BufferUtil; @@ -223,11 +223,11 @@ public void startServer() throws Exception ServletHolder holder = context.addServlet(MultipartServlet.class, "/multipart"); holder.getRegistration().setMultipartConfig(multipartConfig); - DelayedHandler delayedHandler = new DelayedHandler(); - server.setHandler(delayedHandler); + EagerContentHandler eagerContentHandler = new EagerContentHandler(); + server.setHandler(eagerContentHandler); httpConfig.setDelayDispatchUntilContent(false); - delayedHandler.setHandler(context); + eagerContentHandler.setHandler(context); server.start(); } From 2f06d2fd9c905c99d77a98c81a1117ac6f4562d8 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 12 Nov 2024 16:15:38 +1100 Subject: [PATCH 50/61] deprecated DelayedHandler --- .../jetty/server/handler/DelayedHandler.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java new file mode 100644 index 000000000000..74bef43ed217 --- /dev/null +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DelayedHandler.java @@ -0,0 +1,33 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.server.handler; + +import org.eclipse.jetty.server.Handler; + +/** + * @deprecated Use {@link EagerContentHandler} + */ +@Deprecated(forRemoval = true; since="12.1.0") +public class DelayedHandler extends EagerContentHandler +{ + DelayedHandler() + { + this(null); + } + + DelayedHandler(Handler handler) + { + super(handler); + } +} From 50265b4805945b6d9a6a8525a6c5b465756d2bd0 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 13 Nov 2024 09:20:06 +1100 Subject: [PATCH 51/61] Fixed several XmlConfiguration issues so that a Builder pattern can be used for setters with properties --- .../eclipse/jetty/http/MultiPartConfig.java | 6 +-- .../main/config/etc/jetty-eager-content.xml | 18 +++---- .../etc/jetty-eager-multipart-content.xml | 23 ++++++++- .../modules/eager-multipart-content.mod | 41 ++++++++++++++++ .../eclipse/jetty/xml/XmlConfiguration.java | 48 +++++++++++++++++-- .../jetty/xml/ExampleConfiguration.java | 7 +++ .../jetty/xml/XmlConfigurationTest.java | 2 + .../eclipse/jetty/xml/configureWithAttr.xml | 2 + .../jetty/xml/configureWithElements.xml | 2 + .../ee10/test/HttpInputIntegrationTest.java | 28 +++++------ 10 files changed, 147 insertions(+), 30 deletions(-) create mode 100644 jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartConfig.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartConfig.java index ba78ab5ca006..57d99321e0d9 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartConfig.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartConfig.java @@ -60,7 +60,7 @@ public Builder location(String location) location(ResourceFactory.root().newResource(location).getPath()); return this; } - + /** * @param location the directory where parts will be saved as files. */ @@ -80,7 +80,7 @@ public Builder maxParts(int maxParts) } /** - * @return the maximum size in bytes of the whole multipart content, or -1 for unlimited. + * @param maxSize the maximum size in bytes of the whole multipart content, or -1 for unlimited. */ public Builder maxSize(long maxSize) { @@ -89,7 +89,7 @@ public Builder maxSize(long maxSize) } /** - * @return the maximum part size in bytes, or -1 for unlimited. + * @param maxPartSize the maximum part size in bytes, or -1 for unlimited. */ public Builder maxPartSize(long maxPartSize) { diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml index 318481cbc3bf..b1d661c4e0ca 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml @@ -6,23 +6,23 @@ - + - - - + + + - + - - - - + + + + diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml index 33a16e8c50de..992630d59da4 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml @@ -1,7 +1,28 @@ - + + + + + + + + + + + + + + + + + + + + + + diff --git a/jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod b/jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod new file mode 100644 index 000000000000..b98311176e17 --- /dev/null +++ b/jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod @@ -0,0 +1,41 @@ + +[description] +Applies MultiPart configuration to the EagerContentHandler + +[tags] +server + +[before] +eager-content + +[xml] +etc/jetty-eager-multipart-content.xml + +[ini-template] +#tag::documentation[] + +## the directory where parts will be saved as files. +# jetty.eager.multipart.location=/tmp + +## the maximum number of parts that can be parsed from the multipart content, or -1 for unlimited. +# jetty.eager.multipart.maxParts= + +## the maximum size in bytes of the whole multipart content, or -1 for unlimited. +# jetty.eager.multipart.maxSize= + +## the maximum part size in bytes, or -1 for unlimited. +# jetty.eager.multipart.maxPartSize= + +## Sets the maximum size of a part in memory, after which it will be written as a file. +# jetty.eager.multipart.maxMemoryPartSize= + +## the max length of a Part header, in bytes, or -1 for unlimited length. +# jetty.eager.multipart.maxHeadersSize= + +## whether parts without a fileName may be stored as files. +# jetty.eager.multipart.useFilesForPartsWithoutFileName= + +## the compliance mode. +# jetty.eager.multipart.complianceMode=RFC7578 + +#end::documentation[] \ No newline at end of file diff --git a/jetty-core/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java b/jetty-core/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java index a07ccb5df461..9c45a486319d 100644 --- a/jetty-core/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java +++ b/jetty-core/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java @@ -614,7 +614,7 @@ private void set(Object obj, XmlParser.Node node) throws Exception String setter = "set" + name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1); String id = node.getAttribute("id"); String property = node.getAttribute("property"); - String propertyValue = null; + Object propertyValue = null; Class oClass = nodeClass(node); if (oClass == null) @@ -630,6 +630,7 @@ private void set(Object obj, XmlParser.Node node) throws Exception { // check that there is at least one setter or field that could have matched if (Arrays.stream(oClass.getMethods()).noneMatch(m -> m.getName().equals(setter)) && + Arrays.stream(oClass.getMethods()).noneMatch(m -> m.getName().equals(name)) && Arrays.stream(oClass.getFields()).filter(f -> Modifier.isPublic(f.getModifiers())).noneMatch(f -> f.getName().equals(name))) { NoSuchMethodException e = new NoSuchMethodException(String.format("No method '%s' on %s", setter, oClass.getName())); @@ -639,6 +640,8 @@ private void set(Object obj, XmlParser.Node node) throws Exception // otherwise it is a noop return; } + + propertyValue = toType(propertyValue, node.getAttribute("type")); } Object value = value(obj, node); @@ -676,8 +679,8 @@ private void set(Object obj, XmlParser.Node node) throws Exception try { Field type = vClass.getField("TYPE"); - vClass = (Class)type.get(null); - Method set = oClass.getMethod(setter, vClass); + Class nClass = (Class)type.get(null); + Method set = oClass.getMethod(setter, nClass); invokeMethod(set, obj, arg); return; } @@ -687,6 +690,40 @@ private void set(Object obj, XmlParser.Node node) throws Exception errors.add(e); } + // Try a builder + try + { + Method builder = oClass.getMethod(name, vClass); + if (builder.getReturnType() == oClass) + { + invokeMethod(builder, obj, arg); + return; + } + } + catch (IllegalArgumentException | IllegalAccessException | NoSuchMethodException e) + { + LOG.trace("IGNORED", e); + errors.add(e); + } + + // Try for native builder + try + { + Field type = vClass.getField("TYPE"); + Class nClass = (Class)type.get(null); + Method builder = oClass.getMethod(name, nClass); + if (builder.getReturnType() == oClass) + { + invokeMethod(builder, obj, arg); + return; + } + } + catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException | NoSuchMethodException e) + { + LOG.trace("IGNORED", e); + errors.add(e); + } + // Try a field try { @@ -1509,6 +1546,11 @@ private Object value(Object obj, XmlParser.Node node) throws Exception } } + return toType(value, type); + } + + private Object toType(Object value, String type) throws Exception + { // No value if (value == null) { diff --git a/jetty-core/jetty-xml/src/test/java/org/eclipse/jetty/xml/ExampleConfiguration.java b/jetty-core/jetty-xml/src/test/java/org/eclipse/jetty/xml/ExampleConfiguration.java index 6ee1fbc29c13..b1f3babb25c8 100644 --- a/jetty-core/jetty-xml/src/test/java/org/eclipse/jetty/xml/ExampleConfiguration.java +++ b/jetty-core/jetty-xml/src/test/java/org/eclipse/jetty/xml/ExampleConfiguration.java @@ -51,6 +51,7 @@ public class ExampleConfiguration extends HashMap private ConstructorArgTestClass constructorArgTestClass; public Map map; public Double number; + public String builder; public interface TestInterface { @@ -214,4 +215,10 @@ public void setMap(Map map) { this.map = map; } + + public ExampleConfiguration builder(String value) + { + this.builder = value; + return this; + } } diff --git a/jetty-core/jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlConfigurationTest.java b/jetty-core/jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlConfigurationTest.java index a2053277ae18..205d36f15f30 100644 --- a/jetty-core/jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlConfigurationTest.java +++ b/jetty-core/jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlConfigurationTest.java @@ -219,6 +219,8 @@ public void testPassedObject(String configure) throws Exception assertThat(concurrentMap, instanceOf(ConcurrentMap.class)); assertEquals(concurrentMap.get("KEY"), "ITEM"); + assertThat(tc.builder, is("builder")); + if ("org/eclipse/jetty/xml/configureWithElements.xml".equals(configure)) { System.err.println("Static call with TestImpl: " + ExampleConfiguration.calledWithClass); diff --git a/jetty-core/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithAttr.xml b/jetty-core/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithAttr.xml index 90a5e604c028..d7d249e8cc3e 100644 --- a/jetty-core/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithAttr.xml +++ b/jetty-core/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithAttr.xml @@ -141,6 +141,8 @@ + builder + diff --git a/jetty-core/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithElements.xml b/jetty-core/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithElements.xml index ec40d0ba6b00..d492e2d35699 100644 --- a/jetty-core/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithElements.xml +++ b/jetty-core/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithElements.xml @@ -197,6 +197,8 @@ + builder + diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java index 9829bcd809c9..4bb29bc15757 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/HttpInputIntegrationTest.java @@ -87,7 +87,7 @@ enum Mode private static HttpConfiguration __config; private static SslContextFactory.Server __sslContextFactory; private static ArrayByteBufferPool.Tracking __bufferPool; - private static final AtomicBoolean __delayHandler = new AtomicBoolean(); + private static final AtomicBoolean __eagerHandler = new AtomicBoolean(); @BeforeAll public static void beforeClass() throws Exception @@ -136,10 +136,10 @@ public static void beforeClass() throws Exception EagerContentHandler eagerContentHandler = new EagerContentHandler() { @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception + public boolean onConditionsMet(Request request, Response response, Callback callback) throws Exception { - if (__delayHandler.get()) - return super.handle(request, response, callback); + if (__eagerHandler.get()) + return super.onConditionsMet(request, response, callback); return getHandler().handle(request, response, callback); } }; @@ -304,9 +304,9 @@ private static void runMode(Mode mode, ServletContextRequest request, Runnable t @MethodSource("scenarios") public void testOne(Scenario scenario) throws Exception { - __delayHandler.set(scenario._delayHandler); + __eagerHandler.set(scenario._eagerHandler); TestClient client = scenario._client.getDeclaredConstructor().newInstance(); - String response = client.send("/ctx/test?mode=" + scenario._mode, 50, scenario._delay, scenario._length, scenario._send); + String response = client.send("/ctx/test?mode=" + scenario._mode, 50, scenario._eager, scenario._length, scenario._send); int sum = 0; for (String s : scenario._send) @@ -328,7 +328,7 @@ public void testOne(Scenario scenario) throws Exception @MethodSource("scenarios") public void testStress(Scenario scenario) throws Exception { - __delayHandler.set(scenario._delayHandler); + __eagerHandler.set(scenario._eagerHandler); int sum = 0; for (String s : scenario._send) { @@ -352,7 +352,7 @@ public void testStress(Scenario scenario) throws Exception TestClient client = scenario._client.getDeclaredConstructor().newInstance(); for (int j = 0; j < loops; j++) { - String response = client.send("/ctx/test?mode=" + scenario._mode, 10, scenario._delay, scenario._length, scenario._send); + String response = client.send("/ctx/test?mode=" + scenario._mode, 10, scenario._eager, scenario._length, scenario._send); assertTrue(response.startsWith("HTTP"), response); assertTrue(response.contains(" " + scenario._status + " "), response); assertTrue(response.contains("read=" + scenario._read), response); @@ -684,19 +684,19 @@ public static class Scenario { private final Class _client; private final Mode _mode; - private final boolean _delayHandler; - private final Boolean _delay; + private final boolean _eagerHandler; + private final Boolean _eager; private final int _status; private final int _read; private final int _length; private final List _send; - public Scenario(Class client, Mode mode, boolean delayHandler, Boolean delay, int status, int read, int length, String... send) + public Scenario(Class client, Mode mode, boolean eagerHandler, Boolean eager, int status, int read, int length, String... send) { _client = client; _mode = mode; - _delayHandler = delayHandler; - _delay = delay; + _eagerHandler = eagerHandler; + _eager = eager; _status = status; _read = read; _length = length; @@ -707,7 +707,7 @@ public Scenario(Class client, Mode mode, boolean delayHand public String toString() { return String.format("c=%s, m=%s, delayInFrame=%s content-length:%d expect=%d read=%d content:%s%n", - _client.getSimpleName(), _mode, _delay, _length, _status, _read, _send); + _client.getSimpleName(), _mode, _eager, _length, _status, _read, _send); } } } From 2b0fe1bd7b052f0ee885ebe62fa0bf39af983dde Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 13 Nov 2024 18:17:19 +1100 Subject: [PATCH 52/61] Fixed several XmlConfiguration issues so that a Builder pattern can be used for setters with properties --- .../src/main/config/etc/jetty-eager-multipart-content.xml | 8 +++++--- .../src/main/config/modules/delay-until-content.mod | 2 +- .../src/main/config/modules/eager-content.mod | 2 +- jetty-core/jetty-server/src/main/java/module-info.java | 1 - 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml index 992630d59da4..d619dc85edce 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml @@ -4,9 +4,11 @@ - - - + + + + diff --git a/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod b/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod index 7c40cb958539..ffe2c1cb85b7 100644 --- a/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/delay-until-content.mod @@ -22,4 +22,4 @@ etc/jetty-delayed.xml #tag::documentation[] ## The maximum bytes to retain whilst delaying content; or 0 for no delay; or -1 (default) for a default value. # jetty.delayed.maxRetainedContentBytes=-1 -#end::documentation[] \ No newline at end of file +#end::documentation[] diff --git a/jetty-core/jetty-server/src/main/config/modules/eager-content.mod b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod index 070f51fd3e55..ed67d20db488 100644 --- a/jetty-core/jetty-server/src/main/config/modules/eager-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod @@ -40,4 +40,4 @@ etc/jetty-eager-content.xml ## If requests should be rejected if they exceed the maxRetainedBytes # jetty.eager.retained.reject=false -#end::documentation[] \ No newline at end of file +#end::documentation[] diff --git a/jetty-core/jetty-server/src/main/java/module-info.java b/jetty-core/jetty-server/src/main/java/module-info.java index 7a9dd70a7b55..8f3ef3e0f50a 100644 --- a/jetty-core/jetty-server/src/main/java/module-info.java +++ b/jetty-core/jetty-server/src/main/java/module-info.java @@ -18,7 +18,6 @@ // Only required if using JMX. requires static org.eclipse.jetty.jmx; - requires java.desktop; exports org.eclipse.jetty.server; exports org.eclipse.jetty.server.handler; From e391c0b78f749444d612a87153146df39130b668 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 13 Nov 2024 22:06:57 +1100 Subject: [PATCH 53/61] Updates from review --- .../main/config/etc/jetty-eager-content.xml | 4 +- .../etc/jetty-eager-multipart-content.xml | 3 +- .../src/main/config/modules/eager-content.mod | 6 +- .../modules/eager-multipart-content.mod | 16 ++--- .../jetty/server/HttpConfiguration.java | 4 +- .../server/handler/EagerContentHandler.java | 65 +++++++++-------- .../tests/distribution/DistributionTests.java | 70 +++++++++++++++++++ 7 files changed, 118 insertions(+), 50 deletions(-) diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml index b1d661c4e0ca..dba4a0c20aa9 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml @@ -21,8 +21,8 @@ - - + + diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml index d619dc85edce..6291e9cbeddf 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml @@ -6,7 +6,7 @@ - @@ -26,7 +26,6 @@ - diff --git a/jetty-core/jetty-server/src/main/config/modules/eager-content.mod b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod index ed67d20db488..b6d0929e4b51 100644 --- a/jetty-core/jetty-server/src/main/config/modules/eager-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod @@ -1,4 +1,3 @@ - [description] Applies the EagerContentHandler to the entire server #tag::description[] @@ -35,9 +34,8 @@ etc/jetty-eager-content.xml # jetty.eager.retained.maxRetainedBytes=-1 ## The frame overhead to use when calculating the retained bytes or -1 for a default -# jetty.eager.retained.frameOverhead=-1 +# jetty.eager.retained.framingOverhead=-1 ## If requests should be rejected if they exceed the maxRetainedBytes -# jetty.eager.retained.reject=false - +# jetty.eager.retained.rejectWhenExceeded=false #end::documentation[] diff --git a/jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod b/jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod index b98311176e17..d65868d1da8c 100644 --- a/jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod @@ -13,29 +13,27 @@ etc/jetty-eager-multipart-content.xml [ini-template] #tag::documentation[] - ## the directory where parts will be saved as files. # jetty.eager.multipart.location=/tmp -## the maximum number of parts that can be parsed from the multipart content, or -1 for unlimited. +## The maximum number of parts that can be parsed from the multipart content, or -1 for unlimited. # jetty.eager.multipart.maxParts= -## the maximum size in bytes of the whole multipart content, or -1 for unlimited. +## The maximum size in bytes of the whole multipart content, or -1 for unlimited. # jetty.eager.multipart.maxSize= -## the maximum part size in bytes, or -1 for unlimited. +## The maximum part size in bytes, or -1 for unlimited. # jetty.eager.multipart.maxPartSize= -## Sets the maximum size of a part in memory, after which it will be written as a file. +## The maximum size of a part in memory, after which it will be written as a file. # jetty.eager.multipart.maxMemoryPartSize= -## the max length of a Part header, in bytes, or -1 for unlimited length. +## The max length of a Part header, in bytes, or -1 for unlimited length. # jetty.eager.multipart.maxHeadersSize= -## whether parts without a fileName may be stored as files. +## Whether parts without a fileName are stored as files. # jetty.eager.multipart.useFilesForPartsWithoutFileName= -## the compliance mode. +## The MultiPart compliance mode. # jetty.eager.multipart.complianceMode=RFC7578 - #end::documentation[] \ No newline at end of file diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java index 7d7c1041559b..a1aba0e46695 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java @@ -90,7 +90,7 @@ public class HttpConfiguration implements Dumpable private HostPort _serverAuthority; private SocketAddress _localAddress; private int _maxUnconsumedRequestContentReads = 16; - private int _minInputBufferSpace = 1024; + private int _minInputBufferSpace = 1500; /** *

An interface that allows a request object to be customized @@ -355,7 +355,7 @@ public boolean getSendDateHeader() /** * Set if true, delays the application dispatch until content is available (defaults to true). * @param delay if true, delays the application dispatch until content is available (defaults to true) - * @deprecated Use the DelayedHandler instead. + * @deprecated Use {@link org.eclipse.jetty.server.handler.EagerContentHandler} instead. */ @Deprecated (forRemoval = true, since = "12.1.0") public void setDelayDispatchUntilContent(boolean delay) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EagerContentHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EagerContentHandler.java index 7769dcf65b10..9e2dc8cce441 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EagerContentHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EagerContentHandler.java @@ -64,7 +64,7 @@ public class EagerContentHandler extends ConditionalHandler.ElseNext private final ContentLoaderFactory _defaultFactory; /** - * Construct an {@code EagerContentHandler} with default {@link ContentLoaderFactory EagerContentFactories} + * Construct an {@code EagerContentHandler} with the default {@link ContentLoaderFactory} set */ public EagerContentHandler() { @@ -72,7 +72,7 @@ public EagerContentHandler() } /** - * Construct an {@code EagerContentHandler} with default {@link ContentLoaderFactory EagerContentFactories} + * Construct an {@code EagerContentHandler} with the default {@link ContentLoaderFactory} set * @param handler The next handler (also can be set with {@link #setHandler(Handler)} */ public EagerContentHandler(Handler handler) @@ -81,7 +81,7 @@ public EagerContentHandler(Handler handler) } /** - * Construct an {@code EagerContentHandler} with specific {@link ContentLoaderFactory EagerContentFactories} + * Construct an {@code EagerContentHandler} with the specific {@link ContentLoaderFactory} instances * @param factories The {@link ContentLoaderFactory} instances used to eagerly load content. */ public EagerContentHandler(ContentLoaderFactory... factories) @@ -90,7 +90,7 @@ public EagerContentHandler(ContentLoaderFactory... factories) } /** - * Construct an {@code EagerContentHandler} with specific {@link ContentLoaderFactory EagerContentFactories} + * Construct an {@code EagerContentHandler} with the specific {@link ContentLoaderFactory} instances * @param handler The next handler (also can be set with {@link #setHandler(Handler)} * @param factories The {@link ContentLoaderFactory} instances used to eagerly load content. */ @@ -154,11 +154,11 @@ protected boolean onConditionsMet(Request request, Response response, Callback c if (factory == null) return next.handle(request, response, callback); - ContentLoader process = factory.newContentLoader(contentType, mimeType, next, request, response, callback); - if (process == null) + ContentLoader contentLoader = factory.newContentLoader(contentType, mimeType, next, request, response, callback); + if (contentLoader == null) return next.handle(request, response, callback); - process.load(); + contentLoader.load(); return true; } @@ -185,7 +185,7 @@ public interface ContentLoaderFactory } /** - * An Eager Content processor, created by a {@link ContentLoaderFactory} to asynchronous load content from a {@link Request} + * An eager content processor, created by a {@link ContentLoaderFactory} to asynchronous load content from a {@link Request} * before calling the {@link Handler#handle(Request, Response, Callback)} method of the passed {@link Handler}. */ public abstract static class ContentLoader @@ -228,23 +228,21 @@ protected void handle() handle(getRequest(), getResponse(), getCallback()); } - protected boolean handle(Request request, Response response, Callback callback) + protected void handle(Request request, Response response, Callback callback) { try { if (getHandler().handle(request, response, callback)) - return true; + return; // The handle was rejected, so write the error using the original potentially unwrapped request/response/callback - Response.writeError(getRequest(), getResponse(), getCallback(), HttpStatus.NOT_FOUND_404); + Response.writeError(request, response, callback, HttpStatus.NOT_FOUND_404); } catch (Throwable t) { // The handle failed, so write the error using the original potentially unwrapped request/response/callback - Response.writeError(getRequest(), getResponse(), getCallback(), t); + Response.writeError(request, response, callback, t); } - // return false to indicate the passed request/response/callback were not used. - return false; } /** @@ -480,8 +478,8 @@ public static class RetainedContentLoader extends ContentLoader implements Invoc { private final Deque _chunks = new ArrayDeque<>(); private final long _maxRetainedBytes; - private final int _chunkOverhead; - private final boolean _reject; + private final int _framingOverhead; + private final boolean _rejectWhenExceeded; private long _estimatedSize; /** @@ -491,19 +489,19 @@ public static class RetainedContentLoader extends ContentLoader implements Invoc * @param callback The delayed callback * @param maxRetainedBytes The maximum size to buffer before dispatching to the next handler; * or -1 for a heuristically determined default - * @param frameOverhead The bytes to account for per chunk when calculating the size; or -1 for a heuristic. - * @param reject If {@code true} then requests are rejected if the content is not complete before maxRetainedBytes. + * @param framingOverhead The bytes to account for per chunk when calculating the size; or -1 for a heuristic. + * @param rejectWhenExceeded If {@code true} then requests are rejected if the content is not complete before maxRetainedBytes. */ - public RetainedContentLoader(Handler handler, Request request, Response response, Callback callback, long maxRetainedBytes, int frameOverhead, boolean reject) + public RetainedContentLoader(Handler handler, Request request, Response response, Callback callback, long maxRetainedBytes, int framingOverhead, boolean rejectWhenExceeded) { super(handler, request, response, callback); _maxRetainedBytes = maxRetainedBytes < 0 ? Math.max(1, request.getConnectionMetaData().getConnector().getConnectionFactory(HttpConnectionFactory.class).getInputBufferSize() - 1500) : maxRetainedBytes; - _chunkOverhead = frameOverhead < 0 - ? (request.getConnectionMetaData().getHttpVersion() == HttpVersion.HTTP_2 ? 9 : 8) - : frameOverhead; - _reject = reject; + _framingOverhead = framingOverhead < 0 + ? (request.getConnectionMetaData().getHttpVersion().getVersion() <= HttpVersion.HTTP_1_1.getVersion() ? 8 : 9) + : framingOverhead; + _rejectWhenExceeded = rejectWhenExceeded; } @Override @@ -531,11 +529,11 @@ protected void read(boolean execute) } // Estimated size is 8 byte framing overhead per chunk plus the chunk size - _estimatedSize += _chunkOverhead + chunk.remaining(); + _estimatedSize += _framingOverhead + chunk.remaining(); boolean oversize = _estimatedSize >= _maxRetainedBytes; - if (_reject && oversize && !chunk.isLast()) + if (_rejectWhenExceeded && oversize && !chunk.isLast()) { Response.writeError(getRequest(), getResponse(), getCallback(), HttpStatus.PAYLOAD_TOO_LARGE_413); break; @@ -569,8 +567,7 @@ public void run() private void doHandle() { RewindChunksRequest request = new RewindChunksRequest(getRequest(), getCallback(), _chunks); - if (!handle(request, getResponse(), request)) - request.release(); + handle(request, getResponse(), request); } private static class RewindChunksRequest extends Request.Wrapper implements Callback @@ -585,6 +582,12 @@ public RewindChunksRequest(Request wrapped, Callback callback, Deque args = List.of( + "jetty.delayed.maxRetainedContentBytes=-1", + "jetty.eager.form.maxFields=100", + "jetty.eager.form.maxLength=20000", + "jetty.eager.retained.maxRetainedBytes=8192", + "jetty.eager.retained.framingOverhead=10", + "jetty.eager.retained.rejectWhenExceeded=false", + "jetty.eager.multipart.location=/tmp", + "jetty.eager.multipart.maxParts=100", + "jetty.eager.multipart.maxSize=100000", + "jetty.eager.multipart.maxPartSize=32768", + "jetty.eager.multipart.maxMemoryPartSize=2048", + "jetty.eager.multipart.maxHeadersSize=4096", + "jetty.eager.multipart.useFilesForPartsWithoutFileName=true", + "jetty.eager.multipart.complianceMode=RFC7578", + "jetty.http.port=" + httpPort); + try (JettyHomeTester.Run run2 = distribution.start(args); AsyncRequestContent content = new AsyncRequestContent()) + { + assertTrue(run2.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS)); + startHttpClient(); + + CountDownLatch latch = new CountDownLatch(1); + client.newRequest("http://localhost:" + httpPort + "/ee11-test/dump") + .method("POST") + .body(content) + .timeout(15, TimeUnit.SECONDS) + .send(result -> + { + assertThat(result.getResponse().getStatus(), is(HttpStatus.OK_200)); + latch.countDown(); + }); + + for (int i = 1; i < 10; i++) + { + Callback.Completable complete = new Callback.Completable(); + content.write(false, BufferUtil.toBuffer(Integer.toString(i)), complete); + content.flush(); + Thread.sleep(10); + complete.get(5, TimeUnit.SECONDS); + } + + Callback.Completable end = new Callback.Completable(); + content.write(true, BufferUtil.toBuffer("x"), end); + content.close(); + end.get(5, TimeUnit.SECONDS); + + assertTrue(latch.await(15, TimeUnit.SECONDS)); + } + } + } } From da36c869b1b812deb5f220872cc3d9b0a3a4a406 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 14 Nov 2024 07:18:42 +1100 Subject: [PATCH 54/61] Updates from review --- .../src/main/config/modules/eager-content.mod | 5 ++--- .../jetty/server/handler/EagerContentHandler.java | 12 ++++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/jetty-core/jetty-server/src/main/config/modules/eager-content.mod b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod index b6d0929e4b51..d74bb21d8a3c 100644 --- a/jetty-core/jetty-server/src/main/config/modules/eager-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod @@ -21,9 +21,6 @@ etc/jetty-eager-content.xml [ini-template] #tag::documentation[] -## The maximum bytes to retain whilst delaying content; or 0 for no delay; or -1 (default) for a default value. -# jetty.delayed.maxRetainedContentBytes=-1 - ## The maximum number of FormFields to be eagerly loaded or -1 for a default # jetty.eager.form.maxFields=-1 @@ -38,4 +35,6 @@ etc/jetty-eager-content.xml ## If requests should be rejected if they exceed the maxRetainedBytes # jetty.eager.retained.rejectWhenExceeded=false + +## For eager multipart configuration use eager-multipart-content module #end::documentation[] diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EagerContentHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EagerContentHandler.java index 9e2dc8cce441..1ab28bee4868 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EagerContentHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/EagerContentHandler.java @@ -434,7 +434,7 @@ public InvocationType getInvocationType() public static class RetainedContentLoaderFactory implements ContentLoaderFactory { private final long _maxRetainedBytes; - private final int _frameOverhead; + private final int _framingOverhead; private final boolean _reject; public RetainedContentLoaderFactory() @@ -444,18 +444,18 @@ public RetainedContentLoaderFactory() /** * @param maxRetainedBytes the maximum number bytes to retain whilst eagerly loading, which - * includes the content bytes and any {@code frameOverhead} per chunk; + * includes the content bytes and any {@code framingOverhead} per chunk; * or -1 for a heuristically determined value that will not increase memory commitment. - * @param frameOverhead the number of bytes to include in the estimated size per {@link Content.Chunk} to allow + * @param framingOverhead the number of bytes to include in the estimated size per {@link Content.Chunk} to allow * for framing overheads in the transport. Since the content is retained rather than copied, any * framing data is also retained in the IO buffer. * @param reject if {@code true}, then if {@code maxRetainBytes} is exceeded, the request is rejected with a * {@link HttpStatus#PAYLOAD_TOO_LARGE_413} response. */ - public RetainedContentLoaderFactory(long maxRetainedBytes, int frameOverhead, boolean reject) + public RetainedContentLoaderFactory(long maxRetainedBytes, int framingOverhead, boolean reject) { _maxRetainedBytes = maxRetainedBytes; - _frameOverhead = frameOverhead; + _framingOverhead = framingOverhead; _reject = reject; } @@ -468,7 +468,7 @@ public String getApplicableMimeType() @Override public ContentLoader newContentLoader(String contentType, String mimeType, Handler handler, Request request, Response response, Callback callback) { - return new RetainedContentLoader(handler, request, response, callback, _maxRetainedBytes, _frameOverhead, _reject); + return new RetainedContentLoader(handler, request, response, callback, _maxRetainedBytes, _framingOverhead, _reject); } /** From 6bfc8853bc329596a2e77bce33be40ebd915dd0e Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Wed, 13 Nov 2024 21:30:12 +0100 Subject: [PATCH 55/61] Added distribution test cases for EagerContentHandler modules. Signed-off-by: Simone Bordet --- .../jetty/client/FormRequestContent.java | 2 +- .../deploy/providers/ContextProvider.java | 4 - .../src/main/config/modules/eager-content.mod | 4 +- .../modules/eager-multipart-content.mod | 15 +- .../jetty/demo/simple/EchoServlet.java | 64 +++++ .../src/main/webapp/WEB-INF/web.xml | 9 + .../jetty/ee10/servlet/ServletApiRequest.java | 3 - .../jetty/ee11/servlet/ServletApiRequest.java | 3 - .../tests/distribution/DistributionTests.java | 222 ++++++++++++++---- 9 files changed, 257 insertions(+), 69 deletions(-) create mode 100644 jetty-demos/jetty-servlet5-demos/jetty-servlet5-demo-simple-webapp/src/main/java/org/eclipse/jetty/demo/simple/EchoServlet.java diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/FormRequestContent.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/FormRequestContent.java index ee930a737cb3..2c22f5baa2a3 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/FormRequestContent.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/FormRequestContent.java @@ -32,7 +32,7 @@ public FormRequestContent(Fields fields) public FormRequestContent(Fields fields, Charset charset) { - super("application/x-www-form-urlencoded", convert(fields, charset), charset); + super("application/x-www-form-urlencoded;charset=" + charset.name(), convert(fields, charset), charset); } public static String convert(Fields fields) diff --git a/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java b/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java index db604b422eed..dc2c0aecbe55 100644 --- a/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java +++ b/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java @@ -24,7 +24,6 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -305,10 +304,7 @@ else if (Supplier.class.isAssignableFrom(context.getClass())) initializeContextPath(contextHandler, path); if (Files.isDirectory(path)) - { contextHandler.setBaseResource(ResourceFactory.of(this).newResource(path)); - System.err.println("SET BASE RESOURCE to " + path); - } //TODO think of better way of doing this //pass through properties as attributes directly diff --git a/jetty-core/jetty-server/src/main/config/modules/eager-content.mod b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod index d74bb21d8a3c..62f48d8d0375 100644 --- a/jetty-core/jetty-server/src/main/config/modules/eager-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod @@ -2,8 +2,8 @@ Applies the EagerContentHandler to the entire server #tag::description[] The EagerContentHandler can eagerly load content asynchronously before calling the next handler. -Typically this handler is deployed before an application that uses blocking IO to read the request body and if deployed -after this handler, the application will never (or seldom) block for request content. +Typically, this handler is deployed before an application that uses blocking IO to read the request body +and if deployed after this handler, the application will never (or rarely) block for request content. This gives many of the benefits of asynchronous IO without the need to write an asynchronous application. #end::description[] diff --git a/jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod b/jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod index d65868d1da8c..f247c597ebea 100644 --- a/jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod @@ -1,4 +1,3 @@ - [description] Applies MultiPart configuration to the EagerContentHandler @@ -17,23 +16,23 @@ etc/jetty-eager-multipart-content.xml # jetty.eager.multipart.location=/tmp ## The maximum number of parts that can be parsed from the multipart content, or -1 for unlimited. -# jetty.eager.multipart.maxParts= +# jetty.eager.multipart.maxParts=100 ## The maximum size in bytes of the whole multipart content, or -1 for unlimited. -# jetty.eager.multipart.maxSize= +# jetty.eager.multipart.maxSize=52428800 ## The maximum part size in bytes, or -1 for unlimited. -# jetty.eager.multipart.maxPartSize= +# jetty.eager.multipart.maxPartSize=10485760 ## The maximum size of a part in memory, after which it will be written as a file. -# jetty.eager.multipart.maxMemoryPartSize= +# jetty.eager.multipart.maxMemoryPartSize=1024 ## The max length of a Part header, in bytes, or -1 for unlimited length. -# jetty.eager.multipart.maxHeadersSize= +# jetty.eager.multipart.maxHeadersSize=8192 ## Whether parts without a fileName are stored as files. -# jetty.eager.multipart.useFilesForPartsWithoutFileName= +# jetty.eager.multipart.useFilesForPartsWithoutFileName=true ## The MultiPart compliance mode. # jetty.eager.multipart.complianceMode=RFC7578 -#end::documentation[] \ No newline at end of file +#end::documentation[] diff --git a/jetty-demos/jetty-servlet5-demos/jetty-servlet5-demo-simple-webapp/src/main/java/org/eclipse/jetty/demo/simple/EchoServlet.java b/jetty-demos/jetty-servlet5-demos/jetty-servlet5-demo-simple-webapp/src/main/java/org/eclipse/jetty/demo/simple/EchoServlet.java new file mode 100644 index 000000000000..db8ff546c45e --- /dev/null +++ b/jetty-demos/jetty-servlet5-demos/jetty-servlet5-demo-simple-webapp/src/main/java/org/eclipse/jetty/demo/simple/EchoServlet.java @@ -0,0 +1,64 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.demo.simple; + +import java.io.IOException; +import java.util.stream.Collectors; + +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.annotation.MultipartConfig; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@MultipartConfig +public class EchoServlet extends HttpServlet +{ + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + response.setContentType(request.getContentType()); + ServletOutputStream output = response.getOutputStream(); + String pathInfo = request.getPathInfo(); + switch (pathInfo) + { + case "/form" -> + { + String content = request.getParameterMap().entrySet().stream() + .map(e -> "%s=%s".formatted(e.getKey(), String.join(", ", e.getValue()))) + .collect(Collectors.joining("&")); + output.print(content); + } + case "/multipart" -> + { + MultipartConfigElement config = new MultipartConfigElement(""); + request.setAttribute(MultipartConfigElement.class.getName(), config); + + String content = request.getParts().stream() + .map(part -> "name=%s&length=%d".formatted(part.getName(), part.getSize())) + .collect(Collectors.joining(",")); + output.print(content); + } + default -> + { + ServletInputStream input = request.getInputStream(); + response.setContentLengthLong(request.getContentLengthLong()); + input.transferTo(output); + } + } + } +} diff --git a/jetty-demos/jetty-servlet5-demos/jetty-servlet5-demo-simple-webapp/src/main/webapp/WEB-INF/web.xml b/jetty-demos/jetty-servlet5-demos/jetty-servlet5-demo-simple-webapp/src/main/webapp/WEB-INF/web.xml index 0d5de707679c..4361e90df459 100644 --- a/jetty-demos/jetty-servlet5-demos/jetty-servlet5-demo-simple-webapp/src/main/webapp/WEB-INF/web.xml +++ b/jetty-demos/jetty-servlet5-demos/jetty-servlet5-demo-simple-webapp/src/main/webapp/WEB-INF/web.xml @@ -21,4 +21,13 @@ /hello/* + + echo + org.eclipse.jetty.demo.simple.EchoServlet + + + echo + /echo/* + + diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java index 462716e81cd1..71d4027a024e 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java @@ -1283,7 +1283,6 @@ private void extractContentParameters() throws BadMessageException } catch (IllegalStateException | IllegalArgumentException | CompletionException e) { - LOG.warn(e.toString()); throw new BadMessageException("Unable to parse form content", e); } } @@ -1321,7 +1320,6 @@ else if (MimeTypes.Type.MULTIPART_FORM_DATA.is(baseType) && } catch (IllegalStateException | IllegalArgumentException | CompletionException e) { - LOG.warn(e.toString()); throw new BadMessageException("Unable to parse form content", e); } } @@ -1332,7 +1330,6 @@ else if (MimeTypes.Type.MULTIPART_FORM_DATA.is(baseType) && } catch (IllegalStateException | IllegalArgumentException e) { - LOG.warn(e.toString()); throw new BadMessageException("Unable to parse form content", e); } } diff --git a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java index dd3c2324f110..54b4bc312599 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java @@ -1292,7 +1292,6 @@ private void extractContentParameters() throws BadMessageException } catch (IllegalStateException | IllegalArgumentException | CompletionException e) { - LOG.warn(e.toString()); throw new BadMessageException("Unable to parse form content", e); } } @@ -1330,7 +1329,6 @@ else if (MimeTypes.Type.MULTIPART_FORM_DATA.is(baseType) && } catch (IllegalStateException | IllegalArgumentException | CompletionException e) { - LOG.warn(e.toString()); throw new BadMessageException("Unable to parse form content", e); } } @@ -1341,7 +1339,6 @@ else if (MimeTypes.Type.MULTIPART_FORM_DATA.is(baseType) && } catch (IllegalStateException | IllegalArgumentException e) { - LOG.warn(e.toString()); throw new BadMessageException("Unable to parse form content", e); } } diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java index 75fd39b30dcb..75402ed4175e 100644 --- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java +++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java @@ -21,6 +21,7 @@ import java.net.UnknownHostException; import java.net.http.WebSocket; import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -31,25 +32,33 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletionStage; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; import java.util.stream.Stream; -import org.eclipse.jetty.client.AsyncRequestContent; +import org.eclipse.jetty.client.BytesRequestContent; import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.FormRequestContent; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.MultiPartRequestContent; import org.eclipse.jetty.client.transport.HttpClientConnectionFactory; import org.eclipse.jetty.client.transport.HttpClientTransportDynamic; import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MultiPart; import org.eclipse.jetty.http.MultiPartByteRanges; import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2; import org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2; import org.eclipse.jetty.http3.client.HTTP3Client; import org.eclipse.jetty.http3.client.transport.HttpClientTransportOverHTTP3; @@ -63,9 +72,10 @@ import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.PathMatchers; import org.eclipse.jetty.util.BlockingArrayQueue; -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Fields; +import org.eclipse.jetty.util.MultiMap; import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.UrlEncoded; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -74,6 +84,7 @@ import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -1966,69 +1977,184 @@ public void testHTTP2ClientInCoreWebAppProvidedByServer() throws Exception } } - @Test - public void testEagerContentHandler() throws Exception + @ParameterizedTest + @EnumSource(value = HttpVersion.class, names = {"HTTP_1_1", "HTTP_2"}) + public void testEagerContentHandler(HttpVersion httpVersion) throws Exception { - Path jettyBase = newTestJettyBaseDirectory(); String jettyVersion = System.getProperty("jettyVersion"); JettyHomeTester distribution = JettyHomeTester.Builder.newInstance() .jettyVersion(jettyVersion) - .jettyBase(jettyBase) .build(); - try (JettyHomeTester.Run run1 = distribution.start("--add-modules=http,ee11-demo-jetty,eager-content,eager-multipart-content")) + try (JettyHomeTester.Run run1 = distribution.start("--add-modules=resources,test-keystore,http,http2c,ee11-deploy,ee11-annotations,eager-content")) { - assertTrue(run1.awaitFor(10, TimeUnit.SECONDS)); + assertTrue(run1.awaitFor(START_TIMEOUT, TimeUnit.SECONDS)); assertEquals(0, run1.getExitValue()); - int httpPort = Tester.freePort(); - List args = List.of( - "jetty.delayed.maxRetainedContentBytes=-1", - "jetty.eager.form.maxFields=100", - "jetty.eager.form.maxLength=20000", - "jetty.eager.retained.maxRetainedBytes=8192", - "jetty.eager.retained.framingOverhead=10", - "jetty.eager.retained.rejectWhenExceeded=false", - "jetty.eager.multipart.location=/tmp", - "jetty.eager.multipart.maxParts=100", - "jetty.eager.multipart.maxSize=100000", - "jetty.eager.multipart.maxPartSize=32768", - "jetty.eager.multipart.maxMemoryPartSize=2048", - "jetty.eager.multipart.maxHeadersSize=4096", - "jetty.eager.multipart.useFilesForPartsWithoutFileName=true", - "jetty.eager.multipart.complianceMode=RFC7578", - "jetty.http.port=" + httpPort); - try (JettyHomeTester.Run run2 = distribution.start(args); AsyncRequestContent content = new AsyncRequestContent()) + Path war = distribution.resolveArtifact("org.eclipse.jetty.demos:jetty-servlet5-demo-simple-webapp:war:" + jettyVersion); + String contextPath = "ctx"; + distribution.installWar(war, contextPath); + + int port = Tester.freePort(); + int maxRetainedBytes = 128; + String[] properties = { + "jetty.http.selectors=1", + "jetty.http.port=" + port, + "jetty.eager.retained.framingOverhead=16", + "jetty.eager.retained.maxRetainedBytes=" + maxRetainedBytes, + "jetty.eager.retained.rejectWhenExceeded=false" + }; + try (JettyHomeTester.Run run2 = distribution.start(properties)) { assertTrue(run2.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS)); - startHttpClient(); - CountDownLatch latch = new CountDownLatch(1); - client.newRequest("http://localhost:" + httpPort + "/ee11-test/dump") - .method("POST") - .body(content) - .timeout(15, TimeUnit.SECONDS) - .send(result -> + startHttpClient(() -> + { + ClientConnector connector = new ClientConnector(); + HTTP2Client h2Client = new HTTP2Client(connector); + return new HttpClient(new HttpClientTransportDynamic(connector, HttpClientConnectionFactory.HTTP11, new ClientConnectionFactoryOverHTTP2.HTTP2(h2Client))); + }); + + IntStream.of(maxRetainedBytes / 2, maxRetainedBytes * 10).forEach(contentLength -> + { + try + { + ContentResponse response = client.newRequest("http://localhost:" + port + "/" + contextPath + "/echo/content") + .method("POST") + .version(httpVersion) + .body(new BytesRequestContent(new byte[contentLength])) + .send(); + + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertEquals(contentLength, response.getContent().length); + } + catch (Exception x) { - assertThat(result.getResponse().getStatus(), is(HttpStatus.OK_200)); - latch.countDown(); - }); + throw new RuntimeException(x); + } + }); + } + } + } + + @ParameterizedTest + @EnumSource(value = HttpVersion.class, names = {"HTTP_1_1", "HTTP_2"}) + public void testEagerFormContentHandler(HttpVersion httpVersion) throws Exception + { + String jettyVersion = System.getProperty("jettyVersion"); + JettyHomeTester distribution = JettyHomeTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .build(); + + try (JettyHomeTester.Run run1 = distribution.start("--add-modules=resources,test-keystore,http,http2c,ee11-deploy,ee11-annotations,eager-content")) + { + assertTrue(run1.awaitFor(START_TIMEOUT, TimeUnit.SECONDS)); + assertEquals(0, run1.getExitValue()); + + Path war = distribution.resolveArtifact("org.eclipse.jetty.demos:jetty-servlet5-demo-simple-webapp:war:" + jettyVersion); + String contextPath = "ctx"; + distribution.installWar(war, contextPath); + + int port = Tester.freePort(); + String[] properties = { + "jetty.http.selectors=1", + "jetty.http.port=" + port, + "jetty.eager.form.maxFields=16", + "jetty.eager.form.maxLength=128" + }; + try (JettyHomeTester.Run run2 = distribution.start(properties)) + { + assertTrue(run2.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS)); - for (int i = 1; i < 10; i++) + startHttpClient(() -> { - Callback.Completable complete = new Callback.Completable(); - content.write(false, BufferUtil.toBuffer(Integer.toString(i)), complete); - content.flush(); - Thread.sleep(10); - complete.get(5, TimeUnit.SECONDS); - } + ClientConnector connector = new ClientConnector(); + HTTP2Client h2Client = new HTTP2Client(connector); + return new HttpClient(new HttpClientTransportDynamic(connector, HttpClientConnectionFactory.HTTP11, new ClientConnectionFactoryOverHTTP2.HTTP2(h2Client))); + }); + + Map> inMap = new LinkedHashMap<>(); + inMap.put("greet", List.of("Hello World")); + inMap.put("currency", List.of("€")); + Charset charset = StandardCharsets.UTF_8; + ContentResponse response = client.newRequest("http://localhost:" + port + "/" + contextPath + "/echo/form") + .method("POST") + .version(httpVersion) + .body(new FormRequestContent(new Fields(new MultiMap<>(inMap)), charset)) + .send(); + + assertEquals(HttpStatus.OK_200, response.getStatus()); + LinkedHashMap> outMap = new LinkedHashMap<>(); + UrlEncoded.decodeTo(response.getContentAsString(), (name, value) -> outMap.computeIfAbsent(name, k -> new ArrayList<>()).add(value), charset); + assertEquals(inMap, outMap); + } + } + } + + @ParameterizedTest + @EnumSource(value = HttpVersion.class, names = {"HTTP_1_1", "HTTP_2"}) + public void testEagerMultiPartContentHandler(HttpVersion httpVersion) throws Exception + { + String jettyVersion = System.getProperty("jettyVersion"); + JettyHomeTester distribution = JettyHomeTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .build(); + + try (JettyHomeTester.Run run1 = distribution.start("--add-modules=resources,test-keystore,http,http2c,ee11-deploy,ee11-annotations,eager-multipart-content")) + { + assertTrue(run1.awaitFor(START_TIMEOUT, TimeUnit.SECONDS)); + assertEquals(0, run1.getExitValue()); + + Path jettyLogging = distribution.getJettyBase().resolve("resources/jetty-logging.properties"); + String loggingConfig = """ + org.eclipse.jetty.LEVEL=DEBUG + """; + Files.writeString(jettyLogging, loggingConfig, StandardOpenOption.TRUNCATE_EXISTING); + long fileLength = Files.size(jettyLogging); + + Path war = distribution.resolveArtifact("org.eclipse.jetty.demos:jetty-servlet5-demo-simple-webapp:war:" + jettyVersion); + String contextPath = "ctx"; + distribution.installWar(war, contextPath); + + Path work = distribution.getJettyBase().resolve("work"); + + int port = Tester.freePort(); + String[] properties = { + "jetty.http.selectors=1", + "jetty.http.port=" + port, + "jetty.eager.multipart.location=" + work.toAbsolutePath(), + "jetty.eager.multipart.maxParts=3", + "jetty.eager.multipart.maxSize=1024", + "jetty.eager.multipart.maxMemoryPartSize=0", + "jetty.eager.multipart.maxHeadersSize=1024", + "jetty.eager.multipart.useFilesForPartsWithoutFileName=true" + }; + try (JettyHomeTester.Run run2 = distribution.start(properties)) + { + assertTrue(run2.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS)); - Callback.Completable end = new Callback.Completable(); - content.write(true, BufferUtil.toBuffer("x"), end); + startHttpClient(() -> + { + ClientConnector connector = new ClientConnector(); + HTTP2Client h2Client = new HTTP2Client(connector); + return new HttpClient(new HttpClientTransportDynamic(connector, HttpClientConnectionFactory.HTTP11, new ClientConnectionFactoryOverHTTP2.HTTP2(h2Client))); + }); + + MultiPartRequestContent content = new MultiPartRequestContent(); + content.addPart(new MultiPart.ByteBufferPart("part1", null, HttpFields.EMPTY, StandardCharsets.UTF_8.encode("13-bytes-long"))); + content.addPart(new MultiPart.PathPart("part2", null, HttpFields.EMPTY, distribution.getJettyBase().resolve("resources/jetty-logging.properties"))); content.close(); - end.get(5, TimeUnit.SECONDS); + ContentResponse response = client.newRequest("http://localhost:" + port + "/" + contextPath + "/echo/multipart") + .method("POST") + .version(httpVersion) + .body(content) + .send(); - assertTrue(latch.await(15, TimeUnit.SECONDS)); + assertEquals(HttpStatus.OK_200, response.getStatus()); + String[] lines = StringUtil.csvSplit(response.getContentAsString()); + assertEquals(2, lines.length); + assertEquals(lines[0], "name=part1&length=13"); + assertEquals(lines[1], "name=part2&length=" + fileLength); } } } From 4997eed11455d1c6b8804c84db8bcf681cf73728 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 14 Nov 2024 11:28:22 +1100 Subject: [PATCH 56/61] Merged eager modules to one module --- .../main/config/etc/jetty-eager-content.xml | 28 ++++++++++++++ .../etc/jetty-eager-multipart-content.xml | 31 --------------- .../src/main/config/modules/eager-content.mod | 26 ++++++++++++- .../modules/eager-multipart-content.mod | 38 ------------------- .../tests/distribution/DistributionTests.java | 2 +- 5 files changed, 53 insertions(+), 72 deletions(-) delete mode 100644 jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml delete mode 100644 jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml index dba4a0c20aa9..f0a8cf5d9f48 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml @@ -2,6 +2,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml deleted file mode 100644 index 6291e9cbeddf..000000000000 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-multipart-content.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/jetty-core/jetty-server/src/main/config/modules/eager-content.mod b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod index 62f48d8d0375..a93262b97783 100644 --- a/jetty-core/jetty-server/src/main/config/modules/eager-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod @@ -27,6 +27,30 @@ etc/jetty-eager-content.xml ## The maximum size of FormFields to be eagerly loaded or -1 for a default # jetty.eager.form.maxLength=-1 +## the directory where parts will be saved as files. +# jetty.eager.multipart.location=/tmp + +## The maximum number of parts that can be parsed from the multipart content, or -1 for unlimited. +# jetty.eager.multipart.maxParts=100 + +## The maximum size in bytes of the whole multipart content, or -1 for unlimited. +# jetty.eager.multipart.maxSize=52428800 + +## The maximum part size in bytes, or -1 for unlimited. +# jetty.eager.multipart.maxPartSize=10485760 + +## The maximum size of a part in memory, after which it will be written as a file. +# jetty.eager.multipart.maxMemoryPartSize=1024 + +## The max length of a Part header, in bytes, or -1 for unlimited length. +# jetty.eager.multipart.maxHeadersSize=8192 + +## Whether parts without a fileName are stored as files. +# jetty.eager.multipart.useFilesForPartsWithoutFileName=true + +## The MultiPart compliance mode. +# jetty.eager.multipart.complianceMode=RFC7578 + ## The maximum bytes of retained data to be eagerly loaded or -1 for a default # jetty.eager.retained.maxRetainedBytes=-1 @@ -35,6 +59,4 @@ etc/jetty-eager-content.xml ## If requests should be rejected if they exceed the maxRetainedBytes # jetty.eager.retained.rejectWhenExceeded=false - -## For eager multipart configuration use eager-multipart-content module #end::documentation[] diff --git a/jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod b/jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod deleted file mode 100644 index f247c597ebea..000000000000 --- a/jetty-core/jetty-server/src/main/config/modules/eager-multipart-content.mod +++ /dev/null @@ -1,38 +0,0 @@ -[description] -Applies MultiPart configuration to the EagerContentHandler - -[tags] -server - -[before] -eager-content - -[xml] -etc/jetty-eager-multipart-content.xml - -[ini-template] -#tag::documentation[] -## the directory where parts will be saved as files. -# jetty.eager.multipart.location=/tmp - -## The maximum number of parts that can be parsed from the multipart content, or -1 for unlimited. -# jetty.eager.multipart.maxParts=100 - -## The maximum size in bytes of the whole multipart content, or -1 for unlimited. -# jetty.eager.multipart.maxSize=52428800 - -## The maximum part size in bytes, or -1 for unlimited. -# jetty.eager.multipart.maxPartSize=10485760 - -## The maximum size of a part in memory, after which it will be written as a file. -# jetty.eager.multipart.maxMemoryPartSize=1024 - -## The max length of a Part header, in bytes, or -1 for unlimited length. -# jetty.eager.multipart.maxHeadersSize=8192 - -## Whether parts without a fileName are stored as files. -# jetty.eager.multipart.useFilesForPartsWithoutFileName=true - -## The MultiPart compliance mode. -# jetty.eager.multipart.complianceMode=RFC7578 -#end::documentation[] diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java index 75402ed4175e..24cf1b68801e 100644 --- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java +++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java @@ -2100,7 +2100,7 @@ public void testEagerMultiPartContentHandler(HttpVersion httpVersion) throws Exc .jettyVersion(jettyVersion) .build(); - try (JettyHomeTester.Run run1 = distribution.start("--add-modules=resources,test-keystore,http,http2c,ee11-deploy,ee11-annotations,eager-multipart-content")) + try (JettyHomeTester.Run run1 = distribution.start("--add-modules=resources,test-keystore,http,http2c,ee11-deploy,ee11-annotations,eager-content")) { assertTrue(run1.awaitFor(START_TIMEOUT, TimeUnit.SECONDS)); assertEquals(0, run1.getExitValue()); From a68e9b3b4f77435be49a0991653367e9337bfff8 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 14 Nov 2024 11:56:48 +1100 Subject: [PATCH 57/61] Removed the check on inputState. The javadoc of the Servlet API says: > If the parameter data was sent in the request body, such as occurs with an HTTP POST request, then reading the body directly via getInputStream or getReader can interfere with the execution of this method. So there is no requirement to detect or ignore somebody reading before calling a parameter method --- .../java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java | 2 +- .../java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java index 71d4027a024e..a4c910dbde61 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java @@ -1268,7 +1268,7 @@ private void extractContentParameters() throws BadMessageException try { int contentLength = getContentLength(); - if (contentLength != 0 && _inputState == ServletContextRequest.INPUT_NONE) + if (contentLength != 0) { String baseType = HttpField.getValueParameters(getContentType(), null); if (MimeTypes.Type.FORM_ENCODED.is(baseType) && diff --git a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java index 54b4bc312599..9b14fb57dd6e 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiRequest.java @@ -1277,7 +1277,7 @@ private void extractContentParameters() throws BadMessageException try { int contentLength = getContentLength(); - if (contentLength != 0 && _inputState == ServletContextRequest.INPUT_NONE) + if (contentLength != 0) { String baseType = HttpField.getValueParameters(getContentType(), null); if (MimeTypes.Type.FORM_ENCODED.is(baseType) && From e8ce082c73ca2226449f469ea72c8aebb0c473a9 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 14 Nov 2024 17:34:25 +1100 Subject: [PATCH 58/61] Fixed bad content type test --- .../jetty/client/FormRequestContent.java | 8 +++++-- .../client/util/TypedContentProviderTest.java | 17 +++++++------- .../org/eclipse/jetty/http/MimeTypes.java | 22 +++++++++++-------- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/FormRequestContent.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/FormRequestContent.java index 2c22f5baa2a3..4dac58869b15 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/FormRequestContent.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/FormRequestContent.java @@ -17,6 +17,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.util.Fields; /** @@ -32,7 +33,10 @@ public FormRequestContent(Fields fields) public FormRequestContent(Fields fields, Charset charset) { - super("application/x-www-form-urlencoded;charset=" + charset.name(), convert(fields, charset), charset); + super(charset == StandardCharsets.UTF_8 + ? MimeTypes.Type.FORM_ENCODED_UTF_8.asString() + : MimeTypes.Type.FORM_ENCODED.asString() + ";charset=" + charset.name().toLowerCase(), + convert(fields, charset), charset); } public static String convert(Fields fields) @@ -48,7 +52,7 @@ public static String convert(Fields fields, Charset charset) { for (String value : field.getValues()) { - if (builder.length() > 0) + if (!builder.isEmpty()) builder.append("&"); builder.append(encode(field.getName(), charset)).append("=").append(encode(value, charset)); } diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/util/TypedContentProviderTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/util/TypedContentProviderTest.java index 1057d69585bf..71b13ffaa9c0 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/util/TypedContentProviderTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/util/TypedContentProviderTest.java @@ -35,6 +35,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -57,14 +58,12 @@ public void testFormContentProvider(Scenario scenario) throws Exception protected void service(Request request, Response response) { assertEquals("POST", request.getMethod()); - assertEquals(MimeTypes.Type.FORM_ENCODED.asString(), request.getHeaders().get(HttpHeader.CONTENT_TYPE)); - FormFields.from(request).whenComplete((fields, failure) -> - { - assertEquals(value1, fields.get(name1).getValue()); - List values = fields.get(name2).getValues(); - assertEquals(2, values.size()); - assertThat(values, containsInAnyOrder(value2, value3)); - }); + assertThat(request.getHeaders().get(HttpHeader.CONTENT_TYPE), equalToIgnoringCase(MimeTypes.Type.FORM_ENCODED_UTF_8.asString())); + Fields fields = FormFields.getFields(request); + assertEquals(value1, fields.get(name1).getValue()); + List values = fields.get(name2).getValues(); + assertEquals(2, values.size()); + assertThat(values, containsInAnyOrder(value2, value3)); } }); @@ -89,7 +88,7 @@ public void testFormContentProviderWithDifferentContentType(Scenario scenario) t fields.put(name1, value1); fields.add(name2, value2); final String content = FormRequestContent.convert(fields); - final String contentType = "text/plain;charset=UTF-8"; + final String contentType = "text/plain;charset=utf-8"; start(scenario, new EmptyServerHandler() { diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java index 50f530284071..f35766930d95 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java @@ -61,7 +61,11 @@ public class MimeTypes public enum Type { FORM_ENCODED("application/x-www-form-urlencoded"), + FORM_ENCODED_UTF_8("application/x-www-form-urlencoded;charset=utf-8", FORM_ENCODED), + FORM_ENCODED_8859_1("application/x-www-form-urlencoded;charset=iso-8859-1", FORM_ENCODED), + MESSAGE_HTTP("message/http"), + MULTIPART_BYTERANGES("multipart/byteranges"), MULTIPART_FORM_DATA("multipart/form-data"), @@ -77,6 +81,10 @@ public HttpField getContentTypeField(Charset charset) return super.getContentTypeField(charset); } }, + + TEXT_HTML_8859_1("text/html;charset=iso-8859-1", TEXT_HTML), + TEXT_HTML_UTF_8("text/html;charset=utf-8", TEXT_HTML), + TEXT_PLAIN("text/plain") { @Override @@ -89,6 +97,9 @@ public HttpField getContentTypeField(Charset charset) return super.getContentTypeField(charset); } }, + TEXT_PLAIN_8859_1("text/plain;charset=iso-8859-1", TEXT_PLAIN), + TEXT_PLAIN_UTF_8("text/plain;charset=utf-8", TEXT_PLAIN), + TEXT_XML("text/xml") { @Override @@ -101,21 +112,14 @@ public HttpField getContentTypeField(Charset charset) return super.getContentTypeField(charset); } }, - TEXT_JSON("text/json", StandardCharsets.UTF_8), - APPLICATION_JSON("application/json", StandardCharsets.UTF_8), - - TEXT_HTML_8859_1("text/html;charset=iso-8859-1", TEXT_HTML), - TEXT_HTML_UTF_8("text/html;charset=utf-8", TEXT_HTML), - - TEXT_PLAIN_8859_1("text/plain;charset=iso-8859-1", TEXT_PLAIN), - TEXT_PLAIN_UTF_8("text/plain;charset=utf-8", TEXT_PLAIN), - TEXT_XML_8859_1("text/xml;charset=iso-8859-1", TEXT_XML), TEXT_XML_UTF_8("text/xml;charset=utf-8", TEXT_XML), + TEXT_JSON("text/json", StandardCharsets.UTF_8), TEXT_JSON_8859_1("text/json;charset=iso-8859-1", TEXT_JSON), TEXT_JSON_UTF_8("text/json;charset=utf-8", TEXT_JSON), + APPLICATION_JSON("application/json", StandardCharsets.UTF_8), APPLICATION_JSON_8859_1("application/json;charset=iso-8859-1", APPLICATION_JSON), APPLICATION_JSON_UTF_8("application/json;charset=utf-8", APPLICATION_JSON); From fbd09fbcd5446bcfdfce8cb6468099007710c1d1 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Thu, 14 Nov 2024 15:33:52 +0100 Subject: [PATCH 59/61] Added documentation about `EagerContentHandler`. Signed-off-by: Simone Bordet --- .../pages/modules/standard.adoc | 15 ++++++++ .../programming-guide/pages/server/http.adoc | 35 +++++++++++++++++++ .../src/main/config/modules/eager-content.mod | 32 ++++++++++------- 3 files changed, 69 insertions(+), 13 deletions(-) diff --git a/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc b/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc index 730fb220e530..849f839dddbb 100644 --- a/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc +++ b/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc @@ -129,6 +129,21 @@ The module properties are: include::{jetty-home}/modules/debuglog.mod[tags=documentation] ---- +[[eager-content]] +== Module `eager-content` + +The `eager-content` module installs the `org.eclipse.jetty.server.handler.EagerContentHandler` at the root of the `Handler` tree. + +The `EagerContentHandler` can eagerly load request content, asynchronously, before calling the next `Handler`. +For more information see xref:programming-guide:server/http.adoc#handler-use-eager[this section]. + +`EagerContentHandler` can eagerly load content for form uploads, multipart uploads and any request content, and you can configure it with different properties for these three cases. + +The module properties are: + +---- +include::{jetty-home}/modules/eager-content.mod[tags=documentation] +---- [[eeN-deploy]] == Module `{ee-all}-deploy` diff --git a/documentation/jetty/modules/programming-guide/pages/server/http.adoc b/documentation/jetty/modules/programming-guide/pages/server/http.adoc index 19319cd45fc5..081f1e9e88e5 100644 --- a/documentation/jetty/modules/programming-guide/pages/server/http.adoc +++ b/documentation/jetty/modules/programming-guide/pages/server/http.adoc @@ -1198,6 +1198,41 @@ In the example above, `ContextHandlerCollection` will try to match a request to NOTE: `DefaultHandler` just sends a nicer HTTP `404` response in case of wrong requests from clients. Jetty will send an HTTP `404` response anyway if `DefaultHandler` has not been set. +[[handler-use-eager]] +==== EagerContentHandler + +`EagerContentHandler` reads eagerly the HTTP request content, and invokes the next `Handler` in the `Handler` tree when the request content has been read. + +`EagerContentHandler` should be installed when web applications use blocking I/O to read the request content, which is the typical case for Servlet or RESTful (JAX-RS) web applications. + +Because the request content is read eagerly and asynchronously, the web application will never (or rarely) block while reading the request content. +In this way, the application obtains the benefits of asynchronous I/O without forcing web application developers to use more complicated asynchronous I/O APIs. + +The `Handler` tree structure looks like the following: + +[,screen] +---- +Server +└── (GzipHandler) // optional + └── EagerContentHandler + └── ContextHandler /app + └── AppHandler +---- + +`EagerContentHandler` should be installed in the `Handler` tree _after_ other ``Handler``s that may modify or transform the request content, like for example the `GzipHandler`. + +`EagerContentHandler` eagerly reads request content in the following cases: + +* Form request content. +* MultiPart request content. +* Any other type of request content. + +For Form and MultiPart request content, `EagerContentHandler` reads the whole request content and then invokes the next `Handler`. +This allows web applications that use blocking API calls such as `HttpServletRequest.getParameterMap()` (for Form request content) or `HttpServletRequest.getParts()` (for MultiPart request content) to avoid blocking. + +For other types of request content, `EagerContentHandler` reads and retains request content bytes up to a configurable amount, and then invokes the next `Handler`. +This allows web applications that use blocking API calls such as `HttpServletRequest.getInputStream()` to avoid blocking in most cases (if the request is smaller than what has been configured in `EagerContentHandler`). + [[handler-use-servlet]] === Servlet API Handlers diff --git a/jetty-core/jetty-server/src/main/config/modules/eager-content.mod b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod index a93262b97783..2e4183163aa2 100644 --- a/jetty-core/jetty-server/src/main/config/modules/eager-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod @@ -1,11 +1,9 @@ [description] -Applies the EagerContentHandler to the entire server -#tag::description[] +Applies the EagerContentHandler to the entire server. The EagerContentHandler can eagerly load content asynchronously before calling the next handler. Typically, this handler is deployed before an application that uses blocking IO to read the request body and if deployed after this handler, the application will never (or rarely) block for request content. This gives many of the benefits of asynchronous IO without the need to write an asynchronous application. -#end::description[] [tags] server @@ -13,27 +11,35 @@ server [depend] server +[after] +compression +cross-origin +gzip +rewrite +size-limit + [before] -threadlimit +qos +thread-limit [xml] etc/jetty-eager-content.xml [ini-template] #tag::documentation[] -## The maximum number of FormFields to be eagerly loaded or -1 for a default +## The maximum number of form fields or -1 for a default. # jetty.eager.form.maxFields=-1 -## The maximum size of FormFields to be eagerly loaded or -1 for a default +## The maximum size of the form in bytes -1 for a default. # jetty.eager.form.maxLength=-1 -## the directory where parts will be saved as files. +## The directory where MultiPart parts will be saved as files. # jetty.eager.multipart.location=/tmp -## The maximum number of parts that can be parsed from the multipart content, or -1 for unlimited. +## The maximum number of parts that can be parsed from the MultiPart content, or -1 for unlimited. # jetty.eager.multipart.maxParts=100 -## The maximum size in bytes of the whole multipart content, or -1 for unlimited. +## The maximum size in bytes of the whole MultiPart content, or -1 for unlimited. # jetty.eager.multipart.maxSize=52428800 ## The maximum part size in bytes, or -1 for unlimited. @@ -42,7 +48,7 @@ etc/jetty-eager-content.xml ## The maximum size of a part in memory, after which it will be written as a file. # jetty.eager.multipart.maxMemoryPartSize=1024 -## The max length of a Part header, in bytes, or -1 for unlimited length. +## The max length in bytes of the headers of a part, or -1 for unlimited. # jetty.eager.multipart.maxHeadersSize=8192 ## Whether parts without a fileName are stored as files. @@ -51,12 +57,12 @@ etc/jetty-eager-content.xml ## The MultiPart compliance mode. # jetty.eager.multipart.complianceMode=RFC7578 -## The maximum bytes of retained data to be eagerly loaded or -1 for a default +## The maximum bytes of request content, including framing overhead, to read and retain eagerly, or -1 for a default. # jetty.eager.retained.maxRetainedBytes=-1 -## The frame overhead to use when calculating the retained bytes or -1 for a default +## The framing overhead to use when calculating the request content bytes to read and retain, or -1 for a default. # jetty.eager.retained.framingOverhead=-1 -## If requests should be rejected if they exceed the maxRetainedBytes +## Whether requests should be rejected if they exceed maxRetainedBytes. # jetty.eager.retained.rejectWhenExceeded=false #end::documentation[] From 8dd0dfe156ee770c900dfcc011f003dd216fe6c5 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Thu, 14 Nov 2024 22:34:49 +0100 Subject: [PATCH 60/61] Renamed properties "jetty.eager.retained" to "jetty.eager.content". Signed-off-by: Simone Bordet --- .../src/main/config/etc/jetty-eager-content.xml | 6 +++--- .../jetty-server/src/main/config/modules/eager-content.mod | 6 +++--- .../eclipse/jetty/tests/distribution/DistributionTests.java | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml index f0a8cf5d9f48..459922747ba6 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-eager-content.xml @@ -48,9 +48,9 @@ - - - + + + diff --git a/jetty-core/jetty-server/src/main/config/modules/eager-content.mod b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod index 2e4183163aa2..5cfae2e8b4d2 100644 --- a/jetty-core/jetty-server/src/main/config/modules/eager-content.mod +++ b/jetty-core/jetty-server/src/main/config/modules/eager-content.mod @@ -58,11 +58,11 @@ etc/jetty-eager-content.xml # jetty.eager.multipart.complianceMode=RFC7578 ## The maximum bytes of request content, including framing overhead, to read and retain eagerly, or -1 for a default. -# jetty.eager.retained.maxRetainedBytes=-1 +# jetty.eager.content.maxRetainedBytes=-1 ## The framing overhead to use when calculating the request content bytes to read and retain, or -1 for a default. -# jetty.eager.retained.framingOverhead=-1 +# jetty.eager.content.framingOverhead=-1 ## Whether requests should be rejected if they exceed maxRetainedBytes. -# jetty.eager.retained.rejectWhenExceeded=false +# jetty.eager.content.rejectWhenExceeded=false #end::documentation[] diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java index cdc9ede10be7..2abbf4966ac5 100644 --- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java +++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java @@ -2000,9 +2000,9 @@ public void testEagerContentHandler(HttpVersion httpVersion) throws Exception String[] properties = { "jetty.http.selectors=1", "jetty.http.port=" + port, - "jetty.eager.retained.framingOverhead=16", - "jetty.eager.retained.maxRetainedBytes=" + maxRetainedBytes, - "jetty.eager.retained.rejectWhenExceeded=false" + "jetty.eager.content.framingOverhead=16", + "jetty.eager.content.maxRetainedBytes=" + maxRetainedBytes, + "jetty.eager.content.rejectWhenExceeded=false" }; try (JettyHomeTester.Run run2 = distribution.start(properties)) { From ef2aae0c148dd60296a31a209c2d4689d6e1d1de Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Thu, 14 Nov 2024 23:14:33 +0100 Subject: [PATCH 61/61] Clarified documentation about `EagerContentHandler`. Signed-off-by: Simone Bordet --- .../modules/programming-guide/pages/server/http.adoc | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/documentation/jetty/modules/programming-guide/pages/server/http.adoc b/documentation/jetty/modules/programming-guide/pages/server/http.adoc index 081f1e9e88e5..8683a74e0d55 100644 --- a/documentation/jetty/modules/programming-guide/pages/server/http.adoc +++ b/documentation/jetty/modules/programming-guide/pages/server/http.adoc @@ -1227,10 +1227,13 @@ Server * MultiPart request content. * Any other type of request content. -For Form and MultiPart request content, `EagerContentHandler` reads the whole request content and then invokes the next `Handler`. -This allows web applications that use blocking API calls such as `HttpServletRequest.getParameterMap()` (for Form request content) or `HttpServletRequest.getParts()` (for MultiPart request content) to avoid blocking. +For Form request content, `EagerContentHandler` reads the whole request content, parses it into a `Fields` object, and then invokes the next `Handler`. +This allows web applications that use blocking API calls such as `HttpServletRequest.getParameterMap()` to avoid blocking, since they can directly use the already created `Fields` object. -For other types of request content, `EagerContentHandler` reads and retains request content bytes up to a configurable amount, and then invokes the next `Handler`. +Similarly, for MultiPart request content, `EagerContentHandler` reads the whole request content, parses it into `MultiPartFormData.Parts`, and then invokes the next `Handler`. +This allows web applications that use blocking API calls such as `HttpServletRequest.getParts()` to avoid blocking, since the can directly use the already created `MultiPartFormData.Parts` object. + +For other types of request content, `EagerContentHandler` reads and retains request content bytes up to a configurable amount, and then invokes the next `Handler`, without any further processing of the request content bytes. This allows web applications that use blocking API calls such as `HttpServletRequest.getInputStream()` to avoid blocking in most cases (if the request is smaller than what has been configured in `EagerContentHandler`). [[handler-use-servlet]]