Merge branch 'jetty-12.0.x' into jetty-12.0.x-9396-websocket-jpms-review

This commit is contained in:
gregw 2023-06-30 17:17:16 +02:00
commit e0133d72bf
300 changed files with 5821 additions and 4701 deletions

View File

@ -31,17 +31,17 @@ There is also an link:#error-handler[Error Handler] that services errors related
_____
[NOTE]
The `DefaultHandler` will also handle serving out the `flav.ico` file should a request make it through all of the other handlers without being resolved.
The `DefaultHandler` will also handle serving out the `favicon.ico` file should a request make it through all of the other handlers without being resolved.
_____
[source, java, subs="{sub-order}"]
----
Server server = new Server(8080);
HandlerList handlers = new HandlerList();
ResourceHandler resourceHandler = new ResourceHandler();
resourceHandler.setBaseResource(Resource.newResource("."));
handlers.setHandlers(new Handler[]
{ resourceHandler, new DefaultHandler() });
resourceHandler.setBaseResource(ResourceFactory.of(resourceHandler).newResource("."));
Handler.Sequence handlers = new Handler.Sequence(
resourceHandler, new DefaultHandler()
);
server.setHandler(handlers);
server.start();
----

View File

@ -46,7 +46,7 @@ The Jetty Server Libraries provides a number of out-of-the-box __Handler__s that
====== ContextHandler
`ContextHandler` is a `Handler` that represents a _context_ for a web application.
It is a `HandlerWrapper` that performs some action before and after delegating to the nested `Handler`.
It is a `Handler.Wrapper` that performs some action before and after delegating to the nested `Handler`.
// TODO: expand on what the ContextHandler does, e.g. ServletContext.
The simplest use of `ContextHandler` is the following:
@ -140,7 +140,7 @@ See also xref:pg-server-http-handler-use-util-default-handler[how to use] `Defau
`GzipHandler` provides supports for automatic decompression of compressed request content and automatic compression of response content.
`GzipHandler` is a `HandlerWrapper` that inspects the request and, if the request matches the `GzipHandler` configuration, just installs the required components to eventually perform decompression of the request content or compression of the response content.
`GzipHandler` is a `Handler.Wrapper` that inspects the request and, if the request matches the `GzipHandler` configuration, just installs the required components to eventually perform decompression of the request content or compression of the response content.
The decompression/compression is not performed until the web application reads request content or writes response content.
`GzipHandler` can be configured at the server level in this way:
@ -291,7 +291,7 @@ include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPSer
* Sends a HTTP `404` response for any other request
* The HTTP `404` response content nicely shows a HTML table with all the contexts deployed on the `Server` instance
`DefaultHandler` is best used as the last `Handler` of a `HandlerList`, for example:
`DefaultHandler` is best used directly set on the server, for example:
[source,java,indent=0]
----
@ -310,7 +310,7 @@ Server
└── DefaultHandler
----
In the example above, `ContextHandlerCollection` will try to match a request to one of the contexts; if the match fails, `HandlerList` will call the next `Handler` which is `DefaultHandler` that will return a HTTP `404` with an HTML page showing the existing contexts deployed on the `Server`.
In the example above, `ContextHandlerCollection` will try to match a request to one of the contexts; if the match fails, `Server` will call the `DefaultHandler` that will return a HTTP `404` with an HTML page showing the existing contexts deployed on the `Server`.
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` is not used.

View File

@ -0,0 +1,244 @@
//
// ========================================================================
// 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.docs.programming;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.AsyncContent;
import org.eclipse.jetty.io.content.ContentSourceCompletableFuture;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.CharsetStringBuilder;
import org.eclipse.jetty.util.FutureCallback;
import org.eclipse.jetty.util.Utf8StringBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@SuppressWarnings("unused")
public class ContentDocs
{
private static final Logger LOG = LoggerFactory.getLogger(ContentDocs.class);
// tag::echo[]
public void echo(Content.Source source, Content.Sink sink, Callback callback)
{
Callback echo = new Callback()
{
private Content.Chunk chunk;
public void succeeded()
{
// release any previous chunk
if (chunk != null)
{
chunk.release();
// complete if it was the last
if (chunk.isLast())
{
callback.succeeded();
return;
}
}
while (true)
{
// read the next chunk
chunk = source.read();
if (chunk == null)
{
// if no chunk, demand more and call succeeded when demand is met.
source.demand(this::succeeded);
return;
}
if (Content.Chunk.isFailure(chunk, true))
{
// if it is a persistent failure, then fail the callback
callback.failed(chunk.getFailure());
return;
}
if (chunk.hasRemaining() || chunk.isLast())
{
// if chunk has content or is last, write it to the sink and resume this loop in callback
sink.write(chunk.isLast(), chunk.getByteBuffer(), this);
return;
}
chunk.release();
}
}
public void failed(Throwable x)
{
source.fail(x);
callback.failed(x);
}
};
source.demand(echo::succeeded);
}
// tag::echo[]
public static void testEcho() throws Exception
{
AsyncContent source = new AsyncContent();
AsyncContent sink = new AsyncContent();
Callback.Completable echoCallback = new Callback.Completable();
new ContentDocs().echo(source, sink, echoCallback);
Content.Chunk chunk = sink.read();
if (chunk != null)
throw new IllegalStateException("No chunk expected yet");
FutureCallback onContentAvailable = new FutureCallback();
sink.demand(onContentAvailable::succeeded);
if (onContentAvailable.isDone())
throw new IllegalStateException("No demand expected yet");
Callback.Completable writeCallback = new Callback.Completable();
Content.Sink.write(source, false, "One", writeCallback);
if (writeCallback.isDone())
throw new IllegalStateException("Should wait until first chunk is consumed");
onContentAvailable.get();
chunk = sink.read();
if (!"One".equals(BufferUtil.toString(chunk.getByteBuffer())))
throw new IllegalStateException("first chunk is expected");
if (writeCallback.isDone())
throw new IllegalStateException("Should wait until first chunk is consumed");
chunk.release();
writeCallback.get();
writeCallback = new Callback.Completable();
Content.Sink.write(source, true, "Two", writeCallback);
if (writeCallback.isDone())
throw new IllegalStateException("Should wait until second chunk is consumed");
onContentAvailable = new FutureCallback();
sink.demand(onContentAvailable::succeeded);
if (!onContentAvailable.isDone())
throw new IllegalStateException("Demand expected for second chunk");
chunk = sink.read();
if (!"Two".equals(BufferUtil.toString(chunk.getByteBuffer())))
throw new IllegalStateException("second chunk is expected");
chunk.release();
writeCallback.get();
onContentAvailable = new FutureCallback();
sink.demand(onContentAvailable::succeeded);
if (!onContentAvailable.isDone())
throw new IllegalStateException("Demand expected for EOF");
chunk = sink.read();
if (!chunk.isLast())
throw new IllegalStateException("EOF expected");
}
public static class FutureString extends CompletableFuture<String>
{
private final CharsetStringBuilder text;
private final Content.Source source;
public FutureString(Content.Source source, Charset charset)
{
this.source = source;
this.text = CharsetStringBuilder.forCharset(charset);
source.demand(this::onContentAvailable);
}
private void onContentAvailable()
{
while (true)
{
Content.Chunk chunk = source.read();
if (chunk == null)
{
source.demand(this::onContentAvailable);
return;
}
try
{
if (Content.Chunk.isFailure(chunk))
throw chunk.getFailure();
if (chunk.hasRemaining())
text.append(chunk.getByteBuffer());
if (chunk.isLast() && complete(text.build()))
return;
}
catch (Throwable e)
{
completeExceptionally(e);
}
finally
{
chunk.release();
}
}
}
}
public static void testFutureString() throws Exception
{
AsyncContent source = new AsyncContent();
FutureString future = new FutureString(source, StandardCharsets.UTF_8);
if (future.isDone())
throw new IllegalStateException();
Callback.Completable writeCallback = new Callback.Completable();
Content.Sink.write(source, false, "One", writeCallback);
if (!writeCallback.isDone() || future.isDone())
throw new IllegalStateException("Should be consumed");
Content.Sink.write(source, false, "Two", writeCallback);
if (!writeCallback.isDone() || future.isDone())
throw new IllegalStateException("Should be consumed");
Content.Sink.write(source, true, "Three", writeCallback);
if (!writeCallback.isDone() || !future.isDone())
throw new IllegalStateException("Should be consumed");
}
public static class FutureUtf8String extends ContentSourceCompletableFuture<String>
{
private final Utf8StringBuilder builder = new Utf8StringBuilder();
public FutureUtf8String(Content.Source content)
{
super(content);
}
@Override
protected String parse(Content.Chunk chunk) throws Throwable
{
if (chunk.hasRemaining())
builder.append(chunk.getByteBuffer());
return chunk.isLast() ? builder.takeCompleteString(IllegalStateException::new) : null;
}
}
public static void main(String... args) throws Exception
{
testEcho();
testFutureString();
}
}

View File

@ -110,53 +110,6 @@ public class HTTPServerDocs
// end::simple[]
}
public void httpChannelListener() throws Exception
{
// tag::httpChannelListener[]
// TODO: HttpChannelState.Listener does not exist anymore.
/*
class TimingHttpChannelListener implements HttpChannelState.Listener
{
private final ConcurrentMap<Request, Long> times = new ConcurrentHashMap<>();
@Override
public void onRequestBegin(Request request)
{
times.put(request, NanoTime.now());
}
@Override
public void onComplete(Request request)
{
long begin = times.remove(request);
long elapsed = NanoTime.since(begin);
System.getLogger("timing").log(INFO, "Request {0} took {1} ns", request, elapsed);
}
}
Server server = new Server();
Connector connector = new ServerConnector(server);
server.addConnector(connector);
// Add the HttpChannel.Listener as bean to the connector.
connector.addBean(new TimingHttpChannelListener());
// Set a simple Handler to handle requests/responses.
server.setHandler(new AbstractHandler()
{
@Override
public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
{
jettyRequest.setHandled(true);
}
});
server.start();
*/
// end::httpChannelListener[]
}
public void serverRequestLogSLF4J()
{
// tag::serverRequestLogSLF4J[]
@ -508,14 +461,14 @@ public class HTTPServerDocs
// tag::handlerTree[]
Server server = new Server();
GzipHandler gzipHandler = new GzipHandler();
server.setHandler(gzipHandler);
Handler.Sequence sequence = new Handler.Sequence();
gzipHandler.setHandler(sequence);
sequence.addHandler(new App1Handler());
sequence.addHandler(new App2Handler());
GzipHandler gzipHandler = new GzipHandler(sequence);
server.setHandler(gzipHandler);
// end::handlerTree[]
}
@ -585,6 +538,11 @@ public class HTTPServerDocs
// tag::handlerFilter[]
class FilterHandler extends Handler.Wrapper
{
public FilterHandler(Handler handler)
{
super(handler);
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
@ -617,9 +575,7 @@ public class HTTPServerDocs
server.addConnector(connector);
// Link the Handlers.
FilterHandler filter = new FilterHandler();
filter.setHandler(new HelloWorldHandler());
server.setHandler(filter);
server.setHandler(new FilterHandler(new HelloWorldHandler()));
server.start();
// end::handlerFilter[]
@ -643,9 +599,7 @@ public class HTTPServerDocs
server.addConnector(connector);
// Create a ContextHandler with contextPath.
ContextHandler context = new ContextHandler();
context.setContextPath("/shop");
context.setHandler(new ShopHandler());
ContextHandler context = new ContextHandler(new ShopHandler(), "/shop");
// Link the context to the server.
server.setHandler(context);
@ -683,20 +637,17 @@ public class HTTPServerDocs
// Create a ContextHandlerCollection to hold contexts.
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
// Create the context for the shop web application and add it to ContextHandlerCollection.
contextCollection.addHandler(new ContextHandler(new ShopHandler(), "/shop"));
// Link the ContextHandlerCollection to the Server.
server.setHandler(contextCollection);
// Create the context for the shop web application.
ContextHandler shopContext = new ContextHandler("/shop");
shopContext.setHandler(new ShopHandler());
// Add it to ContextHandlerCollection.
contextCollection.addHandler(shopContext);
server.start();
// Create the context for the API web application.
ContextHandler apiContext = new ContextHandler("/api");
apiContext.setHandler(new RESTHandler());
ContextHandler apiContext = new ContextHandler(new RESTHandler(), "/api");
// Web applications can be deployed after the Server is started.
contextCollection.deployHandler(apiContext, Callback.NOOP);
// end::contextHandlerCollection[]
@ -791,7 +742,7 @@ public class HTTPServerDocs
// tag::multipleResourcesHandler[]
ResourceHandler handler = new ResourceHandler();
// For multiple directories, use ResourceCollection.
// For multiple directories, use ResourceFactory.combine().
Resource resource = ResourceFactory.combine(
ResourceFactory.of(handler).newResource("/path/to/static/resources/"),
ResourceFactory.of(handler).newResource("/another/path/to/static/resources/")
@ -822,8 +773,10 @@ public class HTTPServerDocs
Connector connector = new ServerConnector(server);
server.addConnector(connector);
// Create and configure GzipHandler.
GzipHandler gzipHandler = new GzipHandler();
// Create a ContextHandlerCollection to manage contexts.
ContextHandlerCollection contexts = new ContextHandlerCollection();
// Create and configure GzipHandler linked to the ContextHandlerCollection.
GzipHandler gzipHandler = new GzipHandler(contexts);
// Only compress response content larger than this.
gzipHandler.setMinGzipSize(1024);
// Do not compress these URI paths.
@ -833,10 +786,6 @@ public class HTTPServerDocs
// Do not compress these mime types.
gzipHandler.addExcludedMimeTypes("font/ttf");
// Link a ContextHandlerCollection to manage contexts.
ContextHandlerCollection contexts = new ContextHandlerCollection();
gzipHandler.setHandler(contexts);
// Link the GzipHandler to the Server.
server.setHandler(gzipHandler);
@ -873,26 +822,21 @@ public class HTTPServerDocs
// tag::contextGzipHandler[]
// Create a ContextHandlerCollection to hold contexts.
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
// Link the ContextHandlerCollection to the Server.
server.setHandler(contextCollection);
// Create the context for the shop web application.
ContextHandler shopContext = new ContextHandler("/shop");
shopContext.setHandler(new ShopHandler());
// You want to gzip the shop web application only.
GzipHandler shopGzipHandler = new GzipHandler();
shopGzipHandler.setHandler(shopContext);
// Create the context for the shop web application wrapped with GzipHandler so only the shop will do gzip.
GzipHandler shopGzipHandler = new GzipHandler(new ContextHandler(new ShopHandler(), "/shop"));
// Add it to ContextHandlerCollection.
contextCollection.addHandler(shopGzipHandler);
// Create the context for the API web application.
ContextHandler apiContext = new ContextHandler("/api");
apiContext.setHandler(new RESTHandler());
ContextHandler apiContext = new ContextHandler(new RESTHandler(), "/api");
// Add it to ContextHandlerCollection.
contextCollection.addHandler(apiContext);
// Link the ContextHandlerCollection to the Server.
server.setHandler(contextCollection);
// end::contextGzipHandler[]
server.start();
@ -905,7 +849,10 @@ public class HTTPServerDocs
ServerConnector connector = new ServerConnector(server);
server.addConnector(connector);
RewriteHandler rewriteHandler = new RewriteHandler();
// Create a ContextHandlerCollection to hold contexts.
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
// Link the ContextHandlerCollection to the RewriteHandler.
RewriteHandler rewriteHandler = new RewriteHandler(contextCollection);
// Compacts URI paths with double slashes, e.g. /ctx//path/to//resource.
rewriteHandler.addRule(new CompactPathRule());
// Rewrites */products/* to */p/*.
@ -918,11 +865,6 @@ public class HTTPServerDocs
// Link the RewriteHandler to the Server.
server.setHandler(rewriteHandler);
// Create a ContextHandlerCollection to hold contexts.
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
// Link the ContextHandlerCollection to the RewriteHandler.
rewriteHandler.setHandler(contextCollection);
server.start();
// end::rewriteHandler[]
}
@ -934,16 +876,15 @@ public class HTTPServerDocs
ServerConnector connector = new ServerConnector(server);
server.addConnector(connector);
StatisticsHandler statsHandler = new StatisticsHandler();
// Create a ContextHandlerCollection to hold contexts.
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
// Link the ContextHandlerCollection to the StatisticsHandler.
StatisticsHandler statsHandler = new StatisticsHandler(contextCollection);
// Link the StatisticsHandler to the Server.
server.setHandler(statsHandler);
// Create a ContextHandlerCollection to hold contexts.
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
// Link the ContextHandlerCollection to the StatisticsHandler.
statsHandler.setHandler(contextCollection);
server.start();
// end::statisticsHandler[]
}
@ -955,17 +896,15 @@ public class HTTPServerDocs
ServerConnector connector = new ServerConnector(server);
server.addConnector(connector);
// Create the MinimumDataRateHandler with a minimum read rate of 1KB per second and no minimum write rate.
StatisticsHandler.MinimumDataRateHandler dataRateHandler = new StatisticsHandler.MinimumDataRateHandler(1024L, 0L);
// Create a ContextHandlerCollection to hold contexts.
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
// Create the MinimumDataRateHandler linked the ContextHandlerCollection with a minimum read rate of 1KB per second and no minimum write rate.
StatisticsHandler.MinimumDataRateHandler dataRateHandler = new StatisticsHandler.MinimumDataRateHandler(contextCollection, 1024L, 0L);
// Link the MinimumDataRateHandler to the Server.
server.setHandler(dataRateHandler);
// Create a ContextHandlerCollection to hold contexts.
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
// Link the ContextHandlerCollection to the MinimumDataRateHandler.
dataRateHandler.setHandler(contextCollection);
server.start();
// end::dataRateHandler[]
}
@ -1006,16 +945,15 @@ public class HTTPServerDocs
secureConnector.setPort(8443);
server.addConnector(secureConnector);
SecuredRedirectHandler securedHandler = new SecuredRedirectHandler();
// Create a ContextHandlerCollection to hold contexts.
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
// Link the ContextHandlerCollection to the SecuredRedirectHandler.
SecuredRedirectHandler securedHandler = new SecuredRedirectHandler(contextCollection);
// Link the SecuredRedirectHandler to the Server.
server.setHandler(securedHandler);
// Create a ContextHandlerCollection to hold contexts.
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
// Link the ContextHandlerCollection to the StatisticsHandler.
securedHandler.setHandler(contextCollection);
server.start();
// end::securedHandler[]
}

View File

@ -78,7 +78,7 @@ public class SessionDocs
//tag:schsession[]
Server server = new Server();
ServletContextHandler context = new ServletContextHandler(server, "/foo", ServletContextHandler.SESSIONS);
ServletContextHandler context = new ServletContextHandler("/foo", ServletContextHandler.SESSIONS);
SessionHandler sessions = context.getSessionHandler();
//make idle sessions valid for only 5mins
sessions.setMaxInactiveInterval(300);

View File

@ -63,7 +63,7 @@ public class WebSocketServerDocs
Server server = new Server(8080);
// Create a ServletContextHandler with the given context path.
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
ServletContextHandler handler = new ServletContextHandler("/ctx");
server.setHandler(handler);
// Ensure that JavaxWebSocketServletContainerInitializer is initialized,
@ -82,7 +82,7 @@ public class WebSocketServerDocs
Server server = new Server(8080);
// Create a ServletContextHandler with the given context path.
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
ServletContextHandler handler = new ServletContextHandler("/ctx");
server.setHandler(handler);
// Ensure that JavaxWebSocketServletContainerInitializer is initialized,
@ -137,7 +137,7 @@ public class WebSocketServerDocs
Server server = new Server(8080);
// Create a ServletContextHandler with the given context path.
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
ServletContextHandler handler = new ServletContextHandler("/ctx");
server.setHandler(handler);
// Setup the ServerContainer and the WebSocket endpoints for this web application context.
@ -169,7 +169,7 @@ public class WebSocketServerDocs
Server server = new Server(8080);
// Create a ServletContextHandler with the given context path.
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
ServletContextHandler handler = new ServletContextHandler("/ctx");
server.setHandler(handler);
// Ensure that JettyWebSocketServletContainerInitializer is initialized,
@ -188,7 +188,7 @@ public class WebSocketServerDocs
Server server = new Server(8080);
// Create a ServletContextHandler with the given context path.
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
ServletContextHandler handler = new ServletContextHandler("/ctx");
server.setHandler(handler);
// Ensure that JettyWebSocketServletContainerInitializer is initialized,
@ -234,7 +234,7 @@ public class WebSocketServerDocs
Server server = new Server(8080);
// Create a ServletContextHandler with the given context path.
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
ServletContextHandler handler = new ServletContextHandler("/ctx");
server.setHandler(handler);
// Setup the JettyWebSocketServerContainer and the WebSocket endpoints for this web application context.
@ -301,7 +301,7 @@ public class WebSocketServerDocs
Server server = new Server(8080);
// Create a ServletContextHandler with the given context path.
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
ServletContextHandler handler = new ServletContextHandler("/ctx");
server.setHandler(handler);
// Setup the JettyWebSocketServerContainer to initialize WebSocket components.
@ -357,7 +357,8 @@ public class WebSocketServerDocs
Server server = new Server(8080);
// tag::uriTemplatePathSpec[]
ServletContextHandler handler = new ServletContextHandler(server, "/ctx");
ServletContextHandler handler = new ServletContextHandler("/ctx");
server.setHandler(handler);
// Configure the JettyWebSocketServerContainer.
JettyWebSocketServletContainerInitializer.configure(handler, (servletContext, container) ->

View File

@ -9,7 +9,7 @@
<name>Core :: ALPN :: Client</name>
<properties>
<bundle-symbolic-name>${project.groupId}.alpn.client</bundle-symbolic-name>
<spotbugs.onlyAnalyze>org.eclipse.alpn.*</spotbugs.onlyAnalyze>
<spotbugs.onlyAnalyze>org.eclipse.jetty.alpn.*</spotbugs.onlyAnalyze>
</properties>
<build>
<plugins>

View File

@ -9,7 +9,7 @@
<name>Core :: ALPN :: Server</name>
<properties>
<bundle-symbolic-name>${project.groupId}.alpn.server</bundle-symbolic-name>
<spotbugs.onlyAnalyze>org.eclipse.alpn.*</spotbugs.onlyAnalyze>
<spotbugs.onlyAnalyze>org.eclipse.jetty.alpn.*</spotbugs.onlyAnalyze>
</properties>
<build>
<plugins>

View File

@ -11,7 +11,7 @@
<properties>
<bundle-symbolic-name>${project.groupId}.client</bundle-symbolic-name>
<jetty.test.policy.loc>target/test-policy</jetty.test.policy.loc>
<spotbugs.onlyAnalyze>org.eclipse.client.*</spotbugs.onlyAnalyze>
<spotbugs.onlyAnalyze>org.eclipse.jetty.client.*</spotbugs.onlyAnalyze>
</properties>
<build>

View File

@ -175,9 +175,9 @@ public interface Response
contentSource.demand(demandCallback);
return;
}
if (chunk instanceof Content.Chunk.Error error)
if (Content.Chunk.isFailure(chunk))
{
response.abort(error.getCause());
response.abort(chunk.getFailure());
return;
}
if (chunk.isLast() && !chunk.hasRemaining())

View File

@ -554,7 +554,7 @@ public abstract class HttpReceiver
_chunk = inputChunk;
if (_chunk == null)
return null;
if (_chunk instanceof Content.Chunk.Error)
if (Content.Chunk.isFailure(_chunk))
return _chunk;
// Retain the input chunk because its ByteBuffer will be referenced by the Inflater.
@ -748,7 +748,7 @@ public abstract class HttpReceiver
LOG.debug("Erroring {}", this);
try (AutoLock ignored = lock.lock())
{
if (currentChunk instanceof Content.Chunk.Error)
if (Content.Chunk.isFailure(currentChunk))
return false;
if (currentChunk != null)
currentChunk.release();

View File

@ -503,8 +503,8 @@ public abstract class HttpSender
}
}
if (chunk instanceof Content.Chunk.Error error)
throw error.getCause();
if (Content.Chunk.isFailure(chunk))
throw chunk.getFailure();
ByteBuffer buffer = chunk.getByteBuffer();
contentBuffer = buffer.asReadOnlyBuffer();

View File

@ -676,7 +676,7 @@ public class ResponseListeners
Content.Chunk currentChunk = chunk;
if (LOG.isDebugEnabled())
LOG.debug("Content source #{} fail while current chunk is {}", index, currentChunk);
if (currentChunk instanceof Content.Chunk.Error)
if (Content.Chunk.isFailure(currentChunk))
return;
if (currentChunk != null)
currentChunk.release();

View File

@ -176,8 +176,8 @@ public class ConnectionPoolTest
continue;
}
}
if (chunk instanceof Content.Chunk.Error error)
throw error.getCause();
if (Content.Chunk.isFailure(chunk))
throw chunk.getFailure();
if (chunk.hasRemaining())
{

View File

@ -30,7 +30,6 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class HttpClientAsyncContentTest extends AbstractHttpClientServerTest
@ -248,11 +247,11 @@ public class HttpClientAsyncContentTest extends AbstractHttpClientServerTest
.onResponseContentSource((response, contentSource) -> response.abort(new Throwable()).whenComplete((failed, x) ->
{
Content.Chunk chunk = contentSource.read();
assertInstanceOf(Content.Chunk.Error.class, chunk);
assertTrue(Content.Chunk.isFailure(chunk, true));
contentSource.demand(() ->
{
Content.Chunk c = contentSource.read();
assertInstanceOf(Content.Chunk.Error.class, c);
assertTrue(Content.Chunk.isFailure(c, true));
errorContentLatch.countDown();
});
}))

View File

@ -439,12 +439,12 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
assertEquals("multipart/form-data", HttpField.valueParameters(contentType, null));
String boundary = MultiPart.extractBoundary(contentType);
MultiPartFormData formData = new MultiPartFormData(boundary);
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
formData.setFilesDirectory(tmpDir);
formData.parse(request);
try
{
process(formData.join()); // May block waiting for multipart form data.
process(formData.parse(request).join()); // May block waiting for multipart form data.
response.write(true, BufferUtil.EMPTY_BUFFER, callback);
}
catch (Exception x)

View File

@ -12,7 +12,7 @@
<properties>
<bundle-symbolic-name>${project.groupId}.deploy</bundle-symbolic-name>
<spotbugs.onlyAnalyze>org.eclipse.deploy.*</spotbugs.onlyAnalyze>
<spotbugs.onlyAnalyze>org.eclipse.jetty.deploy.*</spotbugs.onlyAnalyze>
</properties>
<build>

View File

@ -197,7 +197,7 @@ public class HttpStreamOverFCGI implements HttpStream
{
if (_chunk == null)
_chunk = Content.Chunk.EOF;
else if (!_chunk.isLast() && !(_chunk instanceof Content.Chunk.Error))
else if (!_chunk.isLast() && !(Content.Chunk.isFailure(_chunk)))
throw new IllegalStateException();
}

View File

@ -1204,6 +1204,167 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
// return a merged header with missing ensured values added
return new HttpField(ensure.getHeader(), ensure.getName(), v.toString());
}
/**
* A wrapper of {@link HttpFields}.
*/
class Wrapper implements Mutable
{
private final Mutable _fields;
public Wrapper(Mutable fields)
{
_fields = fields;
}
/**
* Called when a field is added (including as part of a put).
* @param field The field being added.
* @return The field to add, or null if the add is to be ignored.
*/
public HttpField onAddField(HttpField field)
{
return field;
}
/**
* Called when a field is removed (including as part of a put).
* @param field The field being removed.
* @return True if the field should be removed, false otherwise.
*/
public boolean onRemoveField(HttpField field)
{
return true;
}
@Override
public HttpFields takeAsImmutable()
{
return Mutable.super.takeAsImmutable();
}
@Override
public int size()
{
// This impl needed only as an optimization
return _fields.size();
}
@Override
public Stream<HttpField> stream()
{
// This impl needed only as an optimization
return _fields.stream();
}
@Override
public Mutable add(HttpField field)
{
// This impl needed only as an optimization
if (field != null)
{
field = onAddField(field);
if (field != null)
return _fields.add(field);
}
return this;
}
@Override
public ListIterator<HttpField> listIterator()
{
ListIterator<HttpField> i = _fields.listIterator();
return new ListIterator<>()
{
HttpField last;
@Override
public boolean hasNext()
{
return i.hasNext();
}
@Override
public HttpField next()
{
return last = i.next();
}
@Override
public boolean hasPrevious()
{
return i.hasPrevious();
}
@Override
public HttpField previous()
{
return last = i.previous();
}
@Override
public int nextIndex()
{
return i.nextIndex();
}
@Override
public int previousIndex()
{
return i.previousIndex();
}
@Override
public void remove()
{
if (last != null && onRemoveField(last))
{
last = null;
i.remove();
}
}
@Override
public void set(HttpField field)
{
if (field == null)
{
if (last != null && onRemoveField(last))
{
last = null;
i.remove();
}
}
else
{
if (last != null && onRemoveField(last))
{
field = onAddField(field);
if (field != null)
{
last = null;
i.set(field);
}
}
}
}
@Override
public void add(HttpField field)
{
if (field != null)
{
field = onAddField(field);
if (field != null)
{
last = null;
i.add(field);
}
}
}
};
}
}
}
/**
@ -1391,8 +1552,12 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
public int hashCode()
{
int hash = 0;
for (int i = _fields.length; i-- > 0; )
hash ^= _fields[i].hashCode();
for (int i = _size; i-- > 0; )
{
HttpField field = _fields[i];
if (field != null)
hash ^= field.hashCode();
}
return hash;
}

View File

@ -18,12 +18,14 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import org.eclipse.jetty.util.FileID;
import org.eclipse.jetty.util.Index;
@ -37,6 +39,12 @@ import org.slf4j.LoggerFactory;
public class MimeTypes
{
static final Logger LOG = LoggerFactory.getLogger(MimeTypes.class);
private static final Set<Locale> KNOWN_LOCALES = Set.copyOf(Arrays.asList(Locale.getAvailableLocales()));
public static boolean isKnownLocale(Locale locale)
{
return KNOWN_LOCALES.contains(locale);
}
/** Enumeration of predefined MimeTypes. This is not exhaustive */
public enum Type

View File

@ -563,7 +563,7 @@ public class MultiPart
private State state = State.FIRST;
private boolean closed;
private Runnable demand;
private Content.Chunk.Error errorChunk;
private Content.Chunk errorChunk;
private Part part;
public AbstractContentSource(String boundary)
@ -759,7 +759,7 @@ public class MultiPart
case CONTENT ->
{
Content.Chunk chunk = part.getContentSource().read();
if (chunk == null || chunk instanceof Content.Chunk.Error)
if (chunk == null || Content.Chunk.isFailure(chunk))
yield chunk;
if (!chunk.isLast())
yield chunk;

View File

@ -23,12 +23,12 @@ import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.ContentSourceCompletableFuture;
import org.eclipse.jetty.util.thread.AutoLock;
/**
* <p>A {@link CompletableFuture} that is completed when a multipart/byteranges
* content has been parsed asynchronously from a {@link Content.Source} via
* {@link #parse(Content.Source)}.</p>
* has been parsed asynchronously from a {@link Content.Source}.</p>
* <p>Once the parsing of the multipart/byteranges content completes successfully,
* objects of this class are completed with a {@link MultiPartByteRanges.Parts}
* object.</p>
@ -52,75 +52,10 @@ import org.eclipse.jetty.util.thread.AutoLock;
*
* @see Parts
*/
public class MultiPartByteRanges extends CompletableFuture<MultiPartByteRanges.Parts>
public class MultiPartByteRanges
{
private final PartsListener listener = new PartsListener();
private final MultiPart.Parser parser;
public MultiPartByteRanges(String boundary)
private MultiPartByteRanges()
{
this.parser = new MultiPart.Parser(boundary, listener);
}
/**
* @return the boundary string
*/
public String getBoundary()
{
return parser.getBoundary();
}
@Override
public boolean completeExceptionally(Throwable failure)
{
listener.fail(failure);
return super.completeExceptionally(failure);
}
/**
* <p>Parses the given multipart/byteranges content.</p>
* <p>Returns this {@code MultiPartByteRanges} object,
* so that it can be used in the typical "fluent" style
* of {@link CompletableFuture}.</p>
*
* @param content the multipart/byteranges content to parse
* @return this {@code MultiPartByteRanges} object
*/
public MultiPartByteRanges parse(Content.Source content)
{
new Runnable()
{
@Override
public void run()
{
while (true)
{
Content.Chunk chunk = content.read();
if (chunk == null)
{
content.demand(this);
return;
}
if (chunk instanceof Content.Chunk.Error error)
{
listener.onFailure(error.getCause());
return;
}
parse(chunk);
chunk.release();
if (chunk.isLast() || isDone())
return;
}
}
}.run();
return this;
}
private void parse(Content.Chunk chunk)
{
if (listener.isFailed())
return;
parser.parse(chunk);
}
/**
@ -267,76 +202,123 @@ public class MultiPartByteRanges extends CompletableFuture<MultiPartByteRanges.P
}
}
private class PartsListener extends MultiPart.AbstractPartsListener
public static class Parser
{
private final AutoLock lock = new AutoLock();
private final List<Content.Chunk> partChunks = new ArrayList<>();
private final List<MultiPart.Part> parts = new ArrayList<>();
private Throwable failure;
private final PartsListener listener = new PartsListener();
private final MultiPart.Parser parser;
private Parts parts;
private boolean isFailed()
public Parser(String boundary)
{
try (AutoLock ignored = lock.lock())
{
return failure != null;
}
parser = new MultiPart.Parser(boundary, listener);
}
@Override
public void onPartContent(Content.Chunk chunk)
public CompletableFuture<MultiPartByteRanges.Parts> parse(Content.Source content)
{
try (AutoLock ignored = lock.lock())
ContentSourceCompletableFuture<MultiPartByteRanges.Parts> futureParts = new ContentSourceCompletableFuture<>(content)
{
// Retain the chunk because it is stored for later use.
chunk.retain();
partChunks.add(chunk);
}
@Override
protected MultiPartByteRanges.Parts parse(Content.Chunk chunk) throws Throwable
{
if (listener.isFailed())
throw listener.failure;
parser.parse(chunk);
if (listener.isFailed())
throw listener.failure;
return parts;
}
@Override
public boolean completeExceptionally(Throwable failure)
{
boolean failed = super.completeExceptionally(failure);
if (failed)
listener.fail(failure);
return failed;
}
};
futureParts.parse();
return futureParts;
}
@Override
public void onPart(String name, String fileName, HttpFields headers)
/**
* @return the boundary string
*/
public String getBoundary()
{
try (AutoLock ignored = lock.lock())
{
parts.add(new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks)));
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
}
return parser.getBoundary();
}
@Override
public void onComplete()
private class PartsListener extends MultiPart.AbstractPartsListener
{
super.onComplete();
List<MultiPart.Part> copy;
try (AutoLock ignored = lock.lock())
{
copy = List.copyOf(parts);
}
complete(new Parts(getBoundary(), copy));
}
private final AutoLock lock = new AutoLock();
private final List<Content.Chunk> partChunks = new ArrayList<>();
private final List<MultiPart.Part> parts = new ArrayList<>();
private Throwable failure;
@Override
public void onFailure(Throwable failure)
{
super.onFailure(failure);
completeExceptionally(failure);
}
private void fail(Throwable cause)
{
List<MultiPart.Part> partsToFail;
try (AutoLock ignored = lock.lock())
private boolean isFailed()
{
if (failure != null)
return;
failure = cause;
partsToFail = List.copyOf(parts);
parts.clear();
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
try (AutoLock ignored = lock.lock())
{
return failure != null;
}
}
@Override
public void onPartContent(Content.Chunk chunk)
{
try (AutoLock ignored = lock.lock())
{
// Retain the chunk because it is stored for later use.
chunk.retain();
partChunks.add(chunk);
}
}
@Override
public void onPart(String name, String fileName, HttpFields headers)
{
try (AutoLock ignored = lock.lock())
{
parts.add(new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks)));
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
}
}
@Override
public void onComplete()
{
super.onComplete();
List<MultiPart.Part> copy;
try (AutoLock ignored = lock.lock())
{
copy = List.copyOf(parts);
Parser.this.parts = new Parts(getBoundary(), copy);
}
}
@Override
public void onFailure(Throwable failure)
{
fail(failure);
}
private void fail(Throwable cause)
{
List<MultiPart.Part> partsToFail;
try (AutoLock ignored = lock.lock())
{
if (failure != null)
return;
failure = cause;
partsToFail = List.copyOf(parts);
parts.clear();
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
}
partsToFail.forEach(p -> p.fail(cause));
}
partsToFail.forEach(p -> p.fail(cause));
}
}
}

View File

@ -26,8 +26,11 @@ import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.ContentSourceCompletableFuture;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger;
@ -37,8 +40,7 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
/**
* <p>A {@link CompletableFuture} that is completed when a multipart/form-data content
* has been parsed asynchronously from a {@link Content.Source} via {@link #parse(Content.Source)}
* or from one or more {@link Content.Chunk}s via {@link #parse(Content.Chunk)}.</p>
* has been parsed asynchronously from a {@link Content.Source}.</p>
* <p>Once the parsing of the multipart/form-data content completes successfully,
* objects of this class are completed with a {@link Parts} object.</p>
* <p>Objects of this class may be configured to save multipart files in a configurable
@ -67,241 +69,31 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
*
* @see Parts
*/
public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts>
public class MultiPartFormData
{
private static final Logger LOG = LoggerFactory.getLogger(MultiPartFormData.class);
private final PartsListener listener = new PartsListener();
private final MultiPart.Parser parser;
private boolean useFilesForPartsWithoutFileName;
private Path filesDirectory;
private long maxFileSize = -1;
private long maxMemoryFileSize;
private long maxLength = -1;
private long length;
public MultiPartFormData(String boundary)
private MultiPartFormData()
{
parser = new MultiPart.Parser(Objects.requireNonNull(boundary), listener);
}
/**
* @return the boundary string
*/
public String getBoundary()
public static CompletableFuture<Parts> from(Attributes attributes, String boundary, Function<Parser, CompletableFuture<Parts>> parse)
{
return parser.getBoundary();
}
/**
* <p>Parses the given multipart/form-data content.</p>
* <p>Returns this {@code MultiPartFormData} object,
* so that it can be used in the typical "fluent"
* style of {@link CompletableFuture}.</p>
*
* @param content the multipart/form-data content to parse
* @return this {@code MultiPartFormData} object
*/
public MultiPartFormData parse(Content.Source content)
{
new Runnable()
@SuppressWarnings("unchecked")
CompletableFuture<Parts> futureParts = (CompletableFuture<Parts>)attributes.getAttribute(MultiPartFormData.class.getName());
if (futureParts == null)
{
@Override
public void run()
{
while (true)
{
Content.Chunk chunk = content.read();
if (chunk == null)
{
content.demand(this);
return;
}
if (chunk instanceof Content.Chunk.Error error)
{
listener.onFailure(error.getCause());
return;
}
parse(chunk);
chunk.release();
if (chunk.isLast() || isDone())
return;
}
}
}.run();
return this;
}
/**
* <p>Parses the given chunk containing multipart/form-data bytes.</p>
* <p>One or more chunks may be passed to this method, until the parsing
* of the multipart/form-data content completes.</p>
*
* @param chunk the {@link Content.Chunk} to parse.
*/
public void parse(Content.Chunk chunk)
{
if (listener.isFailed())
return;
length += chunk.getByteBuffer().remaining();
long max = getMaxLength();
if (max > 0 && length > max)
listener.onFailure(new IllegalStateException("max length exceeded: %d".formatted(max)));
else
parser.parse(chunk);
}
/**
* <p>Returns the default charset as specified by
* <a href="https://datatracker.ietf.org/doc/html/rfc7578#section-4.6">RFC 7578, section 4.6</a>,
* that is the charset specified by the part named {@code _charset_}.</p>
* <p>If that part is not present, returns {@code null}.</p>
*
* @return the default charset specified by the {@code _charset_} part,
* or null if that part is not present
*/
public Charset getDefaultCharset()
{
return listener.getDefaultCharset();
}
/**
* @return the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length
*/
public int getPartHeadersMaxLength()
{
return parser.getPartHeadersMaxLength();
}
/**
* @param partHeadersMaxLength the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length
*/
public void setPartHeadersMaxLength(int partHeadersMaxLength)
{
parser.setPartHeadersMaxLength(partHeadersMaxLength);
}
/**
* @return whether parts without fileName may be stored as files
*/
public boolean isUseFilesForPartsWithoutFileName()
{
return useFilesForPartsWithoutFileName;
}
/**
* @param useFilesForPartsWithoutFileName whether parts without fileName may be stored as files
*/
public void setUseFilesForPartsWithoutFileName(boolean useFilesForPartsWithoutFileName)
{
this.useFilesForPartsWithoutFileName = useFilesForPartsWithoutFileName;
}
/**
* @return the directory where files are saved
*/
public Path getFilesDirectory()
{
return filesDirectory;
}
/**
* <p>Sets the directory where the files uploaded in the parts will be saved.</p>
*
* @param filesDirectory the directory where files are saved
*/
public void setFilesDirectory(Path filesDirectory)
{
this.filesDirectory = filesDirectory;
}
/**
* @return the maximum file size in bytes, or -1 for unlimited file size
*/
public long getMaxFileSize()
{
return maxFileSize;
}
/**
* @param maxFileSize the maximum file size in bytes, or -1 for unlimited file size
*/
public void setMaxFileSize(long maxFileSize)
{
this.maxFileSize = maxFileSize;
}
/**
* @return the maximum memory file size in bytes, or -1 for unlimited memory file size
*/
public long getMaxMemoryFileSize()
{
return maxMemoryFileSize;
}
/**
* <p>Sets the maximum memory file size in bytes, after which files will be saved
* in the directory specified by {@link #setFilesDirectory(Path)}.</p>
* <p>Use value {@code 0} to always save the files in the directory.</p>
* <p>Use value {@code -1} to never save the files in the directory.</p>
*
* @param maxMemoryFileSize the maximum memory file size in bytes, or -1 for unlimited memory file size
*/
public void setMaxMemoryFileSize(long maxMemoryFileSize)
{
this.maxMemoryFileSize = maxMemoryFileSize;
}
/**
* @return the maximum length in bytes of the whole multipart content, or -1 for unlimited length
*/
public long getMaxLength()
{
return maxLength;
}
/**
* @param maxLength the maximum length in bytes of the whole multipart content, or -1 for unlimited length
*/
public void setMaxLength(long maxLength)
{
this.maxLength = maxLength;
}
/**
* @return the maximum number of parts that can be parsed from the multipart content.
*/
public long getMaxParts()
{
return parser.getMaxParts();
}
/**
* @param maxParts the maximum number of parts that can be parsed from the multipart content.
*/
public void setMaxParts(long maxParts)
{
parser.setMaxParts(maxParts);
}
@Override
public boolean completeExceptionally(Throwable failure)
{
listener.fail(failure);
return super.completeExceptionally(failure);
}
// Only used for testing.
int getPartsSize()
{
return listener.getPartsSize();
futureParts = parse.apply(new Parser(boundary));
attributes.setAttribute(MultiPartFormData.class.getName(), futureParts);
}
return futureParts;
}
/**
* <p>An ordered list of {@link MultiPart.Part}s that can
* be accessed by index or by name, or iterated over.</p>
*/
public class Parts implements Iterable<MultiPart.Part>, Closeable
public static class Parts implements Iterable<MultiPart.Part>, Closeable
{
private final List<MultiPart.Part> parts;
@ -310,11 +102,6 @@ public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts
this.parts = parts;
}
public MultiPartFormData getMultiPartFormData()
{
return MultiPartFormData.this;
}
/**
* <p>Returns the {@link MultiPart.Part} at the given index, a number
* between {@code 0} included and the value returned by {@link #size()}
@ -409,251 +196,447 @@ public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts
}
}
private class PartsListener extends MultiPart.AbstractPartsListener
public static class Parser
{
private final AutoLock lock = new AutoLock();
private final List<MultiPart.Part> parts = new ArrayList<>();
private final List<Content.Chunk> partChunks = new ArrayList<>();
private long fileSize;
private long memoryFileSize;
private Path filePath;
private SeekableByteChannel fileChannel;
private Throwable failure;
private final PartsListener listener = new PartsListener();
private final MultiPart.Parser parser;
private boolean useFilesForPartsWithoutFileName;
private Path filesDirectory;
private long maxFileSize = -1;
private long maxMemoryFileSize;
private long maxLength = -1;
private long length;
private Parts parts;
@Override
public void onPartContent(Content.Chunk chunk)
public Parser(String boundary)
{
ByteBuffer buffer = chunk.getByteBuffer();
String fileName = getFileName();
if (fileName != null || isUseFilesForPartsWithoutFileName())
parser = new MultiPart.Parser(Objects.requireNonNull(boundary), listener);
}
public CompletableFuture<Parts> parse(Content.Source content)
{
ContentSourceCompletableFuture<Parts> futureParts = new ContentSourceCompletableFuture<>(content)
{
long maxFileSize = getMaxFileSize();
fileSize += buffer.remaining();
if (maxFileSize >= 0 && fileSize > maxFileSize)
@Override
protected Parts parse(Content.Chunk chunk) throws Throwable
{
onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxFileSize)));
return;
if (listener.isFailed())
throw listener.failure;
length += chunk.getByteBuffer().remaining();
long max = getMaxLength();
if (max >= 0 && length > max)
throw new IllegalStateException("max length exceeded: %d".formatted(max));
parser.parse(chunk);
if (listener.isFailed())
throw listener.failure;
return parts;
}
long maxMemoryFileSize = getMaxMemoryFileSize();
if (maxMemoryFileSize >= 0)
@Override
public boolean completeExceptionally(Throwable failure)
{
memoryFileSize += buffer.remaining();
if (memoryFileSize > maxMemoryFileSize)
{
try
{
// Must save to disk.
if (ensureFileChannel())
{
// Write existing memory chunks.
List<Content.Chunk> partChunks;
try (AutoLock ignored = lock.lock())
{
partChunks = List.copyOf(this.partChunks);
}
for (Content.Chunk c : partChunks)
{
write(c.getByteBuffer());
}
}
write(buffer);
if (chunk.isLast())
close();
}
catch (Throwable x)
{
onFailure(x);
}
try (AutoLock ignored = lock.lock())
{
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
}
return;
}
boolean failed = super.completeExceptionally(failure);
if (failed)
listener.fail(failure);
return failed;
}
}
// Retain the chunk because it is stored for later use.
chunk.retain();
try (AutoLock ignored = lock.lock())
{
partChunks.add(chunk);
}
};
futureParts.parse();
return futureParts;
}
private void write(ByteBuffer buffer) throws Exception
/**
* @return the boundary string
*/
public String getBoundary()
{
int remaining = buffer.remaining();
while (remaining > 0)
{
SeekableByteChannel channel = fileChannel();
if (channel == null)
throw new IllegalStateException();
int written = channel.write(buffer);
if (written == 0)
throw new NonWritableChannelException();
remaining -= written;
}
return parser.getBoundary();
}
private void close()
/**
* <p>Returns the default charset as specified by
* <a href="https://datatracker.ietf.org/doc/html/rfc7578#section-4.6">RFC 7578, section 4.6</a>,
* that is the charset specified by the part named {@code _charset_}.</p>
* <p>If that part is not present, returns {@code null}.</p>
*
* @return the default charset specified by the {@code _charset_} part,
* or null if that part is not present
*/
public Charset getDefaultCharset()
{
try
{
Closeable closeable = fileChannel();
if (closeable != null)
closeable.close();
}
catch (Throwable x)
{
onFailure(x);
}
return listener.getDefaultCharset();
}
@Override
public void onPart(String name, String fileName, HttpFields headers)
/**
* @return the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length
*/
public int getPartHeadersMaxLength()
{
fileSize = 0;
memoryFileSize = 0;
try (AutoLock ignored = lock.lock())
{
MultiPart.Part part;
if (fileChannel != null)
part = new MultiPart.PathPart(name, fileName, headers, filePath);
else
part = new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks));
// Reset part-related state.
filePath = null;
fileChannel = null;
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
// Store the new part.
parts.add(part);
}
return parser.getPartHeadersMaxLength();
}
@Override
public void onComplete()
/**
* @param partHeadersMaxLength the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length
*/
public void setPartHeadersMaxLength(int partHeadersMaxLength)
{
super.onComplete();
List<MultiPart.Part> result;
try (AutoLock ignored = lock.lock())
{
result = List.copyOf(parts);
}
complete(new Parts(result));
parser.setPartHeadersMaxLength(partHeadersMaxLength);
}
Charset getDefaultCharset()
/**
* @return whether parts without fileName may be stored as files
*/
public boolean isUseFilesForPartsWithoutFileName()
{
try (AutoLock ignored = lock.lock())
{
return parts.stream()
.filter(part -> "_charset_".equals(part.getName()))
.map(part -> part.getContentAsString(US_ASCII))
.map(Charset::forName)
.findFirst()
.orElse(null);
}
return useFilesForPartsWithoutFileName;
}
/**
* @param useFilesForPartsWithoutFileName whether parts without fileName may be stored as files
*/
public void setUseFilesForPartsWithoutFileName(boolean useFilesForPartsWithoutFileName)
{
this.useFilesForPartsWithoutFileName = useFilesForPartsWithoutFileName;
}
/**
* @return the directory where files are saved
*/
public Path getFilesDirectory()
{
return filesDirectory;
}
/**
* <p>Sets the directory where the files uploaded in the parts will be saved.</p>
*
* @param filesDirectory the directory where files are saved
*/
public void setFilesDirectory(Path filesDirectory)
{
this.filesDirectory = filesDirectory;
}
/**
* @return the maximum file size in bytes, or -1 for unlimited file size
*/
public long getMaxFileSize()
{
return maxFileSize;
}
/**
* @param maxFileSize the maximum file size in bytes, or -1 for unlimited file size
*/
public void setMaxFileSize(long maxFileSize)
{
this.maxFileSize = maxFileSize;
}
/**
* @return the maximum memory file size in bytes, or -1 for unlimited memory file size
*/
public long getMaxMemoryFileSize()
{
return maxMemoryFileSize;
}
/**
* <p>Sets the maximum memory file size in bytes, after which files will be saved
* in the directory specified by {@link #setFilesDirectory(Path)}.</p>
* <p>Use value {@code 0} to always save the files in the directory.</p>
* <p>Use value {@code -1} to never save the files in the directory.</p>
*
* @param maxMemoryFileSize the maximum memory file size in bytes, or -1 for unlimited memory file size
*/
public void setMaxMemoryFileSize(long maxMemoryFileSize)
{
this.maxMemoryFileSize = maxMemoryFileSize;
}
/**
* @return the maximum length in bytes of the whole multipart content, or -1 for unlimited length
*/
public long getMaxLength()
{
return maxLength;
}
/**
* @param maxLength the maximum length in bytes of the whole multipart content, or -1 for unlimited length
*/
public void setMaxLength(long maxLength)
{
this.maxLength = maxLength;
}
/**
* @return the maximum number of parts that can be parsed from the multipart content.
*/
public long getMaxParts()
{
return parser.getMaxParts();
}
/**
* @param maxParts the maximum number of parts that can be parsed from the multipart content.
*/
public void setMaxParts(long maxParts)
{
parser.setMaxParts(maxParts);
}
// Only used for testing.
int getPartsSize()
{
try (AutoLock ignored = lock.lock())
{
return parts.size();
}
return listener.getPartsSize();
}
@Override
public void onFailure(Throwable failure)
private class PartsListener extends MultiPart.AbstractPartsListener
{
super.onFailure(failure);
completeExceptionally(failure);
}
private final AutoLock lock = new AutoLock();
private final List<MultiPart.Part> parts = new ArrayList<>();
private final List<Content.Chunk> partChunks = new ArrayList<>();
private long fileSize;
private long memoryFileSize;
private Path filePath;
private SeekableByteChannel fileChannel;
private Throwable failure;
private void fail(Throwable cause)
{
List<MultiPart.Part> partsToFail;
try (AutoLock ignored = lock.lock())
@Override
public void onPartContent(Content.Chunk chunk)
{
if (failure != null)
return;
failure = cause;
partsToFail = List.copyOf(parts);
parts.clear();
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
}
partsToFail.forEach(p -> p.fail(cause));
close();
delete();
}
ByteBuffer buffer = chunk.getByteBuffer();
String fileName = getFileName();
if (fileName != null || isUseFilesForPartsWithoutFileName())
{
long maxFileSize = getMaxFileSize();
fileSize += buffer.remaining();
if (maxFileSize >= 0 && fileSize > maxFileSize)
{
onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxFileSize)));
return;
}
private SeekableByteChannel fileChannel()
{
try (AutoLock ignored = lock.lock())
{
return fileChannel;
}
}
long maxMemoryFileSize = getMaxMemoryFileSize();
if (maxMemoryFileSize >= 0)
{
memoryFileSize += buffer.remaining();
if (memoryFileSize > maxMemoryFileSize)
{
try
{
// Must save to disk.
if (ensureFileChannel())
{
// Write existing memory chunks.
List<Content.Chunk> partChunks;
try (AutoLock ignored = lock.lock())
{
partChunks = List.copyOf(this.partChunks);
}
for (Content.Chunk c : partChunks)
{
write(c.getByteBuffer());
}
}
write(buffer);
if (chunk.isLast())
close();
}
catch (Throwable x)
{
onFailure(x);
}
private void delete()
{
try
{
Path path = null;
try (AutoLock ignored = lock.lock())
{
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
}
return;
}
}
}
// Retain the chunk because it is stored for later use.
chunk.retain();
try (AutoLock ignored = lock.lock())
{
if (filePath != null)
path = filePath;
partChunks.add(chunk);
}
}
private void write(ByteBuffer buffer) throws Exception
{
int remaining = buffer.remaining();
while (remaining > 0)
{
SeekableByteChannel channel = fileChannel();
if (channel == null)
throw new IllegalStateException();
int written = channel.write(buffer);
if (written == 0)
throw new NonWritableChannelException();
remaining -= written;
}
}
private void close()
{
try
{
Closeable closeable = fileChannel();
if (closeable != null)
closeable.close();
}
catch (Throwable x)
{
onFailure(x);
}
}
@Override
public void onPart(String name, String fileName, HttpFields headers)
{
fileSize = 0;
memoryFileSize = 0;
try (AutoLock ignored = lock.lock())
{
MultiPart.Part part;
if (fileChannel != null)
part = new MultiPart.PathPart(name, fileName, headers, filePath);
else
part = new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks));
// Reset part-related state.
filePath = null;
fileChannel = null;
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
// Store the new part.
parts.add(part);
}
if (path != null)
Files.delete(path);
}
catch (Throwable x)
{
if (LOG.isTraceEnabled())
LOG.trace("IGNORED", x);
}
}
private boolean isFailed()
{
try (AutoLock ignored = lock.lock())
@Override
public void onComplete()
{
return failure != null;
super.onComplete();
List<MultiPart.Part> result;
try (AutoLock ignored = lock.lock())
{
result = List.copyOf(parts);
Parser.this.parts = new Parts(result);
}
}
}
private boolean ensureFileChannel()
{
try (AutoLock ignored = lock.lock())
Charset getDefaultCharset()
{
if (fileChannel != null)
return false;
createFileChannel();
return true;
try (AutoLock ignored = lock.lock())
{
return parts.stream()
.filter(part -> "_charset_".equals(part.getName()))
.map(part -> part.getContentAsString(US_ASCII))
.map(Charset::forName)
.findFirst()
.orElse(null);
}
}
}
private void createFileChannel()
{
try (AutoLock ignored = lock.lock())
int getPartsSize()
{
Path directory = getFilesDirectory();
Files.createDirectories(directory);
String fileName = "MultiPart";
filePath = Files.createTempFile(directory, fileName, "");
fileChannel = Files.newByteChannel(filePath, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
try (AutoLock ignored = lock.lock())
{
return parts.size();
}
}
catch (Throwable x)
@Override
public void onFailure(Throwable failure)
{
onFailure(x);
fail(failure);
}
private void fail(Throwable cause)
{
List<MultiPart.Part> partsToFail;
try (AutoLock ignored = lock.lock())
{
if (failure != null)
return;
failure = cause;
partsToFail = List.copyOf(parts);
parts.clear();
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
}
partsToFail.forEach(p -> p.fail(cause));
close();
delete();
}
private SeekableByteChannel fileChannel()
{
try (AutoLock ignored = lock.lock())
{
return fileChannel;
}
}
private void delete()
{
try
{
Path path = null;
try (AutoLock ignored = lock.lock())
{
if (filePath != null)
path = filePath;
filePath = null;
fileChannel = null;
}
if (path != null)
Files.delete(path);
}
catch (Throwable x)
{
if (LOG.isTraceEnabled())
LOG.trace("IGNORED", x);
}
}
private boolean isFailed()
{
try (AutoLock ignored = lock.lock())
{
return failure != null;
}
}
private boolean ensureFileChannel()
{
try (AutoLock ignored = lock.lock())
{
if (fileChannel != null)
return false;
createFileChannel();
return true;
}
}
private void createFileChannel()
{
try (AutoLock ignored = lock.lock())
{
Path directory = getFilesDirectory();
Files.createDirectories(directory);
String fileName = "MultiPart";
filePath = Files.createTempFile(directory, fileName, "");
fileChannel = Files.newByteChannel(filePath, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
}
catch (Throwable x)
{
onFailure(x);
}
}
}
}

View File

@ -177,7 +177,7 @@ wml=text/vnd.wap.wml
wmlc=application/vnd.wap.wmlc
wmls=text/vnd.wap.wmlscript
wmlsc=application/vnd.wap.wmlscriptc
woff=application/font-woff
woff=font/woff
woff2=font/woff2
wrl=model/vrml
wtls-ca-certificate=application/vnd.wap.wtls-ca-certificate

View File

@ -54,6 +54,7 @@ public class HttpFieldsTest
return Stream.of(
HttpFields.build(),
HttpFields.build(0),
new HttpFields.Mutable.Wrapper(HttpFields.build()),
new HttpFields.Mutable()
{
private final HttpFields.Mutable fields = HttpFields.build();

View File

@ -16,19 +16,20 @@ package org.eclipse.jetty.http;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.AsyncContent;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
@ -71,32 +72,19 @@ public class MultiPartFormDataTest
int leaks = 0;
for (Content.Chunk chunk : _allocatedChunks)
{
// Any release that does not return true is a leak.
if (!chunk.release())
leaks++;
// Any release that does not throw or return true is a leak.
try
{
if (!chunk.release())
leaks++;
}
catch (IllegalStateException ignored)
{
}
}
assertThat("Leaked " + leaks + "/" + _allocatedChunks.size() + " chunk(s)", leaks, is(0));
}
Content.Chunk asChunk(String data, boolean last)
{
byte[] b = data.getBytes(StandardCharsets.UTF_8);
ByteBuffer buffer = BufferUtil.allocate(b.length);
BufferUtil.append(buffer, b);
Content.Chunk chunk = Content.Chunk.from(buffer, last);
_allocatedChunks.add(chunk);
return chunk;
}
Content.Chunk asChunk(ByteBuffer data, boolean last)
{
ByteBuffer buffer = BufferUtil.allocate(data.remaining());
BufferUtil.append(buffer, data);
Content.Chunk chunk = Content.Chunk.from(buffer, last);
_allocatedChunks.add(chunk);
return chunk;
}
@Test
public void testBadMultiPart() throws Exception
{
@ -109,14 +97,14 @@ public class MultiPartFormDataTest
"Content-Disposition: form-data; name=\"fileup\"; filename=\"test.upload\"\r\n" +
"\r\n";
MultiPartFormData formData = new MultiPartFormData(boundary);
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(asChunk(str, true));
formData.handle((parts, failure) ->
Content.Sink.write(source, true, str, Callback.NOOP);
formData.parse(source).handle((parts, failure) ->
{
assertNull(parts);
assertInstanceOf(BadMessageException.class, failure);
@ -139,14 +127,14 @@ public class MultiPartFormDataTest
eol +
"--" + boundary + "--" + eol;
MultiPartFormData formData = new MultiPartFormData(boundary);
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(asChunk(str, true));
formData.whenComplete((parts, failure) ->
Content.Sink.write(source, true, str, Callback.NOOP);
formData.parse(source).whenComplete((parts, failure) ->
{
// No errors and no parts.
assertNull(failure);
@ -165,14 +153,14 @@ public class MultiPartFormDataTest
String str = eol +
"--" + boundary + "--" + eol;
MultiPartFormData formData = new MultiPartFormData(boundary);
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(asChunk(str, true));
formData.whenComplete((parts, failure) ->
Content.Sink.write(source, true, str, Callback.NOOP);
formData.parse(source).whenComplete((parts, failure) ->
{
// No errors and no parts.
assertNull(failure);
@ -213,14 +201,14 @@ public class MultiPartFormDataTest
----\r
""";
MultiPartFormData formData = new MultiPartFormData("");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("");
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(asChunk(str, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, str, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertThat(parts.size(), is(4));
@ -253,10 +241,10 @@ public class MultiPartFormDataTest
@Test
public void testNoBody() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("boundary");
formData.parse(Content.Chunk.EOF);
formData.handle((parts, failure) ->
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("boundary");
source.close();
formData.parse(source).handle((parts, failure) ->
{
assertNull(parts);
assertNotNull(failure);
@ -268,11 +256,11 @@ public class MultiPartFormDataTest
@Test
public void testBodyWithOnlyCRLF() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("boundary");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("boundary");
String body = " \n\n\n\r\n\r\n\r\n\r\n";
formData.parse(asChunk(body, true));
formData.handle((parts, failure) ->
Content.Sink.write(source, true, body, Callback.NOOP);
formData.parse(source).handle((parts, failure) ->
{
assertNull(parts);
assertNotNull(failure);
@ -285,7 +273,7 @@ public class MultiPartFormDataTest
public void testLeadingWhitespaceBodyWithCRLF() throws Exception
{
String body = """
\r
\r
@ -303,14 +291,14 @@ public class MultiPartFormDataTest
--AaB03x--\r
""";
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(asChunk(body, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, body, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertThat(parts.size(), is(2));
MultiPart.Part part1 = parts.getFirst("field1");
@ -340,14 +328,14 @@ public class MultiPartFormDataTest
--AaB03x--\r
""";
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(asChunk(body, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, body, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
// The first boundary must be on a new line, so the first "part" is not recognized as such.
assertThat(parts.size(), is(1));
@ -361,7 +349,8 @@ public class MultiPartFormDataTest
@Test
public void testDefaultLimits() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
String body = """
--AaB03x\r
@ -371,9 +360,8 @@ public class MultiPartFormDataTest
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
--AaB03x--\r
""";
formData.parse(asChunk(body, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, body, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertThat(parts.size(), is(1));
MultiPart.Part part = parts.get(0);
@ -390,7 +378,8 @@ public class MultiPartFormDataTest
@Test
public void testRequestContentTooBig() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxLength(16);
@ -402,9 +391,8 @@ public class MultiPartFormDataTest
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
--AaB03x--\r
""";
formData.parse(asChunk(body, true));
formData.handle((parts, failure) ->
Content.Sink.write(source, true, body, Callback.NOOP);
formData.parse(source).handle((parts, failure) ->
{
assertNull(parts);
assertNotNull(failure);
@ -416,7 +404,8 @@ public class MultiPartFormDataTest
@Test
public void testFileTooBig() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(16);
@ -428,9 +417,8 @@ public class MultiPartFormDataTest
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
--AaB03x--\r
""";
formData.parse(asChunk(body, true));
formData.handle((parts, failure) ->
Content.Sink.write(source, true, body, Callback.NOOP);
formData.parse(source).handle((parts, failure) ->
{
assertNull(parts);
assertNotNull(failure);
@ -442,7 +430,8 @@ public class MultiPartFormDataTest
@Test
public void testTwoFilesOneInMemoryOneOnDisk() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
String chunk = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
formData.setMaxMemoryFileSize(chunk.length() + 1);
@ -460,9 +449,8 @@ public class MultiPartFormDataTest
$C$C$C$C\r
--AaB03x--\r
""".replace("$C", chunk);
formData.parse(asChunk(body, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, body, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertNotNull(parts);
assertEquals(2, parts.size());
@ -482,7 +470,8 @@ public class MultiPartFormDataTest
@Test
public void testPartWrite() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
String chunk = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
formData.setMaxMemoryFileSize(chunk.length() + 1);
@ -500,9 +489,8 @@ public class MultiPartFormDataTest
$C$C$C$C\r
--AaB03x--\r
""".replace("$C", chunk);
formData.parse(asChunk(body, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, body, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertNotNull(parts);
assertEquals(2, parts.size());
@ -528,7 +516,8 @@ public class MultiPartFormDataTest
@Test
public void testPathPartDelete() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
String body = """
@ -539,9 +528,8 @@ public class MultiPartFormDataTest
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
--AaB03x--\r
""";
formData.parse(asChunk(body, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, body, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertNotNull(parts);
assertEquals(1, parts.size());
@ -559,7 +547,8 @@ public class MultiPartFormDataTest
@Test
public void testAbort()
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxMemoryFileSize(32);
@ -575,24 +564,27 @@ public class MultiPartFormDataTest
--AaB03x--\r
""";
// Parse only part of the content.
formData.parse(asChunk(body, false));
Content.Sink.write(source, false, body, Callback.NOOP);
CompletableFuture<MultiPartFormData.Parts> futureParts = formData.parse(source);
assertEquals(1, formData.getPartsSize());
// Abort MultiPartFormData.
formData.completeExceptionally(new IOException());
futureParts.completeExceptionally(new IOException());
// Parse the rest of the content.
formData.parse(asChunk(terminator, true));
Content.Sink.write(source, true, terminator, Callback.NOOP);
// Try to get the parts, it should fail.
assertThrows(ExecutionException.class, () -> formData.get(5, TimeUnit.SECONDS));
assertThrows(ExecutionException.class, () -> futureParts.get(5, TimeUnit.SECONDS));
assertEquals(0, formData.getPartsSize());
}
@Test
public void testMaxHeaderLength() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setPartHeadersMaxLength(32);
@ -604,9 +596,8 @@ public class MultiPartFormDataTest
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
--AaB03x--\r
""";
formData.parse(asChunk(body, true));
formData.handle((parts, failure) ->
Content.Sink.write(source, true, body, Callback.NOOP);
formData.parse(source).handle((parts, failure) ->
{
assertNull(parts);
assertNotNull(failure);
@ -618,7 +609,8 @@ public class MultiPartFormDataTest
@Test
public void testDefaultCharset() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxMemoryFileSize(-1);
@ -645,13 +637,14 @@ public class MultiPartFormDataTest
\r
--AaB03x--\r
""";
formData.parse(asChunk(body1, false));
formData.parse(asChunk(isoCedilla, false));
formData.parse(asChunk(body2, false));
formData.parse(asChunk(utfCedilla, false));
formData.parse(asChunk(terminator, true));
CompletableFuture<MultiPartFormData.Parts> futureParts = formData.parse(source);
Content.Sink.write(source, false, body1, Callback.NOOP);
source.write(false, isoCedilla, Callback.NOOP);
Content.Sink.write(source, false, body2, Callback.NOOP);
source.write(false, utfCedilla, Callback.NOOP);
Content.Sink.write(source, true, terminator, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
try (MultiPartFormData.Parts parts = futureParts.get(5, TimeUnit.SECONDS))
{
Charset defaultCharset = formData.getDefaultCharset();
assertEquals(ISO_8859_1, defaultCharset);
@ -669,7 +662,8 @@ public class MultiPartFormDataTest
@Test
public void testPartWithBackSlashInFileName() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxMemoryFileSize(-1);
@ -681,9 +675,9 @@ public class MultiPartFormDataTest
stuffaaa\r
--AaB03x--\r
""";
formData.parse(asChunk(contents, true));
Content.Sink.write(source, true, contents, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertThat(parts.size(), is(1));
MultiPart.Part part = parts.get(0);
@ -694,7 +688,8 @@ public class MultiPartFormDataTest
@Test
public void testPartWithWindowsFileName() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxMemoryFileSize(-1);
@ -706,9 +701,8 @@ public class MultiPartFormDataTest
stuffaaa\r
--AaB03x--\r
""";
formData.parse(asChunk(contents, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, contents, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertThat(parts.size(), is(1));
MultiPart.Part part = parts.get(0);
@ -722,7 +716,8 @@ public class MultiPartFormDataTest
@Disabled
public void testCorrectlyEncodedMSFilename() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxMemoryFileSize(-1);
@ -734,9 +729,8 @@ public class MultiPartFormDataTest
stuffaaa\r
--AaB03x--\r
""";
formData.parse(asChunk(contents, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, contents, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertThat(parts.size(), is(1));
MultiPart.Part part = parts.get(0);
@ -747,7 +741,8 @@ public class MultiPartFormDataTest
@Test
public void testWriteFilesForPartWithoutFileName() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setUseFilesForPartsWithoutFileName(true);
@ -759,9 +754,8 @@ public class MultiPartFormDataTest
sssaaa\r
--AaB03x--\r
""";
formData.parse(asChunk(body, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, body, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertThat(parts.size(), is(1));
MultiPart.Part part = parts.get(0);
@ -775,7 +769,8 @@ public class MultiPartFormDataTest
@Test
public void testPartsWithSameName() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
String sameNames = """
@ -791,9 +786,8 @@ public class MultiPartFormDataTest
AAAAA\r
--AaB03x--\r
""";
formData.parse(asChunk(sameNames, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, sameNames, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertEquals(2, parts.size());
@ -810,4 +804,16 @@ public class MultiPartFormDataTest
assertEquals("AAAAA", part2.getContentAsString(formData.getDefaultCharset()));
}
}
private class TestContent extends AsyncContent
{
@Override
public Content.Chunk read()
{
Content.Chunk chunk = super.read();
if (chunk != null && chunk.canRetain())
_allocatedChunks.add(chunk);
return chunk;
}
}
}

View File

@ -607,9 +607,9 @@ public class IdleTimeoutTest extends AbstractTest
_request.demand(this::onContentAvailable);
return;
}
if (chunk instanceof Content.Chunk.Error error)
if (Content.Chunk.isFailure(chunk))
{
_callback.failed(error.getCause());
_callback.failed(chunk.getFailure());
return;
}
chunk.release();

View File

@ -53,8 +53,9 @@ public class Content
/**
* <p>Copies the given content source to the given content sink, notifying
* the given callback when the copy is complete (either succeeded or failed).</p>
* <p>In case of failures, the content source is {@link Source#fail(Throwable) failed}
* too.</p>
* <p>In case of {@link Chunk#getFailure() failure chunks},
* the content source is {@link Source#fail(Throwable) failed} if the failure
* chunk is {@link Chunk#isLast() last}, else the failing is transient and is ignored.</p>
*
* @param source the source to copy from
* @param sink the sink to copy to
@ -76,6 +77,9 @@ public class Content
* <p>If the predicate returns {@code false}, it means that the chunk is not
* handled, its callback will not be completed, and the implementation will
* handle the chunk and its callback.</p>
* <p>In case of {@link Chunk#getFailure() failure chunks} not handled by any {@code chunkHandler},
* the content source is {@link Source#fail(Throwable) failed} if the failure
* chunk is {@link Chunk#isLast() last}, else the failure is transient and is ignored.</p>
*
* @param source the source to copy from
* @param sink the sink to copy to
@ -103,10 +107,11 @@ public class Content
* return;
* }
*
* // The chunk is an error.
* if (chunk instanceof Chunk.Error error) {
* // Handle the error.
* Throwable cause = error.getCause();
* // The chunk is a failure.
* if (Content.Chunk.isFailure(chunk)) {
* // Handle the failure.
* Throwable cause = chunk.getFailure();
* boolean transient = !chunk.isLast();
* // ...
* return;
* }
@ -190,7 +195,7 @@ public class Content
* @return the String obtained from the content
* @throws IOException if reading the content fails
*/
public static String asString(Source source, Charset charset) throws IOException
static String asString(Source source, Charset charset) throws IOException
{
try
{
@ -227,12 +232,12 @@ public class Content
}
/**
* <p>Reads, non-blocking, the given content source, until either an error or EOF,
* <p>Reads, non-blocking, the given content source, until a {@link Chunk#isFailure(Chunk) failure} or EOF
* and discards the content.</p>
*
* @param source the source to read from
* @param callback the callback to notify when the whole content has been read
* or an error occurred while reading the content
* or a failure occurred while reading the content
*/
static void consumeAll(Source source, Callback callback)
{
@ -240,7 +245,7 @@ public class Content
}
/**
* <p>Reads, blocking if necessary, the given content source, until either an error
* <p>Reads, blocking if necessary, the given content source, until a {@link Chunk#isFailure(Chunk) failure}
* or EOF, and discards the content.</p>
*
* @param source the source to read from
@ -274,13 +279,15 @@ public class Content
* <p>The returned chunk could be:</p>
* <ul>
* <li>{@code null}, to signal that there isn't a chunk of content available</li>
* <li>an {@link Chunk.Error error} instance, to signal that there was an error
* <li>an {@link Chunk} instance with non null {@link Chunk#getFailure()}, to signal that there was a failure
* trying to produce a chunk of content, or that the content production has been
* {@link #fail(Throwable) failed} externally</li>
* <li>a {@link Chunk} instance, containing the chunk of content.</li>
* </ul>
* <p>Once a read returns an {@link Chunk.Error error} instance, further reads
* will continue to return the same error instance.</p>
* <p>Once a read returns an {@link Chunk} instance with non-null {@link Chunk#getFailure()}
* then if the failure is {@link Chunk#isLast() last} further reads
* will continue to return the same failure chunk instance, otherwise further
* {@code read()} operations may return different non-failure chunks.</p>
* <p>Once a read returns a {@link Chunk#isLast() last chunk}, further reads will
* continue to return a last chunk (although the instance may be different).</p>
* <p>The content reader code must ultimately arrange for a call to
@ -296,7 +303,7 @@ public class Content
* race condition (the thread that reads with the thread that invokes the
* demand callback).</p>
*
* @return a chunk of content, possibly an error instance, or {@code null}
* @return a chunk of content, possibly a failure instance, or {@code null}
* @see #demand(Runnable)
* @see Retainable
*/
@ -327,18 +334,38 @@ public class Content
void demand(Runnable demandCallback);
/**
* <p>Fails this content source, possibly failing and discarding accumulated
* content chunks that were not yet read.</p>
* <p>Fails this content source with a {@link Chunk#isLast() last} {@link Chunk#getFailure() failure chunk},
* failing and discarding accumulated content chunks that were not yet read.</p>
* <p>The failure may be notified to the content reader at a later time, when
* the content reader reads a content chunk, via an {@link Chunk.Error} instance.</p>
* the content reader reads a content chunk, via a {@link Chunk} instance
* with a non null {@link Chunk#getFailure()}.</p>
* <p>If {@link #read()} has returned a last chunk, this is a no operation.</p>
* <p>Typical failure: the content being aborted by user code, or idle timeouts.</p>
* <p>If this method has already been called, then it is a no operation.</p>
*
* @param failure the cause of the failure
* @see Chunk#getFailure()
*/
void fail(Throwable failure);
/**
* <p>Fails this content source with a {@link Chunk#getFailure() failure chunk}
* that may or not may be {@link Chunk#isLast() last}.
* If {@code last} is {@code true}, then the failure is persistent and a call to this method acts
* as {@link #fail(Throwable)}. Otherwise the failure is transient and a
* {@link Chunk#getFailure() failure chunk} will be {@link #read() read} in order with content chunks,
* and subsequent calls to {@link #read() read} may produce other content.</p>
* <p>A {@code Content.Source} or its {@link #read() reader} may treat a transient failure as persistent.</p>
*
* @param failure A failure.
* @param last true if the failure is persistent, false if the failure is transient.
* @see Chunk#getFailure()
*/
default void fail(Throwable failure, boolean last)
{
fail(failure);
}
/**
* <p>Rewinds this content, if possible, so that subsequent reads return
* chunks starting from the beginning of this content.</p>
@ -555,14 +582,52 @@ public class Content
}
/**
* <p>Creates an {@link Error error chunk} with the given failure.</p>
* <p>Creates an {@link Chunk#isFailure(Chunk) failure chunk} with the given failure
* and {@link Chunk#isLast()} returning true.</p>
*
* @param failure the cause of the failure
* @return a new Error.Chunk
* @return a new {@link Chunk#isFailure(Chunk) failure chunk}
*/
static Error from(Throwable failure)
static Chunk from(Throwable failure)
{
return new Error(failure);
return from(failure, true);
}
/**
* <p>Creates an {@link Chunk#isFailure(Chunk) failure chunk} with the given failure
* and given {@link Chunk#isLast() last} state.</p>
*
* @param failure the cause of the failure
* @param last true if the failure is terminal, else false for transient failure
* @return a new {@link Chunk#isFailure(Chunk) failure chunk}
*/
static Chunk from(Throwable failure, boolean last)
{
return new Chunk()
{
public Throwable getFailure()
{
return failure;
}
@Override
public ByteBuffer getByteBuffer()
{
return BufferUtil.EMPTY_BUFFER;
}
@Override
public boolean isLast()
{
return last;
}
@Override
public String toString()
{
return String.format("Chunk@%x{c=%s,l=%b}", hashCode(), failure, last);
}
};
}
/**
@ -581,8 +646,12 @@ public class Content
* <td>{@code null}</td>
* </tr>
* <tr>
* <td>{@link Chunk#isFailure(Chunk) Failure} and {@link Chunk#isLast() last}</td>
* <td>{@link Error Error}</td>
* <td>{@link Error Error}</td>
* </tr>
* <tr>
* <td>{@link Chunk#isFailure(Chunk) Failure} and {@link Chunk#isLast() not last}</td>
* <td>{@code null}</td>
* </tr>
* <tr>
* <td>{@link #isLast()}</td>
@ -597,18 +666,57 @@ public class Content
*/
static Chunk next(Chunk chunk)
{
if (chunk == null || chunk instanceof Error)
return chunk;
if (chunk == null)
return null;
if (Content.Chunk.isFailure(chunk))
return chunk.isLast() ? chunk : null;
if (chunk.isLast())
return EOF;
return null;
}
/**
* @param chunk The chunk to test for an {@link Chunk#getFailure() failure}.
* @return True if the chunk is non-null and {@link Chunk#getFailure() chunk.getError()} returns non-null.
*/
static boolean isFailure(Chunk chunk)
{
return chunk != null && chunk.getFailure() != null;
}
/**
* @param chunk The chunk to test for an {@link Chunk#getFailure() failure}
* @param last The {@link Chunk#isLast() last} status to test for.
* @return True if the chunk is non-null and {@link Chunk#getFailure()} returns non-null
* and {@link Chunk#isLast()} matches the passed status.
*/
static boolean isFailure(Chunk chunk, boolean last)
{
return chunk != null && chunk.getFailure() != null && chunk.isLast() == last;
}
/**
* @return the ByteBuffer of this Chunk
*/
ByteBuffer getByteBuffer();
/**
* Get a failure (which may be from a {@link Source#fail(Throwable) failure} or
* a {@link Source#fail(Throwable, boolean) warning}), if any, associated with the chunk.
* <ul>
* <li>A {@code chunk} must not have a failure and a {@link #getByteBuffer()} with content.</li>
* <li>A {@code chunk} with a failure may or may not be {@link #isLast() last}.</li>
* <li>A {@code chunk} with a failure must not be {@link #canRetain() retainable}.</li>
* </ul>
* @return A {@link Throwable} indicating the failure or null if there is no failure or warning.
* @see Source#fail(Throwable)
* @see Source#fail(Throwable, boolean)
*/
default Throwable getFailure()
{
return null;
}
/**
* @return whether this is the last Chunk
*/
@ -674,46 +782,6 @@ public class Content
return asChunk(getByteBuffer().asReadOnlyBuffer(), isLast(), this);
}
/**
* <p>A chunk that wraps a failure.</p>
* <p>Error Chunks are always last and have no bytes to read,
* as such they are <em>terminal</em> Chunks.</p>
*
* @see #from(Throwable)
*/
final class Error implements Chunk
{
private final Throwable cause;
private Error(Throwable cause)
{
this.cause = cause;
}
public Throwable getCause()
{
return cause;
}
@Override
public ByteBuffer getByteBuffer()
{
return BufferUtil.EMPTY_BUFFER;
}
@Override
public boolean isLast()
{
return true;
}
@Override
public String toString()
{
return String.format("%s@%x{c=%s}", getClass().getSimpleName(), hashCode(), cause);
}
}
/**
* <p>Implementations of this interface may process {@link Chunk}s being copied by the
* {@link Content#copy(Source, Sink, Processor, Callback)} method, so that

View File

@ -148,14 +148,14 @@ public abstract class SelectorManager extends ContainerLifeCycle implements Dump
public int getTotalKeys()
{
int keys = 0;
for (final ManagedSelector selector : _selectors)
for (ManagedSelector selector : _selectors)
{
keys += selector.getTotalKeys();
if (selector != null)
keys += selector.getTotalKeys();
}
return keys;
}
/**
* @return the number of selectors in use
*/

View File

@ -51,8 +51,8 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
private final AutoLock.WithCondition lock = new AutoLock.WithCondition();
private final SerializedInvoker invoker = new SerializedInvoker();
private final Queue<AsyncChunk> chunks = new ArrayDeque<>();
private Content.Chunk.Error errorChunk;
private final Queue<Content.Chunk> chunks = new ArrayDeque<>();
private Content.Chunk persistentFailure;
private boolean readClosed;
private boolean writeClosed;
private Runnable demandCallback;
@ -62,7 +62,7 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
* {@inheritDoc}
* <p>The write completes:</p>
* <ul>
* <li>immediately with a failure when this instance is closed or already in error</li>
* <li>immediately with a failure when this instance is closed or already has a failure</li>
* <li>successfully when a non empty {@link Content.Chunk} returned by {@link #read()} is released</li>
* <li>successfully just before the {@link Content.Chunk} is returned by {@link #read()},
* for any empty chunk {@link Content.Chunk}.</li>
@ -79,7 +79,7 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
* or succeeded if and only if the chunk is terminal, as non-terminal
* chunks have to bind the succeeding of the callback to their release.
*/
private void offer(AsyncChunk chunk)
private void offer(Content.Chunk chunk)
{
Throwable failure = null;
boolean wasEmpty = false;
@ -89,9 +89,9 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
{
failure = new IOException("closed");
}
else if (errorChunk != null)
else if (persistentFailure != null)
{
failure = errorChunk.getCause();
failure = persistentFailure.getFailure();
}
else
{
@ -105,14 +105,14 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
if (length == UNDETERMINED_LENGTH)
{
length = 0;
for (AsyncChunk c : chunks)
for (Content.Chunk c : chunks)
length += c.remaining();
}
}
}
}
if (failure != null)
chunk.failed(failure);
if (failure != null && chunk instanceof AsyncChunk asyncChunk)
asyncChunk.failed(failure);
if (wasEmpty)
invoker.run(this::invokeDemandCallback);
}
@ -125,14 +125,14 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
{
// Always wrap the exception to make sure
// the stack trace comes from flush().
if (errorChunk != null)
throw new IOException(errorChunk.getCause());
if (persistentFailure != null)
throw new IOException(persistentFailure.getFailure());
if (chunks.isEmpty())
return;
// Special case for a last empty chunk that may not be read.
if (writeClosed && chunks.size() == 1)
{
AsyncChunk chunk = chunks.peek();
Content.Chunk chunk = chunks.peek();
if (chunk.isLast() && !chunk.hasRemaining())
return;
}
@ -171,7 +171,7 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
@Override
public Content.Chunk read()
{
AsyncChunk current;
Content.Chunk current;
try (AutoLock.WithCondition condition = lock.lock())
{
if (length == UNDETERMINED_LENGTH)
@ -181,8 +181,8 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
{
if (readClosed)
return Content.Chunk.EOF;
if (errorChunk != null)
return errorChunk;
if (persistentFailure != null)
return persistentFailure;
return null;
}
readClosed = current.isLast();
@ -195,7 +195,12 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
return current;
// If the chunk is not reference counted, we can succeed it now and return a chunk with a noop release.
current.succeeded();
if (current instanceof AsyncChunk asyncChunk)
asyncChunk.succeeded();
if (Content.Chunk.isFailure(current))
return current;
return current.isLast() ? Content.Chunk.EOF : Content.Chunk.EMPTY;
}
@ -208,7 +213,7 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
if (this.demandCallback != null)
throw new IllegalStateException("demand pending");
this.demandCallback = Objects.requireNonNull(demandCallback);
invoke = !chunks.isEmpty() || readClosed || errorChunk != null;
invoke = !chunks.isEmpty() || readClosed || persistentFailure != null;
}
if (invoke)
invoker.run(this::invokeDemandCallback);
@ -241,22 +246,35 @@ public class AsyncContent implements Content.Sink, Content.Source, Closeable
@Override
public void fail(Throwable failure)
{
List<AsyncChunk> drained;
List<Content.Chunk> drained;
try (AutoLock.WithCondition condition = lock.lock())
{
if (readClosed)
return;
if (errorChunk != null)
if (persistentFailure != null)
return;
errorChunk = Content.Chunk.from(failure);
persistentFailure = Content.Chunk.from(failure);
drained = List.copyOf(chunks);
chunks.clear();
condition.signal();
}
drained.forEach(ac -> ac.failed(failure));
drained.forEach(c ->
{
if (c instanceof AsyncChunk ac)
ac.failed(failure);
});
invoker.run(this::invokeDemandCallback);
}
@Override
public void fail(Throwable failure, boolean last)
{
if (last)
fail(failure);
else
offer(Content.Chunk.from(failure, false));
}
public int count()
{
try (AutoLock ignored = lock.lock())

View File

@ -0,0 +1,144 @@
//
// ========================================================================
// 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.io.content;
import java.io.EOFException;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.io.Content;
/**
* <p>A utility class to convert content from a {@link Content.Source} to an instance
* available via a {@link CompletableFuture}.</p>
* <p>An example usage to asynchronously read UTF-8 content is:</p>
* <pre>{@code
* public static class CompletableUTF8String extends ContentSourceCompletableFuture<String>;
* {
* private final Utf8StringBuilder builder = new Utf8StringBuilder();
*
* public CompletableUTF8String(Content.Source content)
* {
* super(content);
* }
*
* @Override
* protected String parse(Content.Chunk chunk) throws Throwable
* {
* // Accumulate the chunk bytes.
* if (chunk.hasRemaining())
* builder.append(chunk.getByteBuffer());
*
* // Not the last chunk, the result is not ready yet.
* if (!chunk.isLast())
* return null;
*
* // The result is ready.
* return builder.takeCompleteString(IllegalStateException::new);
* }
* }
*
* new CompletableUTF8String(source).thenAccept(System.err::println);
* }</pre>
*/
public abstract class ContentSourceCompletableFuture<X> extends CompletableFuture<X>
{
private final Content.Source _content;
public ContentSourceCompletableFuture(Content.Source content)
{
_content = content;
}
/**
* <p>Initiates the parsing of the {@link Content.Source}.</p>
* <p>For every valid chunk that is read, {@link #parse(Content.Chunk)}
* is called, until a result is produced that is used to
* complete this {@link CompletableFuture}.</p>
* <p>Internally, this method is called multiple times to progress
* the parsing in response to {@link Content.Source#demand(Runnable)}
* calls.</p>
* <p>Exceptions thrown during parsing result in this
* {@link CompletableFuture} to be completed exceptionally.</p>
*/
public void parse()
{
while (true)
{
Content.Chunk chunk = _content.read();
if (chunk == null)
{
_content.demand(this::parse);
return;
}
if (Content.Chunk.isFailure(chunk))
{
if (!chunk.isLast() && onTransientFailure(chunk.getFailure()))
continue;
completeExceptionally(chunk.getFailure());
return;
}
try
{
X x = parse(chunk);
if (x != null)
{
complete(x);
return;
}
}
catch (Throwable failure)
{
completeExceptionally(failure);
return;
}
finally
{
chunk.release();
}
if (chunk.isLast())
{
completeExceptionally(new EOFException());
return;
}
}
}
/**
* <p>Called by {@link #parse()} to parse a {@link org.eclipse.jetty.io.Content.Chunk}.</p>
*
* @param chunk The chunk containing content to parse. The chunk will never be {@code null} nor a
* {@link org.eclipse.jetty.io.Content.Chunk#isFailure(Content.Chunk) failure chunk}.
* If the chunk is stored away to be used later beyond the scope of this call,
* then implementations must call {@link Content.Chunk#retain()} and
* {@link Content.Chunk#release()} as appropriate.
* @return The parsed {@code X} result instance or {@code null} if parsing is not yet complete
* @throws Throwable If there is an error parsing
*/
protected abstract X parse(Content.Chunk chunk) throws Throwable;
/**
* <p>Callback method that informs the parsing about how to handle transient failures.</p>
*
* @param cause A transient failure obtained by reading a {@link Content.Chunk#isLast() non-last}
* {@link org.eclipse.jetty.io.Content.Chunk#isFailure(Content.Chunk) failure chunk}
* @return {@code true} if the transient failure can be ignored, {@code false} otherwise
*/
protected boolean onTransientFailure(Throwable cause)
{
return false;
}
}

View File

@ -54,8 +54,12 @@ public class ContentSourceInputStream extends InputStream
{
if (chunk != null)
{
if (chunk instanceof Content.Chunk.Error error)
throw IO.rethrow(error.getCause());
if (Content.Chunk.isFailure(chunk))
{
Content.Chunk c = chunk;
chunk = null;
throw IO.rethrow(c.getFailure());
}
ByteBuffer byteBuffer = chunk.getByteBuffer();
if (chunk.isLast() && !byteBuffer.hasRemaining())
@ -96,8 +100,8 @@ public class ContentSourceInputStream extends InputStream
@Override
public void close()
{
// If we have already reached a real EOF or an error, close is a noop.
if (chunk == Content.Chunk.EOF || chunk instanceof Content.Chunk.Error)
// If we have already reached a real EOF or a persistent failure, close is a noop.
if (chunk == Content.Chunk.EOF || Content.Chunk.isFailure(chunk, true))
return;
// If we have a chunk here, then it needs to be released

View File

@ -125,10 +125,10 @@ public class ContentSourcePublisher implements Flow.Publisher<Content.Chunk>
return;
}
if (chunk instanceof Content.Chunk.Error error)
if (Content.Chunk.isFailure(chunk))
{
terminate();
subscriber.onError(error.getCause());
subscriber.onError(chunk.getFailure());
return;
}

View File

@ -60,10 +60,10 @@ public abstract class ContentSourceTransformer implements Content.Source
return null;
}
if (rawChunk instanceof Content.Chunk.Error)
if (Content.Chunk.isFailure(rawChunk))
return rawChunk;
if (transformedChunk instanceof Content.Chunk.Error)
if (Content.Chunk.isFailure(transformedChunk))
return transformedChunk;
transformedChunk = process(rawChunk);
@ -142,13 +142,14 @@ public abstract class ContentSourceTransformer implements Content.Source
* <p>The input chunk is released as soon as this method returns, so
* implementations that must hold onto the input chunk must arrange to call
* {@link Content.Chunk#retain()} and its correspondent {@link Content.Chunk#release()}.</p>
* <p>Implementations should return an {@link Content.Chunk.Error error chunk} in case
* <p>Implementations should return an {@link Content.Chunk} with non-null
* {@link Content.Chunk#getFailure()} in case
* of transformation errors.</p>
* <p>Exceptions thrown by this method are equivalent to returning an error chunk.</p>
* <p>Implementations of this method may return:</p>
* <ul>
* <li>{@code null}, if more input chunks are necessary to produce an output chunk</li>
* <li>the {@code inputChunk} itself, typically in case of {@link Content.Chunk.Error}s,
* <li>the {@code inputChunk} itself, typically in case of non-null {@link Content.Chunk#getFailure()},
* or when no transformation is required</li>
* <li>a new {@link Content.Chunk} derived from {@code inputChunk}.</li>
* </ul>

View File

@ -40,7 +40,7 @@ public class InputStreamContentSource implements Content.Source
private final ByteBufferPool bufferPool;
private int bufferSize = 4096;
private Runnable demandCallback;
private Content.Chunk.Error errorChunk;
private Content.Chunk errorChunk;
private boolean closed;
public InputStreamContentSource(InputStream inputStream)

View File

@ -46,7 +46,7 @@ public class PathContentSource implements Content.Source
private SeekableByteChannel channel;
private long totalRead;
private Runnable demandCallback;
private Content.Chunk.Error errorChunk;
private Content.Chunk errorChunk;
public PathContentSource(Path path)
{

View File

@ -16,9 +16,13 @@ package org.eclipse.jetty.io.internal;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IteratingNestedCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ContentCopier extends IteratingNestedCallback
{
private static final Logger LOG = LoggerFactory.getLogger(ContentCopier.class);
private final Content.Source source;
private final Content.Sink sink;
private final Content.Chunk.Processor chunkProcessor;
@ -56,8 +60,15 @@ public class ContentCopier extends IteratingNestedCallback
if (chunkProcessor != null && chunkProcessor.process(current, this))
return Action.SCHEDULED;
if (current instanceof Error error)
throw error.getCause();
if (Content.Chunk.isFailure(current))
{
if (current.isLast())
throw current.getFailure();
if (LOG.isDebugEnabled())
LOG.debug("ignored transient failure", current.getFailure());
succeeded();
return Action.SCHEDULED;
}
sink.write(current.isLast(), current.getByteBuffer(), this);
return Action.SCHEDULED;

View File

@ -44,9 +44,9 @@ public class ContentSourceByteBuffer implements Runnable
return;
}
if (chunk instanceof Content.Chunk.Error error)
if (Content.Chunk.isFailure(chunk))
{
promise.failed(error.getCause());
promise.failed(chunk.getFailure());
return;
}

View File

@ -41,9 +41,9 @@ public class ContentSourceConsumer implements Invocable.Task
return;
}
if (chunk instanceof Content.Chunk.Error error)
if (Content.Chunk.isFailure(chunk))
{
callback.failed(error.getCause());
callback.failed(chunk.getFailure());
return;
}

View File

@ -42,9 +42,9 @@ public class ContentSourceString
content.demand(this::convert);
return;
}
if (chunk instanceof Content.Chunk.Error error)
if (Content.Chunk.isFailure(chunk))
{
promise.failed(error.getCause());
promise.failed(chunk.getFailure());
return;
}
text.append(chunk.getByteBuffer());

View File

@ -33,7 +33,6 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -101,7 +100,7 @@ public class AsyncContentTest
// We must read the error.
chunk = async.read();
assertInstanceOf(Content.Chunk.Error.class, chunk);
assertTrue(Content.Chunk.isFailure(chunk, true));
// Offering more should fail.
CountDownLatch failLatch = new CountDownLatch(1);
@ -209,14 +208,14 @@ public class AsyncContentTest
assertThat(chunk.release(), is(true));
callback1.assertNoFailureWithSuccesses(1);
Exception error1 = new Exception("test1");
async.fail(error1);
Exception failure1 = new Exception("test1");
async.fail(failure1);
chunk = async.read();
assertSame(error1, ((Content.Chunk.Error)chunk).getCause());
assertSame(failure1, chunk.getFailure());
callback2.assertSingleFailureSameInstanceNoSuccess(error1);
callback3.assertSingleFailureSameInstanceNoSuccess(error1);
callback2.assertSingleFailureSameInstanceNoSuccess(failure1);
callback3.assertSingleFailureSameInstanceNoSuccess(failure1);
}
}

View File

@ -15,8 +15,10 @@ package org.eclipse.jetty.io;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
@ -27,6 +29,7 @@ import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@ -49,13 +52,14 @@ import org.junit.jupiter.params.provider.MethodSource;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class ContentSourceTest
{
@ -237,7 +241,7 @@ public class ContentSourceTest
// We must read the error.
chunk = source.read();
assertInstanceOf(Content.Chunk.Error.class, chunk);
assertTrue(Content.Chunk.isFailure(chunk, true));
}
@ParameterizedTest
@ -259,7 +263,7 @@ public class ContentSourceTest
source.fail(new CancellationException());
Content.Chunk chunk = source.read();
assertInstanceOf(Content.Chunk.Error.class, chunk);
assertTrue(Content.Chunk.isFailure(chunk, true));
CountDownLatch latch = new CountDownLatch(1);
source.demand(latch::countDown);
@ -285,7 +289,7 @@ public class ContentSourceTest
});
chunk = source.read();
assertInstanceOf(Content.Chunk.Error.class, chunk);
assertTrue(Content.Chunk.isFailure(chunk, true));
}
@Test
@ -560,4 +564,63 @@ public class ContentSourceTest
{
}
}
@Test
public void testAsyncContentWithWarnings()
{
AsyncContent content = new AsyncContent();
Content.Sink.write(content, false, "One", Callback.NOOP);
content.fail(new TimeoutException("test"), false);
Content.Sink.write(content, true, "Two", Callback.NOOP);
Content.Chunk chunk = content.read();
assertFalse(chunk.isLast());
assertFalse(Content.Chunk.isFailure(chunk));
assertThat(BufferUtil.toString(chunk.getByteBuffer()), is("One"));
chunk = content.read();
assertFalse(chunk.isLast());
assertTrue(Content.Chunk.isFailure(chunk));
assertThat(chunk.getFailure(), instanceOf(TimeoutException.class));
chunk = content.read();
assertTrue(chunk.isLast());
assertFalse(Content.Chunk.isFailure(chunk));
assertThat(BufferUtil.toString(chunk.getByteBuffer()), is("Two"));
}
@Test
public void testAsyncContentWithWarningsAsInputStream() throws Exception
{
AsyncContent content = new AsyncContent();
Content.Sink.write(content, false, "One", Callback.NOOP);
content.fail(new TimeoutException("test"), false);
Content.Sink.write(content, true, "Two", Callback.NOOP);
InputStream in = Content.Source.asInputStream(content);
byte[] buffer = new byte[1024];
int len = in.read(buffer);
assertThat(len, is(3));
assertThat(new String(buffer, 0, 3, StandardCharsets.ISO_8859_1), is("One"));
try
{
int ignored = in.read();
fail();
}
catch (IOException ioe)
{
assertThat(ioe.getCause(), instanceOf(TimeoutException.class));
}
len = in.read(buffer);
assertThat(len, is(3));
assertThat(new String(buffer, 0, 3, StandardCharsets.ISO_8859_1), is("Two"));
len = in.read(buffer);
assertThat(len, is(-1));
}
}

View File

@ -34,7 +34,6 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -259,11 +258,11 @@ public class ContentSourceTransformerTest
chunk.release();
chunk = transformer.read();
assertInstanceOf(Content.Chunk.Error.class, chunk);
assertTrue(Content.Chunk.isFailure(chunk, true));
// Trying to read again returns the error again.
chunk = transformer.read();
assertInstanceOf(Content.Chunk.Error.class, chunk);
assertTrue(Content.Chunk.isFailure(chunk, true));
// Make sure that the source is failed.
assertEquals(0, source.count());
@ -284,11 +283,11 @@ public class ContentSourceTransformerTest
chunk.release();
chunk = transformer.read();
assertInstanceOf(Content.Chunk.Error.class, chunk);
assertTrue(Content.Chunk.isFailure(chunk, true));
// Trying to read again returns the error again.
chunk = transformer.read();
assertInstanceOf(Content.Chunk.Error.class, chunk);
assertTrue(Content.Chunk.isFailure(chunk, true));
}
@Test
@ -306,11 +305,11 @@ public class ContentSourceTransformerTest
source.fail(new IOException());
chunk = transformer.read();
assertInstanceOf(Content.Chunk.Error.class, chunk);
assertTrue(Content.Chunk.isFailure(chunk, true));
// Trying to read again returns the error again.
chunk = transformer.read();
assertInstanceOf(Content.Chunk.Error.class, chunk);
assertTrue(Content.Chunk.isFailure(chunk, true));
}
private static class WordSplitLowCaseTransformer extends ContentSourceTransformer

View File

@ -43,11 +43,22 @@ public class RewriteHandler extends Handler.Wrapper
public RewriteHandler()
{
this(new RuleContainer());
this(null, new RuleContainer());
}
public RewriteHandler(RuleContainer rules)
{
this(null, rules);
}
public RewriteHandler(Handler handler)
{
this(handler, new RuleContainer());
}
public RewriteHandler(Handler handler, RuleContainer rules)
{
super(handler);
_rules = rules;
addBean(_rules);
}

View File

@ -87,6 +87,12 @@ public abstract class SecurityHandler extends Handler.Wrapper implements Configu
protected SecurityHandler()
{
this(null);
}
protected SecurityHandler(Handler handler)
{
super(handler);
addBean(new DumpableCollection("knownAuthenticatorFactories", __knownAuthenticatorFactories));
}
@ -645,6 +651,12 @@ public abstract class SecurityHandler extends Handler.Wrapper implements Configu
public PathMapped()
{
this(null);
}
public PathMapped(Handler handler)
{
super(handler);
}
public Constraint put(String pathSpec, Constraint constraint)

View File

@ -24,24 +24,15 @@
<!-- configuration that may be set here. -->
<!-- =============================================================== -->
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Arg name="threadPool"><Ref refid="threadPool"/></Arg>
<Call name="addBean">
<Arg><Ref refid="byteBufferPool"/></Arg>
</Call>
<!-- =========================================================== -->
<!-- Add shared Scheduler instance -->
<!-- =========================================================== -->
<Call name="addBean">
<Arg>
<New class="org.eclipse.jetty.util.thread.ScheduledExecutorScheduler">
<Arg name="name"><Property name="jetty.scheduler.name"/></Arg>
<Arg name="daemon" type="boolean"><Property name="jetty.scheduler.daemon" default="false" /></Arg>
<Arg name="threads" type="int"><Property name="jetty.scheduler.threads" default="-1" /></Arg>
</New>
</Arg>
</Call>
<Arg name="threadPool"><Ref refid="threadPool"/></Arg>
<Arg>
<New class="org.eclipse.jetty.util.thread.ScheduledExecutorScheduler">
<Arg name="name"><Property name="jetty.scheduler.name"/></Arg>
<Arg name="daemon" type="boolean"><Property name="jetty.scheduler.daemon" default="false" /></Arg>
<Arg name="threads" type="int"><Property name="jetty.scheduler.threads" default="-1" /></Arg>
</New>
</Arg>
<Arg><Ref refid="byteBufferPool"/></Arg>
<!-- =========================================================== -->
<!-- Http Configuration. -->

View File

@ -22,6 +22,8 @@ import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.ContentSourceCompletableFuture;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.CharsetStringBuilder;
import org.eclipse.jetty.util.Fields;
@ -33,7 +35,7 @@ import static org.eclipse.jetty.util.UrlEncoded.decodeHexByte;
* A {@link CompletableFuture} that is completed once a {@code application/x-www-form-urlencoded}
* content has been parsed asynchronously from the {@link Content.Source}.
*/
public class FormFields extends CompletableFuture<Fields> implements Runnable
public class FormFields extends ContentSourceCompletableFuture<Fields>
{
public static final String MAX_FIELDS_ATTRIBUTE = "org.eclipse.jetty.server.Request.maxFormKeys";
public static final String MAX_LENGTH_ATTRIBUTE = "org.eclipse.jetty.server.Request.maxFormContentSize";
@ -57,29 +59,22 @@ public class FormFields extends CompletableFuture<Fields> implements Runnable
return StringUtil.isEmpty(cs) ? StandardCharsets.UTF_8 : Charset.forName(cs);
}
public static CompletableFuture<Fields> from(Request request)
{
// TODO make this attributes provided by the ContextRequest wrapper
int maxFields = getRequestAttribute(request, FormFields.MAX_FIELDS_ATTRIBUTE);
int maxLength = getRequestAttribute(request, FormFields.MAX_LENGTH_ATTRIBUTE);
return from(request, maxFields, maxLength);
}
public static CompletableFuture<Fields> from(Request request, Charset charset)
{
// TODO make this attributes provided by the ContextRequest wrapper
int maxFields = getRequestAttribute(request, FormFields.MAX_FIELDS_ATTRIBUTE);
int maxLength = getRequestAttribute(request, FormFields.MAX_LENGTH_ATTRIBUTE);
return from(request, charset, maxFields, maxLength);
}
/**
* Set a {@link Fields} or related failure for the request
* @param request The request to which to associate the fields with
* @param fields A {@link CompletableFuture} that will provide either the fields or a failure.
*/
public static void set(Request request, CompletableFuture<Fields> fields)
{
request.setAttribute(FormFields.class.getName(), fields);
}
/**
* @param request The request to enquire from
* @return A {@link CompletableFuture} that will provide either the fields or a failure, or null if none set.
* @see #from(Request)
*
*/
public static CompletableFuture<Fields> get(Request request)
{
Object attr = request.getAttribute(FormFields.class.getName());
@ -88,26 +83,93 @@ public class FormFields extends CompletableFuture<Fields> implements Runnable
return EMPTY;
}
/**
* Find or create a {@link FormFields} from a {@link Content.Source}.
* @param request The {@link Request} in which to look for an existing {@link FormFields} attribute,
* using the classname as the attribute name, else the request is used
* as a {@link Content.Source} from which to read the fields and set the attribute.
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
* @see #from(Content.Source, Attributes, Charset, int, int)
*/
public static CompletableFuture<Fields> from(Request request)
{
int maxFields = getRequestAttribute(request, FormFields.MAX_FIELDS_ATTRIBUTE);
int maxLength = getRequestAttribute(request, FormFields.MAX_LENGTH_ATTRIBUTE);
return from(request, maxFields, maxLength);
}
/**
* Find or create a {@link FormFields} from a {@link Content.Source}.
* @param request The {@link Request} in which to look for an existing {@link FormFields} attribute,
* using the classname as the attribute name, else the request is used
* as a {@link Content.Source} from which to read the fields and set the attribute.
* @param charset the {@link Charset} to use for byte to string conversion.
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
* @see #from(Content.Source, Attributes, Charset, int, int)
*/
public static CompletableFuture<Fields> from(Request request, Charset charset)
{
int maxFields = getRequestAttribute(request, FormFields.MAX_FIELDS_ATTRIBUTE);
int maxLength = getRequestAttribute(request, FormFields.MAX_LENGTH_ATTRIBUTE);
return from(request, charset, maxFields, maxLength);
}
/**
* Find or create a {@link FormFields} from a {@link Content.Source}.
* @param request The {@link Request} in which to look for an existing {@link FormFields} attribute,
* using the classname as the attribute name, else the request is used
* as a {@link Content.Source} from which to read the fields and set the attribute.
* @param maxFields The maximum number of fields to be parsed
* @param maxLength The maximum total size of the fields
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
* @see #from(Content.Source, Attributes, Charset, int, int)
*/
public static CompletableFuture<Fields> from(Request request, int maxFields, int maxLength)
{
Object attr = request.getAttribute(FormFields.class.getName());
return from(request, getFormEncodedCharset(request), maxFields, maxLength);
}
/**
* Find or create a {@link FormFields} from a {@link Content.Source}.
* @param request The {@link Request} in which to look for an existing {@link FormFields} attribute,
* using the classname as the attribute name, else the request is used
* as a {@link Content.Source} from which to read the fields and set the attribute.
* @param charset the {@link Charset} to use for byte to string conversion.
* @param maxFields The maximum number of fields to be parsed
* @param maxLength The maximum total size of the fields
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
* @see #from(Content.Source, Attributes, Charset, int, int)
*/
public static CompletableFuture<Fields> from(Request request, Charset charset, int maxFields, int maxLength)
{
return from(request, request, charset, maxFields, maxLength);
}
/**
* Find or create a {@link FormFields} from a {@link Content.Source}.
* @param source The {@link Content.Source} from which to read the fields.
* @param attributes The {@link Attributes} in which to look for an existing {@link CompletableFuture} of
* {@link FormFields}, using the classname as the attribute name. If not found the attribute
* is set with the created {@link CompletableFuture} of {@link FormFields}.
* @param charset the {@link Charset} to use for byte to string conversion.
* @param maxFields The maximum number of fields to be parsed
* @param maxLength The maximum total size of the fields
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
*/
static CompletableFuture<Fields> from(Content.Source source, Attributes attributes, Charset charset, int maxFields, int maxLength)
{
Object attr = attributes.getAttribute(FormFields.class.getName());
if (attr instanceof FormFields futureFormFields)
return futureFormFields;
else if (attr instanceof Fields fields)
return CompletableFuture.completedFuture(fields);
Charset charset = getFormEncodedCharset(request);
if (charset == null)
return EMPTY;
return from(request, charset, maxFields, maxLength);
}
public static CompletableFuture<Fields> from(Request request, Charset charset, int maxFields, int maxLength)
{
FormFields futureFormFields = new FormFields(request, charset, maxFields, maxLength);
request.setAttribute(FormFields.class.getName(), futureFormFields);
futureFormFields.run();
FormFields futureFormFields = new FormFields(source, charset, maxFields, maxLength);
attributes.setAttribute(FormFields.class.getName(), futureFormFields);
futureFormFields.parse();
return futureFormFields;
}
@ -126,7 +188,6 @@ public class FormFields extends CompletableFuture<Fields> implements Runnable
}
}
private final Content.Source _source;
private final Fields _fields;
private final CharsetStringBuilder _builder;
private final int _maxFields;
@ -136,9 +197,9 @@ public class FormFields extends CompletableFuture<Fields> implements Runnable
private int _percent = 0;
private byte _percentCode;
public FormFields(Content.Source source, Charset charset, int maxFields, int maxSize)
private FormFields(Content.Source source, Charset charset, int maxFields, int maxSize)
{
_source = source;
super(source);
_maxFields = maxFields;
_maxLength = maxSize;
_builder = CharsetStringBuilder.forCharset(charset);
@ -146,137 +207,91 @@ public class FormFields extends CompletableFuture<Fields> implements Runnable
}
@Override
public void run()
{
Content.Chunk chunk = null;
try
{
while (true)
{
chunk = _source.read();
if (chunk == null)
{
_source.demand(this);
return;
}
if (chunk instanceof Content.Chunk.Error error)
{
completeExceptionally(error.getCause());
return;
}
while (true)
{
Fields.Field field = parse(chunk);
if (field == null)
break;
if (_maxFields >= 0 && _fields.getSize() >= _maxFields)
{
chunk.release();
// Do not double release if completeExceptionally() throws.
chunk = null;
completeExceptionally(new IllegalStateException("form with too many fields"));
return;
}
_fields.add(field);
}
chunk.release();
if (chunk.isLast())
{
// Do not double release if complete() throws.
chunk = null;
complete(_fields);
return;
}
}
}
catch (Throwable x)
{
if (chunk != null)
chunk.release();
completeExceptionally(x);
}
}
protected Fields.Field parse(Content.Chunk chunk) throws CharacterCodingException
protected Fields parse(Content.Chunk chunk) throws CharacterCodingException
{
String value = null;
ByteBuffer buffer = chunk.getByteBuffer();
loop:
while (BufferUtil.hasContent(buffer))
do
{
byte b = buffer.get();
switch (_percent)
loop:
while (BufferUtil.hasContent(buffer))
{
case 1 ->
byte b = buffer.get();
switch (_percent)
{
_percentCode = b;
_percent++;
continue;
case 1 ->
{
_percentCode = b;
_percent++;
continue;
}
case 2 ->
{
_builder.append(decodeHexByte((char)_percentCode, (char)b));
_percent = 0;
continue;
}
}
case 2 ->
if (_name == null)
{
_builder.append(decodeHexByte((char)_percentCode, (char)b));
_percent = 0;
continue;
switch (b)
{
case '=' ->
{
_name = _builder.build();
checkLength(_name);
}
case '+' -> _builder.append((byte)' ');
case '%' -> _percent++;
default -> _builder.append(b);
}
}
else
{
switch (b)
{
case '&' ->
{
value = _builder.build();
checkLength(value);
break loop;
}
case '+' -> _builder.append((byte)' ');
case '%' -> _percent++;
default -> _builder.append(b);
}
}
}
if (_name == null)
if (_name != null)
{
switch (b)
if (value == null && chunk.isLast())
{
case '=' ->
if (_percent > 0)
{
_name = _builder.build();
checkLength(_name);
_builder.append((byte)'%');
_builder.append(_percentCode);
}
case '+' -> _builder.append((byte)' ');
case '%' -> _percent++;
default -> _builder.append(b);
value = _builder.build();
checkLength(value);
}
}
else
{
switch (b)
if (value != null)
{
case '&' ->
{
value = _builder.build();
checkLength(value);
break loop;
}
case '+' -> _builder.append((byte)' ');
case '%' -> _percent++;
default -> _builder.append(b);
Fields.Field field = new Fields.Field(_name, value);
_name = null;
value = null;
if (_maxFields >= 0 && _fields.getSize() >= _maxFields)
throw new IllegalStateException("form with too many fields > " + _maxFields);
_fields.add(field);
}
}
}
while (BufferUtil.hasContent(buffer));
if (_name != null)
{
if (value == null && chunk.isLast())
{
if (_percent > 0)
{
_builder.append((byte)'%');
_builder.append(_percentCode);
}
value = _builder.build();
checkLength(value);
}
if (value != null)
{
Fields.Field field = new Fields.Field(_name, value);
_name = null;
return field;
}
}
return null;
return chunk.isLast() ? _fields : null;
}
private void checkLength(String nameOrValue)

View File

@ -216,21 +216,6 @@ public interface Handler extends LifeCycle, Destroyable, Request.Handler
}
return null;
}
/**
* <p>Make a {@link Container} the parent of a {@link Handler}</p>
* @param parent The {@link Container} that will be the parent
* @param handler The {@link Handler} that will be the child
*/
static void setAsParent(Container parent, Handler handler)
{
if (parent instanceof Collection collection)
collection.addHandler(handler);
else if (parent instanceof Singleton wrapper)
wrapper.setHandler(handler);
else if (parent != null)
throw new IllegalArgumentException("Unknown parent type: " + parent);
}
}
/**

View File

@ -26,7 +26,7 @@ import org.eclipse.jetty.util.StaticException;
/**
* A HttpStream is an abstraction that together with {@link MetaData.Request}, represents the
* flow of data from and to a single request and response cycle. It is roughly analogous to the
* Stream within a HTTP/2 connection, in that a connection can have many streams, each used once
* Stream within an HTTP/2 connection, in that a connection can have many streams, each used once
* and each representing a single request and response exchange.
*/
public interface HttpStream extends Callback
@ -42,7 +42,7 @@ public interface HttpStream extends Callback
/**
* @return an ID unique within the lifetime scope of the associated protocol connection.
* This may be a protocol ID (eg HTTP/2 stream ID) or it may be unrelated to the protocol.
* This may be a protocol ID (e.g. HTTP/2 stream ID) or it may be unrelated to the protocol.
*/
String getId();
@ -50,7 +50,7 @@ public interface HttpStream extends Callback
* <p>Reads a chunk of content, with the same semantic as {@link Content.Source#read()}.</p>
* <p>This method is called from the implementation of {@link Request#read()}.</p>
*
* @return a chunk of content, possibly an {@link Chunk.Error error} or {@code null}.
* @return a chunk of content, possibly with non-null {@link Chunk#getFailure()} or {@code null}.
*/
Content.Chunk read();
@ -125,8 +125,8 @@ public interface HttpStream extends Callback
content.release();
// if the input failed, then fail the stream for same reason
if (content instanceof Chunk.Error error)
return error.getCause();
if (Content.Chunk.isFailure(content))
return content.getFailure();
if (content.isLast())
return null;

View File

@ -20,22 +20,22 @@ import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.eclipse.jetty.http.CookieCache;
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;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.Trailers;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.internal.HttpChannelState;
@ -69,12 +69,14 @@ import org.eclipse.jetty.util.thread.Invocable;
* return true;
* }
*
* if (chunk instanceof Content.Chunk.Error error)
* if (Content.Chunk.isError(chunk))
* {
* Throwable failure = error.getCause();
*
* // Handle errors.
* // Mark the handling as complete, either generating a custom
* // If the chunk is not last, then the error can be ignored and reading can be tried again.
* // Otherwise, if the chunk is last, or we do not wish to ignore a non-last error, then
* // mark the handling as complete, either generating a custom
* // response and succeeding the callback, or failing the callback.
* callback.failed(failure);
* return true;
@ -117,10 +119,11 @@ public interface Request extends Attributes, Content.Source
{
String CACHE_ATTRIBUTE = Request.class.getCanonicalName() + ".CookieCache";
String COOKIE_ATTRIBUTE = Request.class.getCanonicalName() + ".Cookies";
List<Locale> DEFAULT_LOCALES = List.of(Locale.getDefault());
/**
* an ID unique within the lifetime scope of the {@link ConnectionMetaData#getId()}).
* This may be a protocol ID (eg HTTP/2 stream ID) or it may be unrelated to the protocol.
* This may be a protocol ID (e.g. HTTP/2 stream ID) or it may be unrelated to the protocol.
*
* @see HttpStream#getId()
*/
@ -241,7 +244,7 @@ public interface Request extends Attributes, Content.Source
/**
* Consume any available content. This bypasses any request wrappers to process the content in
* {@link Request#read()} and reads directly from the {@link HttpStream}. This reads until
* there is no content currently available or it reaches EOF.
* there is no content currently available, or it reaches EOF.
* The {@link HttpConfiguration#setMaxUnconsumedRequestContentReads(int)} configuration can be used
* to configure how many reads will be attempted by this method.
* @return true if the content was fully consumed.
@ -358,7 +361,7 @@ public interface Request extends Attributes, Content.Source
: address.getHostAddress();
return HostPort.normalizeHost(result);
}
return local.toString();
return local == null ? null : local.toString();
}
static int getLocalPort(Request request)
@ -387,7 +390,7 @@ public interface Request extends Attributes, Content.Source
: address.getHostAddress();
return HostPort.normalizeHost(result);
}
return remote.toString();
return remote == null ? null : remote.toString();
}
static int getRemotePort(Request request)
@ -417,7 +420,7 @@ public interface Request extends Attributes, Content.Source
if (local instanceof InetSocketAddress)
return HostPort.normalizeHost(((InetSocketAddress)local).getHostString());
return local.toString();
return local == null ? null : local.toString();
}
static int getServerPort(Request request)
@ -450,26 +453,27 @@ public interface Request extends Attributes, Content.Source
{
HttpFields fields = request.getHeaders();
if (fields == null)
return List.of(Locale.getDefault());
return DEFAULT_LOCALES;
List<String> acceptable = fields.getQualityCSV(HttpHeader.ACCEPT_LANGUAGE);
// handle no locale
if (acceptable.isEmpty())
return List.of(Locale.getDefault());
return acceptable.stream().map(language ->
// return sorted list of locals, with known locales in quality order before unknown locales in quality order
return switch (acceptable.size())
{
language = HttpField.stripParameters(language);
String country = "";
int dash = language.indexOf('-');
if (dash > -1)
case 0 -> DEFAULT_LOCALES;
case 1 -> List.of(Locale.forLanguageTag(acceptable.get(0)));
default ->
{
country = language.substring(dash + 1).trim();
language = language.substring(0, dash).trim();
List<Locale> locales = acceptable.stream().map(Locale::forLanguageTag).toList();
List<Locale> known = locales.stream().filter(MimeTypes::isKnownLocale).toList();
if (known.size() == locales.size())
yield locales; // All locales are known
List<Locale> unknown = locales.stream().filter(l -> !MimeTypes.isKnownLocale(l)).toList();
locales = new ArrayList<>(known);
locales.addAll(unknown);
yield locales; // List of known locales before unknown locales
}
return new Locale(language, country);
}).collect(Collectors.toList());
};
}
// TODO: consider inline and remove.
@ -605,7 +609,7 @@ public interface Request extends Attributes, Content.Source
* @param request the HTTP request to handle
* @param response the HTTP response to handle
* @param callback the callback to complete when the handling is complete
* @return True if an only if the request will be handled, a response generated and the callback eventually called.
* @return True if and only if the request will be handled, a response generated and the callback eventually called.
* This may occur within the scope of the call to this method, or asynchronously some time later. If false
* is returned, then this method must not generate a response, nor complete the callback.
* @throws Exception if there is a failure during the handling. Catchers cannot assume that the callback will be

View File

@ -51,7 +51,7 @@ public class ResourceListing
* @param base The base URL
* @param parent True if the parent directory should be included
* @param query query params
* @return the HTML as String
* @return the XHTML as String
*/
public static String getAsXHTML(Resource resource, String base, boolean parent, String query)
{
@ -108,7 +108,8 @@ public class ResourceListing
StringBuilder buf = new StringBuilder(4096);
// Doctype Declaration + XHTML
// Doctype Declaration + XHTML. The spec says the encoding MUST be "utf-8" in all cases at it is ignored;
// see: https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-charset
buf.append("""
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.server;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
@ -440,11 +441,12 @@ public class ResourceService
protected void sendWelcome(HttpContent content, String pathInContext, boolean endsWithSlash, Request request, Response response, Callback callback) throws Exception
{
if (!Objects.requireNonNull(content).getResource().isDirectory())
throw new IllegalArgumentException("content must be a directory");
if (LOG.isDebugEnabled())
{
LOG.debug("sendWelcome(content={}, pathInContext={}, endsWithSlash={}, req={}, resp={}, callback={})",
content, pathInContext, endsWithSlash, request, response, callback);
}
// Redirect to directory
if (!endsWithSlash)
@ -460,7 +462,7 @@ public class ResourceService
}
// process optional Welcome behaviors
if (welcome(request, response, callback))
if (welcome(content, request, response, callback))
return;
if (!passConditionalHeaders(request, response, content, callback))
@ -499,9 +501,9 @@ public class ResourceService
{
}
private boolean welcome(Request request, Response response, Callback callback) throws Exception
private boolean welcome(HttpContent content, Request request, Response response, Callback callback) throws Exception
{
WelcomeAction welcomeAction = processWelcome(request);
WelcomeAction welcomeAction = processWelcome(content, request);
if (LOG.isDebugEnabled())
LOG.debug("welcome(req={}, rsp={}, cbk={}) welcomeAction={}", request, response, callback, welcomeAction);
@ -581,18 +583,15 @@ public class ResourceService
Response.writeError(request, response, callback, HttpStatus.INTERNAL_SERVER_ERROR_500);
}
private WelcomeAction processWelcome(Request request) throws IOException
private WelcomeAction processWelcome(HttpContent content, Request request) throws IOException
{
String welcomeTarget = getWelcomeFactory().getWelcomeTarget(request);
String welcomeTarget = getWelcomeFactory().getWelcomeTarget(content, request);
if (welcomeTarget == null)
return null;
String contextPath = request.getContext().getContextPath();
if (LOG.isDebugEnabled())
LOG.debug("welcome={}", welcomeTarget);
WelcomeMode welcomeMode = getWelcomeMode();
welcomeTarget = switch (welcomeMode)
{
case REDIRECT, REHANDLE -> HttpURI.build(request.getHttpURI())
@ -601,6 +600,9 @@ public class ResourceService
case SERVE -> welcomeTarget;
};
if (LOG.isDebugEnabled())
LOG.debug("welcome {} {}", welcomeMode, welcomeTarget);
return new WelcomeAction(welcomeTarget, welcomeMode);
}
@ -625,8 +627,10 @@ public class ResourceService
return;
}
byte[] data = listing.getBytes(StandardCharsets.UTF_8);
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html;charset=utf-8");
String characterEncoding = httpContent.getCharacterEncoding();
Charset charset = characterEncoding == null ? StandardCharsets.UTF_8 : Charset.forName(characterEncoding);
byte[] data = listing.getBytes(charset);
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html;charset=" + charset.name());
response.getHeaders().put(HttpHeader.CONTENT_LENGTH, data.length);
response.write(true, ByteBuffer.wrap(data), callback);
}
@ -892,7 +896,7 @@ public class ResourceService
* @return The URI path of the matching welcome target in context or null
* if no welcome target was found
*/
String getWelcomeTarget(Request request) throws IOException;
String getWelcomeTarget(HttpContent content, Request request) throws IOException;
}
private static class ContentWriterIteratingCallback extends IteratingCallback

View File

@ -65,6 +65,12 @@ public class BufferedResponseHandler extends Handler.Wrapper
public BufferedResponseHandler()
{
this(null);
}
public BufferedResponseHandler(Handler handler)
{
super(handler);
_methods.include(HttpMethod.GET.asString());
for (String type : MimeTypes.DEFAULTS.getMimeMap().values())
{

View File

@ -77,7 +77,7 @@ public class ConnectHandler extends Handler.Wrapper
public ConnectHandler(Handler handler)
{
setHandler(handler);
super(handler);
}
public Executor getExecutor()

View File

@ -159,11 +159,22 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Alias
public ContextHandler()
{
this(null);
this(null, null);
}
public ContextHandler(Handler handler)
{
this(handler, null);
}
public ContextHandler(String contextPath)
{
this(null, contextPath);
}
public ContextHandler(Handler handler, String contextPath)
{
super(handler);
_context = newContext();
if (contextPath != null)
setContextPath(contextPath);
@ -180,14 +191,6 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Alias
_classLoader = classLoader;
}
@Deprecated
public ContextHandler(Handler.Container parent, String contextPath)
{
this(contextPath);
Container.setAsParent(parent, this);
}
@Override
public void setServer(Server server)
{

View File

@ -41,6 +41,16 @@ public class DebugHandler extends Handler.Wrapper implements Connection.Listener
private OutputStream _out;
private PrintStream _print;
public DebugHandler()
{
this(null);
}
public DebugHandler(Handler handler)
{
super(handler);
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{

View File

@ -13,7 +13,6 @@
package org.eclipse.jetty.server.handler;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
@ -25,8 +24,6 @@ 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.MultiPart;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.FormFields;
import org.eclipse.jetty.server.Handler;
@ -38,6 +35,16 @@ import org.eclipse.jetty.util.StringUtil;
public class DelayedHandler extends Handler.Wrapper
{
public DelayedHandler()
{
this(null);
}
public DelayedHandler(Handler handler)
{
super(handler);
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
@ -105,7 +112,6 @@ public class DelayedHandler extends Handler.Wrapper
return switch (mimeType)
{
case FORM_ENCODED -> new UntilFormDelayedProcess(handler, request, response, callback, contentType);
case MULTIPART_FORM_DATA -> new UntilMultiPartDelayedProcess(handler, request, response, callback, contentType);
default -> new UntilContentDelayedProcess(handler, request, response, callback);
};
}
@ -260,102 +266,4 @@ public class DelayedHandler extends Handler.Wrapper
Response.writeError(getRequest(), getResponse(), getCallback(), x);
}
}
protected static class UntilMultiPartDelayedProcess extends DelayedProcess
{
private final MultiPartFormData _formData;
public UntilMultiPartDelayedProcess(Handler handler, Request wrapped, Response response, Callback callback, String contentType)
{
super(handler, wrapped, response, callback);
String boundary = MultiPart.extractBoundary(contentType);
_formData = boundary == null ? null : new MultiPartFormData(boundary);
}
private void process(MultiPartFormData.Parts parts, Throwable x)
{
if (x == null)
{
getRequest().setAttribute(MultiPartFormData.Parts.class.getName(), parts);
super.process();
}
else
{
Response.writeError(getRequest(), getResponse(), getCallback(), 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
// invoked in a demand runnable that is serialized with any write callbacks that might be done in process
getRequest().getContext().execute(() -> process(parts, x));
}
else
{
Response.writeError(getRequest(), getResponse(), getCallback(), x);
}
}
@Override
public void delay()
{
if (_formData == null)
{
this.process();
}
else
{
_formData.setFilesDirectory(getRequest().getContext().getTempDirectory().toPath());
readAndParse();
// 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 (_formData.isDone())
{
try
{
MultiPartFormData.Parts parts = _formData.join();
process(parts, null);
}
catch (Throwable t)
{
process(null, t);
}
}
else
{
_formData.whenComplete(this::executeProcess);
}
}
}
private void readAndParse()
{
while (!_formData.isDone())
{
Content.Chunk chunk = getRequest().read();
if (chunk == null)
{
getRequest().demand(this::readAndParse);
return;
}
if (chunk instanceof Content.Chunk.Error error)
{
_formData.completeExceptionally(error.getCause());
return;
}
_formData.parse(chunk);
chunk.release();
if (chunk.isLast())
{
if (!_formData.isDone())
process(null, new IOException("Incomplete multipart"));
return;
}
}
}
}
}

View File

@ -55,6 +55,7 @@ public abstract class EventsHandler extends Handler.Wrapper
public EventsHandler()
{
this(null);
}
public EventsHandler(Handler handler)
@ -160,7 +161,7 @@ public abstract class EventsHandler extends Handler.Wrapper
{
try
{
onResponseWrite(request, last, content.asReadOnlyBuffer());
onResponseWrite(request, last, content == null ? null : content.asReadOnlyBuffer());
}
catch (Throwable x)
{
@ -229,7 +230,7 @@ public abstract class EventsHandler extends Handler.Wrapper
* {@link Request#read()}).
*
* @param request the request object. The {@code read()}, {@code demand(Runnable)} and {@code fail(Throwable)} methods must not be called by the listener.
* @param chunk a potentially null request content chunk, including {@link org.eclipse.jetty.io.Content.Chunk.Error}
* @param chunk a potentially null request content chunk, including {@link org.eclipse.jetty.io.Content.Chunk#isFailure(Content.Chunk) error}
* and {@link org.eclipse.jetty.http.Trailers} chunks.
* If a reference to the chunk (or its {@link ByteBuffer}) is kept,
* then {@link Content.Chunk#retain()} must be called.

View File

@ -1,226 +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.io.File;
import java.nio.file.Path;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* A Handler that can apply a
* mechanism to buffer the entire response content until the output is closed.
* This allows the commit to be delayed until the response is complete and thus
* headers and response status can be changed while writing the body.
* </p>
* <p>
* Note that the decision to buffer is influenced by the headers and status at the
* first write, and thus subsequent changes to those headers will not influence the
* decision to buffer or not.
* </p>
* <p>
* Note also that there are no memory limits to the size of the buffer, thus
* this handler can represent an unbounded memory commitment if the content
* generated can also be unbounded.
* </p>
*/
public class FileBufferedResponseHandler extends BufferedResponseHandler
{
private static final Logger LOG = LoggerFactory.getLogger(FileBufferedResponseHandler.class);
private Path _tempDir = new File(System.getProperty("java.io.tmpdir")).toPath();
public Path getTempDir()
{
return _tempDir;
}
public void setTempDir(Path tempDir)
{
_tempDir = Objects.requireNonNull(tempDir);
}
// TODO
/*
@Override
protected BufferedInterceptor newBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
{
return new FileBufferedInterceptor(httpChannel, interceptor);
}
class FileBufferedInterceptor implements BufferedResponseHandler.BufferedInterceptor
{
private static final int MAX_MAPPED_BUFFER_SIZE = Integer.MAX_VALUE / 2;
private final Interceptor _next;
private final HttpChannel _channel;
private Boolean _aggregating;
private Path _filePath;
private OutputStream _fileOutputStream;
public FileBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
{
_next = interceptor;
_channel = httpChannel;
}
@Override
public Interceptor getNextInterceptor()
{
return _next;
}
@Override
public void resetBuffer()
{
dispose();
BufferedInterceptor.super.resetBuffer();
}
protected void dispose()
{
IO.close(_fileOutputStream);
_fileOutputStream = null;
_aggregating = null;
if (_filePath != null)
{
try
{
Files.delete(_filePath);
}
catch (Throwable t)
{
if (LOG.isDebugEnabled())
LOG.debug("Could not immediately delete file (delaying to jvm exit) {}", _filePath, t);
_filePath.toFile().deleteOnExit();
}
_filePath = null;
}
}
@Override
public void write(ByteBuffer content, boolean last, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("{} write last={} {}", this, last, BufferUtil.toDetailString(content));
// If we are not committed, must decide if we should aggregate or not.
if (_aggregating == null)
_aggregating = shouldBuffer(_channel, last);
// If we are not aggregating, then handle normally.
if (!_aggregating)
{
getNextInterceptor().write(content, last, callback);
return;
}
if (LOG.isDebugEnabled())
LOG.debug("{} aggregating", this);
try
{
if (BufferUtil.hasContent(content))
aggregate(content);
}
catch (Throwable t)
{
dispose();
callback.failed(t);
return;
}
if (last)
commit(callback);
else
callback.succeeded();
}
private void aggregate(ByteBuffer content) throws IOException
{
if (_fileOutputStream == null)
{
// Create a new OutputStream to a file.
_filePath = Files.createTempFile(_tempDir, "BufferedResponse", "");
_fileOutputStream = Files.newOutputStream(_filePath, StandardOpenOption.WRITE);
}
BufferUtil.writeTo(content, _fileOutputStream);
}
private void commit(Callback callback)
{
if (_fileOutputStream == null)
{
// We have no content to write, signal next interceptor that we are finished.
getNextInterceptor().write(BufferUtil.EMPTY_BUFFER, true, callback);
return;
}
try
{
_fileOutputStream.close();
_fileOutputStream = null;
}
catch (Throwable t)
{
dispose();
callback.failed(t);
return;
}
// Create an iterating callback to do the writing
IteratingCallback icb = new IteratingCallback()
{
private final long fileLength = _filePath.toFile().length();
private long _pos = 0;
private boolean _last = false;
@Override
protected Action process() throws Exception
{
if (_last)
return Action.SUCCEEDED;
long len = Math.min(MAX_MAPPED_BUFFER_SIZE, fileLength - _pos);
_last = (_pos + len == fileLength);
ByteBuffer buffer = BufferUtil.toMappedBuffer(_filePath, _pos, len);
getNextInterceptor().write(buffer, _last, this);
_pos += len;
return Action.SCHEDULED;
}
@Override
protected void onCompleteSuccess()
{
dispose();
callback.succeeded();
}
@Override
protected void onCompleteFailure(Throwable cause)
{
dispose();
callback.failed(cause);
}
};
icb.iterate();
}
}
*/
}

View File

@ -39,6 +39,12 @@ public class GracefulHandler extends Handler.Wrapper implements Graceful
public GracefulHandler()
{
this(null);
}
public GracefulHandler(Handler handler)
{
super(handler);
_shutdown = new Shutdown(this)
{
@Override

View File

@ -36,6 +36,16 @@ public class IdleTimeoutHandler extends Handler.Wrapper
private long _idleTimeoutMs = 1000;
private boolean _applyToAsync = false;
public IdleTimeoutHandler()
{
this(null);
}
public IdleTimeoutHandler(Handler handler)
{
super(handler);
}
public boolean isApplyToAsync()
{
return _applyToAsync;

View File

@ -47,6 +47,16 @@ public class InetAccessHandler extends Handler.Wrapper
private final IncludeExcludeSet<PatternTuple, AccessTuple> _set = new IncludeExcludeSet<>(InetAccessSet.class);
public InetAccessHandler()
{
this(null);
}
public InetAccessHandler(Handler handler)
{
super(handler);
}
/**
* Clears all the includes, excludes, included connector names and excluded
* connector names.

View File

@ -18,7 +18,6 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
@ -42,13 +41,6 @@ public class MovedContextHandler extends ContextHandler
setAllowNullPathInContext(true);
}
public MovedContextHandler(Handler.Container parent, String contextPath, String redirectURI)
{
Handler.Container.setAsParent(parent, this);
setContextPath(contextPath);
setRedirectURI(redirectURI);
}
/**
* @return the redirect status code, by default 303
*/

View File

@ -63,6 +63,16 @@ public class ResourceHandler extends Handler.Wrapper
private MimeTypes _mimeTypes;
private List<String> _welcomes = List.of("index.html");
public ResourceHandler()
{
this(null);
}
public ResourceHandler(Handler handler)
{
super(handler);
}
protected ResourceService newResourceService()
{
return new HandlerResourceService();
@ -122,7 +132,7 @@ public class ResourceHandler extends Handler.Wrapper
protected ResourceService.WelcomeFactory setupWelcomeFactory()
{
return request ->
return (content, request) ->
{
if (_welcomes == null)
return null;
@ -131,7 +141,7 @@ public class ResourceHandler extends Handler.Wrapper
{
String pathInContext = Request.getPathInContext(request);
String welcomeInContext = URIUtil.addPaths(pathInContext, welcome);
Resource welcomePath = _baseResource.resolve(pathInContext).resolve(welcome);
Resource welcomePath = content.getResource().resolve(welcome);
if (Resources.isReadableFile(welcomePath))
return welcomeInContext;
}

View File

@ -47,7 +47,7 @@ public class SecuredRedirectHandler extends Handler.Wrapper
*/
public SecuredRedirectHandler()
{
this(HttpStatus.MOVED_TEMPORARILY_302);
this(null, HttpStatus.MOVED_TEMPORARILY_302);
}
/**
@ -56,8 +56,29 @@ public class SecuredRedirectHandler extends Handler.Wrapper
* @param code the redirect code to use in the response
* @throws IllegalArgumentException if parameter is an invalid redirect code
*/
public SecuredRedirectHandler(final int code)
public SecuredRedirectHandler(int code)
{
this(null, code);
}
/**
* Uses moved temporarily code (302) as the redirect code.
*/
public SecuredRedirectHandler(Handler handler)
{
this(handler, HttpStatus.MOVED_TEMPORARILY_302);
}
/**
* Use supplied code as the redirect code.
*
* @param handler the handler to wrap
* @param code the redirect code to use in the response
* @throws IllegalArgumentException if parameter is an invalid redirect code
*/
public SecuredRedirectHandler(Handler handler, int code)
{
super(handler);
if (!HttpStatus.isRedirection(code))
throw new IllegalArgumentException("Not a 3xx redirect code");
_redirectCode = code;

View File

@ -91,7 +91,7 @@ public class ShutdownHandler extends Handler.Wrapper
*/
public ShutdownHandler(String shutdownToken)
{
this(null, shutdownToken, false);
this(null, null, shutdownToken, false);
}
/**
@ -102,18 +102,20 @@ public class ShutdownHandler extends Handler.Wrapper
*/
public ShutdownHandler(String shutdownToken, boolean exitJVM)
{
this(null, shutdownToken, exitJVM);
this(null, null, shutdownToken, exitJVM);
}
/**
* Creates a Handler that lets the server be shut down remotely (but only from localhost).
*
* @param handler the handler to wrap
* @param shutdownPath the path to respond to shutdown requests against (default is "{@code /shutdown}")
* @param shutdownToken a secret password to avoid unauthorized shutdown attempts
* @param exitJVM If true, when the shutdown is executed, the handler class System.exit()
*/
public ShutdownHandler(String shutdownPath, String shutdownToken, boolean exitJVM)
public ShutdownHandler(Handler handler, String shutdownPath, String shutdownToken, boolean exitJVM)
{
super(handler);
this._shutdownPath = StringUtil.isBlank(shutdownPath) ? "/shutdown" : shutdownPath;
this._shutdownToken = shutdownToken;
this._exitJvm = exitJVM;

View File

@ -241,17 +241,17 @@ public class StatisticsHandler extends EventsHandler
*/
public MinimumDataRateHandler(long minimumReadRate, long minimumWriteRate)
{
_minimumReadRate = minimumReadRate;
_minimumWriteRate = minimumWriteRate;
this(null, minimumReadRate, minimumWriteRate);
}
/**
* Creates a {@code MinimumDataRateHandler} with the specified read and write rates.
*
* @param handler the handler to wrap.
* @param minimumReadRate the minimum number of bytes to be read per second, or 0 for not checking the read rate.
* @param minimumWriteRate the minimum number of bytes to be written per second, or 0 for not checking the write rate.
* @param handler the handler to wrap.
*/
public MinimumDataRateHandler(long minimumReadRate, long minimumWriteRate, Handler handler)
public MinimumDataRateHandler(Handler handler, long minimumReadRate, long minimumWriteRate)
{
super(handler);
_minimumReadRate = minimumReadRate;
@ -268,7 +268,7 @@ public class StatisticsHandler extends EventsHandler
protected class MinimumDataRateRequest extends Request.Wrapper
{
private Content.Chunk.Error _errorContent;
private Content.Chunk _errorContent;
private MinimumDataRateRequest(Request request)
{

View File

@ -80,17 +80,22 @@ public class ThreadLimitHandler extends Handler.Wrapper
public ThreadLimitHandler()
{
this(null, true);
this(null, null, true);
}
public ThreadLimitHandler(@Name("forwardedHeader") String forwardedHeader)
{
this(forwardedHeader, HttpHeader.FORWARDED.is(forwardedHeader));
this(null, forwardedHeader, HttpHeader.FORWARDED.is(forwardedHeader));
}
public ThreadLimitHandler(@Name("forwardedHeader") String forwardedHeader, @Name("rfc7239") boolean rfc7239)
{
super();
this(null, forwardedHeader, rfc7239);
}
public ThreadLimitHandler(@Name("handler") Handler handler, @Name("forwardedHeader") String forwardedHeader, @Name("rfc7239") boolean rfc7239)
{
super(handler);
_rfc7239 = rfc7239;
_forwardedHeader = forwardedHeader;
_enabled = true;

View File

@ -71,6 +71,16 @@ public class TryPathsHandler extends Handler.Wrapper
private String originalQueryAttribute;
private List<String> paths;
public TryPathsHandler()
{
this(null);
}
public TryPathsHandler(Handler handler)
{
super(handler);
}
/**
* @return the attribute name of the original request path
*/

View File

@ -63,6 +63,16 @@ public class GzipHandler extends Handler.Wrapper implements GzipFactory
*/
public GzipHandler()
{
this(null);
}
/**
* Instantiates a new GzipHandler.
* @param handler the handler to wrap
*/
public GzipHandler(Handler handler)
{
super(handler);
_methods.include(HttpMethod.GET.asString());
_methods.include(HttpMethod.POST.asString());
for (String type : MimeTypes.DEFAULTS.getMimeMap().values())
@ -526,9 +536,9 @@ public class GzipHandler extends Handler.Wrapper implements GzipFactory
if (Request.as(request, GzipRequest.class) != null)
return next.handle(request, response, callback);
String path = Request.getPathInContext(request);
boolean tryInflate = getInflateBufferSize() >= 0 && isPathInflatable(path);
boolean tryDeflate = _methods.test(request.getMethod()) && isPathDeflatable(path) && isMimeTypeDeflatable(request.getContext().getMimeTypes(), path);
String pathInContext = Request.getPathInContext(request);
boolean tryInflate = getInflateBufferSize() >= 0 && isPathInflatable(pathInContext);
boolean tryDeflate = _methods.test(request.getMethod()) && isPathDeflatable(pathInContext) && isMimeTypeDeflatable(request.getContext().getMimeTypes(), pathInContext);
// Can we skip looking at the request and wrapping request or response?
if (!tryInflate && !tryDeflate)
@ -624,15 +634,15 @@ public class GzipHandler extends Handler.Wrapper implements GzipFactory
/**
* Test if the provided Request URI is allowed to be inflated based on the Path Specs filters.
*
* @param requestURI the request uri
* @param pathInContext the request path in context
* @return whether decompressing is allowed for the given the path.
*/
protected boolean isPathInflatable(String requestURI)
protected boolean isPathInflatable(String pathInContext)
{
if (requestURI == null)
if (pathInContext == null)
return true;
return _inflatePaths.test(requestURI);
return _inflatePaths.test(pathInContext);
}
/**

View File

@ -159,7 +159,7 @@ public class GzipRequest extends Request.Wrapper
_chunk = inputChunk;
if (_chunk == null)
return null;
if (_chunk instanceof Content.Chunk.Error)
if (Content.Chunk.isFailure(_chunk))
return _chunk;
if (_chunk.isLast() && !_chunk.hasRemaining())
return Content.Chunk.EOF;

View File

@ -104,7 +104,6 @@ public class HttpChannelState implements HttpChannel, Components
private final HandlerInvoker _handlerInvoker = new HandlerInvoker();
private final ConnectionMetaData _connectionMetaData;
private final SerializedInvoker _serializedInvoker;
private final Attributes _requestAttributes = new Attributes.Lazy();
private final ResponseHttpFields _responseHeaders = new ResponseHttpFields();
private Thread _handling;
private boolean _handled;
@ -120,7 +119,7 @@ public class HttpChannelState implements HttpChannel, Components
/**
* Failure passed to {@link #onFailure(Throwable)}
*/
private Content.Chunk.Error _failure;
private Content.Chunk _failure;
/**
* Listener for {@link #onFailure(Throwable)} events
*/
@ -157,7 +156,6 @@ public class HttpChannelState implements HttpChannel, Components
_streamSendState = StreamSendState.SENDING;
// Recycle.
_requestAttributes.clearAttributes();
_responseHeaders.reset();
_handling = null;
_handled = false;
@ -402,9 +400,9 @@ public class HttpChannelState implements HttpChannel, Components
{
_failure = Content.Chunk.from(x);
}
else if (ExceptionUtil.areNotAssociated(_failure.getCause(), x) && _failure.getCause().getClass() != x.getClass())
else if (ExceptionUtil.areNotAssociated(_failure.getFailure(), x) && _failure.getFailure().getClass() != x.getClass())
{
_failure.getCause().addSuppressed(x);
_failure.getFailure().addSuppressed(x);
}
// If not handled, then we just fail the request callback
@ -741,6 +739,7 @@ public class HttpChannelState implements HttpChannel, Components
private final MetaData.Request _metaData;
private final AutoLock _lock;
private final LongAdder _contentBytesRead = new LongAdder();
private final Attributes _attributes = new Attributes.Lazy();
private HttpChannelState _httpChannelState;
private Request _loggedRequest;
private HttpFields _trailers;
@ -777,26 +776,25 @@ public class HttpChannelState implements HttpChannel, Components
@Override
public Object getAttribute(String name)
{
HttpChannelState httpChannel = getHttpChannelState();
if (name.startsWith("org.eclipse.jetty"))
{
if (Server.class.getName().equals(name))
return httpChannel.getConnectionMetaData().getConnector().getServer();
return getConnectionMetaData().getConnector().getServer();
if (HttpChannelState.class.getName().equals(name))
return httpChannel;
return getHttpChannelState();
// TODO: is the instanceof needed?
// TODO: possibly remove this if statement or move to Servlet.
if (HttpConnection.class.getName().equals(name) &&
getConnectionMetaData().getConnection() instanceof HttpConnection)
return getConnectionMetaData().getConnection();
}
return httpChannel._requestAttributes.getAttribute(name);
return _attributes.getAttribute(name);
}
@Override
public Object removeAttribute(String name)
{
return getHttpChannelState()._requestAttributes.removeAttribute(name);
return _attributes.removeAttribute(name);
}
@Override
@ -804,19 +802,19 @@ public class HttpChannelState implements HttpChannel, Components
{
if (Server.class.getName().equals(name) || HttpChannelState.class.getName().equals(name) || HttpConnection.class.getName().equals(name))
return null;
return getHttpChannelState()._requestAttributes.setAttribute(name, attribute);
return _attributes.setAttribute(name, attribute);
}
@Override
public Set<String> getAttributeNameSet()
{
return getHttpChannelState()._requestAttributes.getAttributeNameSet();
return _attributes.getAttributeNameSet();
}
@Override
public void clearAttributes()
{
getHttpChannelState()._requestAttributes.clearAttributes();
_attributes.clearAttributes();
}
@Override
@ -837,7 +835,7 @@ public class HttpChannelState implements HttpChannel, Components
return _connectionMetaData;
}
HttpChannelState getHttpChannelState()
private HttpChannelState getHttpChannelState()
{
try (AutoLock ignore = _lock.lock())
{
@ -1178,16 +1176,15 @@ public class HttpChannelState implements HttpChannel, Components
{
long length = BufferUtil.length(content);
long totalWritten;
HttpChannelState httpChannelState;
HttpStream stream = null;
Throwable failure = null;
Throwable failure;
MetaData.Response responseMetaData = null;
try (AutoLock ignored = _request._lock.lock())
{
httpChannelState = _request.lockedGetHttpChannelState();
long committedContentLength = httpChannelState._committedContentLength;
totalWritten = _contentBytesWritten + length;
long totalWritten = _contentBytesWritten + length;
long contentLength = committedContentLength >= 0 ? committedContentLength : getHeaders().getLongField(HttpHeader.CONTENT_LENGTH);
if (_writeCallback != null)
@ -1195,11 +1192,14 @@ public class HttpChannelState implements HttpChannel, Components
else
{
failure = getFailure(httpChannelState);
if (failure == null && contentLength >= 0)
if (failure == null && contentLength >= 0 && totalWritten != contentLength)
{
// If the content length were not compatible with what was written, then we need to abort.
String lengthError = (totalWritten > contentLength) ? "written %d > %d content-length"
: (last && totalWritten < contentLength) ? "written %d < %d content-length" : null;
String lengthError = null;
if (totalWritten > contentLength)
lengthError = "written %d > %d content-length";
else if (last && !(totalWritten == 0 && HttpMethod.HEAD.is(_request.getMethod())))
lengthError = "written %d < %d content-length";
if (lengthError != null)
{
String message = lengthError.formatted(totalWritten, contentLength);
@ -1245,8 +1245,8 @@ public class HttpChannelState implements HttpChannel, Components
protected Throwable getFailure(HttpChannelState httpChannelState)
{
Content.Chunk.Error failure = httpChannelState._failure;
return failure == null ? null : failure.getCause();
Content.Chunk failure = httpChannelState._failure;
return failure == null ? null : failure.getFailure();
}
/**
@ -1441,7 +1441,7 @@ public class HttpChannelState implements HttpChannel, Components
long totalWritten = response._contentBytesWritten;
long committedContentLength = httpChannelState._committedContentLength;
if (committedContentLength >= 0 && committedContentLength != totalWritten)
if (committedContentLength >= 0 && committedContentLength != totalWritten && !(totalWritten == 0 && HttpMethod.HEAD.is(_request.getMethod())))
failure = new IOException("content-length %d != %d written".formatted(committedContentLength, totalWritten));
// is the request fully consumed?
@ -1696,7 +1696,7 @@ public class HttpChannelState implements HttpChannel, Components
protected void onError(Runnable task, Throwable failure)
{
ChannelRequest request;
Content.Chunk.Error error;
Content.Chunk error;
boolean callbackCompleted;
try (AutoLock ignore = _lock.lock())
{
@ -1728,9 +1728,9 @@ public class HttpChannelState implements HttpChannel, Components
{
// We are already in error, so we will not handle this one,
// but we will add as suppressed if we have not seen it already.
Throwable cause = error.getCause();
Throwable cause = error.getFailure();
if (ExceptionUtil.areNotAssociated(cause, failure))
error.getCause().addSuppressed(failure);
error.getFailure().addSuppressed(failure);
}
}
}

View File

@ -1098,8 +1098,8 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
{
BadMessageException bad = new BadMessageException("Early EOF");
if (stream._chunk instanceof Error error)
error.getCause().addSuppressed(bad);
if (Content.Chunk.isFailure(stream._chunk))
stream._chunk.getFailure().addSuppressed(bad);
else
{
if (stream._chunk != null)

View File

@ -657,9 +657,9 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
request.demand(this);
return;
}
if (chunk instanceof Content.Chunk.Error error)
if (Content.Chunk.isFailure(chunk))
{
callback.failed(error.getCause());
callback.failed(chunk.getFailure());
return;
}
// copy buffer

View File

@ -0,0 +1,92 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.eclipse.jetty.io.content.AsyncContent;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.FutureCallback;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class FormFieldsTest
{
public static Stream<Arguments> tests()
{
return Stream.of(
Arguments.of(List.of("name=value"), UTF_8, -1, -1, Map.of("name", "value")),
Arguments.of(List.of("name=value", ""), UTF_8, -1, -1, Map.of("name", "value")),
Arguments.of(List.of("name", "=value", ""), UTF_8, -1, -1, Map.of("name", "value")),
Arguments.of(List.of("n", "ame", "=", "value"), UTF_8, -1, -1, Map.of("name", "value")),
Arguments.of(List.of("n=v&X=Y"), UTF_8, 2, 4, Map.of("n", "v", "X", "Y")),
Arguments.of(List.of("name=f¤¤&X=Y"), UTF_8, -1, -1, Map.of("name", "f¤¤", "X", "Y")),
Arguments.of(List.of("n=v&X=Y"), UTF_8, 1, -1, null),
Arguments.of(List.of("n=v&X=Y"), UTF_8, -1, 3, null)
);
}
@ParameterizedTest
@MethodSource("tests")
public void testFormFields(List<String> chunks, Charset charset, int maxFields, int maxLength, Map<String, String> expected)
throws Exception
{
AsyncContent source = new AsyncContent();
Attributes attributes = new Attributes.Mapped();
CompletableFuture<Fields> futureFields = FormFields.from(source, attributes, charset, maxFields, maxLength);
assertFalse(futureFields.isDone());
int last = chunks.size() - 1;
FutureCallback eof = new FutureCallback();
for (int i = 0; i <= last; i++)
source.write(i == last, BufferUtil.toBuffer(chunks.get(i), charset), i == last ? eof : Callback.NOOP);
try
{
eof.get(10, TimeUnit.SECONDS);
assertTrue(futureFields.isDone());
Map<String, String> result = new HashMap<>();
for (Fields.Field f : futureFields.get())
result.put(f.getName(), f.getValue());
assertEquals(expected, result);
}
catch (AssertionError e)
{
throw e;
}
catch (Throwable e)
{
assertNull(expected);
}
}
}

View File

@ -710,9 +710,9 @@ public class GracefulHandlerTest
}
}
LOG.debug("chunk = {}", chunk);
if (chunk instanceof Content.Chunk.Error error)
if (Content.Chunk.isFailure(chunk))
{
Response.writeError(request, response, callback, error.getCause());
Response.writeError(request, response, callback, chunk.getFailure());
return true;
}
bytesRead += chunk.remaining();

View File

@ -66,7 +66,6 @@ import static org.hamcrest.Matchers.sameInstance;
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.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -1214,8 +1213,8 @@ public class HttpChannelTest
Request rq = handling.get().getRequest();
Content.Chunk chunk = rq.read();
assertTrue(chunk.isLast());
assertInstanceOf(Content.Chunk.Error.class, chunk);
assertThat(((Content.Chunk.Error)chunk).getCause(), sameInstance(failure));
assertTrue(Content.Chunk.isFailure(chunk, true));
assertThat(chunk.getFailure(), sameInstance(failure));
CountDownLatch demand = new CountDownLatch(1);
// Callback serialized until after onError task

View File

@ -36,6 +36,7 @@ import java.util.concurrent.atomic.AtomicReference;
import org.awaitility.Awaitility;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.io.AbstractConnection;
@ -542,10 +543,10 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
continue;
}
if (chunk instanceof Content.Chunk.Error error)
if (Content.Chunk.isFailure(chunk))
{
earlyEOFException.countDown();
throw IO.rethrow(error.getCause());
throw IO.rethrow(chunk.getFailure());
}
if (chunk.hasRemaining())
@ -1334,6 +1335,75 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
}
}
@Test
public void testHeadHandled() throws Exception
{
startServer(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.getHeaders().put(HttpHeader.CONTENT_LENGTH, 10);
if (HttpMethod.HEAD.is(request.getMethod()))
{
if (request.getHttpURI().getCanonicalPath().equals("/writeNull"))
response.write(true, null, callback);
else
callback.succeeded();
}
else
{
Content.Sink.write(response, true, "123456789\n", callback);
}
return true;
}
});
_httpConfiguration.setSendDateHeader(false);
_httpConfiguration.setSendServerVersion(false);
_httpConfiguration.setSendXPoweredBy(false);
try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
{
OutputStream os = client.getOutputStream();
InputStream is = client.getInputStream();
os.write("""
GET / HTTP/1.1
Host: localhost
HEAD / HTTP/1.1
Host: localhost
HEAD /writeNull HTTP/1.1
Host: localhost
GET / HTTP/1.1
Host: localhost
Connection: close
""".getBytes(StandardCharsets.ISO_8859_1));
String in = IO.toString(is);
assertThat(in.replace("\r", ""), is("""
HTTP/1.1 200 OK
Content-Length: 10
123456789
HTTP/1.1 200 OK
Content-Length: 10
HTTP/1.1 200 OK
Content-Length: 10
HTTP/1.1 200 OK
Content-Length: 10
Connection: close
123456789
"""));
}
}
@Test
public void testBlockedClient() throws Exception
{

View File

@ -119,9 +119,8 @@ public class MultiPartByteRangesTest
assertNotNull(contentType);
String boundary = MultiPart.extractBoundary(contentType);
MultiPartByteRanges byteRanges = new MultiPartByteRanges(boundary);
byteRanges.parse(new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes())));
MultiPartByteRanges.Parts parts = byteRanges.join();
MultiPartByteRanges.Parser byteRanges = new MultiPartByteRanges.Parser(boundary);
MultiPartByteRanges.Parts parts = byteRanges.parse(new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes()))).join();
assertEquals(3, parts.size());
MultiPart.Part part1 = parts.get(0);

View File

@ -17,7 +17,9 @@ import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpHeader;
@ -30,6 +32,9 @@ import org.eclipse.jetty.util.component.LifeCycle;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
@ -372,6 +377,39 @@ public class RequestTest
assertThat(response.getStatus(), is(HttpStatus.OK_200));
}
public static Stream<Arguments> localeTests()
{
return Stream.of(
Arguments.of(null, List.of(Locale.getDefault().toLanguageTag()).toString()),
Arguments.of("zz", "[zz]"),
Arguments.of("en", "[en]"),
Arguments.of("en-gb", List.of(Locale.UK.toLanguageTag()).toString()),
Arguments.of("en-us", List.of(Locale.US.toLanguageTag()).toString()),
Arguments.of("EN-US", List.of(Locale.US.toLanguageTag()).toString()),
Arguments.of("en-us,en-gb", List.of(Locale.US.toLanguageTag(), Locale.UK.toLanguageTag()).toString()),
Arguments.of("en-us;q=0.5,fr;q=0.0,en-gb;q=1.0", List.of(Locale.UK.toLanguageTag(), Locale.US.toLanguageTag()).toString()),
Arguments.of("en-us;q=0.5,zz-yy;q=0.7,en-gb;q=1.0", List.of(Locale.UK.toLanguageTag(), Locale.US.toLanguageTag(), "zz-YY").toString())
);
}
@ParameterizedTest
@MethodSource("localeTests")
public void testAcceptableLocales(String acceptLanguage, String expectedLocales) throws Exception
{
acceptLanguage = acceptLanguage == null ? "" : (HttpHeader.ACCEPT_LANGUAGE.asString() + ": " + acceptLanguage + "\n");
String rawRequest = """
GET / HTTP/1.1
Host: tester
Connection: close
%s
""".formatted(acceptLanguage);
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(rawRequest));
assertNotNull(response);
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(response.getContent(), containsString("locales=" + expectedLocales));
}
private static void checkCookieResult(String containedCookie, String[] notContainedCookies, String response)
{
assertNotNull(containedCookie);

View File

@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.LocalConnector.LocalEndPoint;
import org.eclipse.jetty.server.handler.ContextHandler;
@ -39,11 +40,11 @@ import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.util.component.LifeCycle;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
@ -57,7 +58,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@Disabled // TODO
public class StopTest
{
private static final Logger LOG = LoggerFactory.getLogger(StopTest.class);
@ -93,7 +93,7 @@ public class StopTest
}
catch (Exception e)
{
e.printStackTrace();
throw new RuntimeException(e);
}
});
stopper.start();
@ -108,10 +108,7 @@ public class StopTest
).getBytes());
client.getOutputStream().flush();
while (!connector.isShutdown())
{
Thread.sleep(10);
}
await().atMost(10, TimeUnit.SECONDS).until(connector::isShutdown);
handler.latchB.countDown();
@ -281,89 +278,83 @@ public class StopTest
LocalConnector connector = new LocalConnector(server);
server.addConnector(connector);
StatisticsHandler stats = new StatisticsHandler();
ContextHandler context = new ContextHandler("/");
StatisticsHandler stats = new StatisticsHandler(context);
server.setHandler(stats);
ContextHandler context = new ContextHandler(stats, "/");
Exchanger<Void> exchanger0 = new Exchanger<>();
Exchanger<Void> exchanger1 = new Exchanger<>();
/* TODO
context.setHandler(new AbstractHandler()
context.setHandler(new Handler.Abstract()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
try
{
exchanger0.exchange(null);
exchanger1.exchange(null);
response.setStatus(200);
Content.Sink.write(response, true, "The Response", callback);
}
catch (Throwable x)
{
throw new ServletException(x);
callback.failed(x);
}
baseRequest.setHandled(true);
response.setStatus(200);
response.getWriter().println("The Response");
response.getWriter().close();
return true;
}
});
*/
server.setStopTimeout(1000);
server.start();
LocalEndPoint endp = connector.executeRequest(
"GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"\r\n"
);
exchanger0.exchange(null);
exchanger1.exchange(null);
String response = endp.getResponse();
assertThat(response, containsString("200 OK"));
assertThat(response, Matchers.not(containsString("Connection: close")));
endp.addInputAndExecute(BufferUtil.toBuffer("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"));
exchanger0.exchange(null);
FutureCallback stopped = new FutureCallback();
new Thread(() ->
try (LocalEndPoint endp = connector.executeRequest(
"""
GET / HTTP/1.1\r
Host: localhost\r
\r
"""
))
{
try
{
server.stop();
stopped.succeeded();
}
catch (Throwable e)
{
stopped.failed(e);
}
}).start();
exchanger0.exchange(null);
exchanger1.exchange(null);
long start = NanoTime.now();
while (!connector.isShutdown())
{
assertThat(NanoTime.secondsSince(start), lessThan(10L));
Thread.sleep(10);
String response = endp.getResponse();
assertThat(response, containsString("200 OK"));
assertThat(response, Matchers.not(containsString("Connection: close")));
endp.addInputAndExecute(BufferUtil.toBuffer("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"));
exchanger0.exchange(null);
FutureCallback stopped = new FutureCallback();
new Thread(() ->
{
try
{
server.stop();
stopped.succeeded();
}
catch (Throwable e)
{
stopped.failed(e);
}
}).start();
await().atMost(10, TimeUnit.SECONDS).until(connector::isShutdown);
// Check new connections rejected!
assertThrows(IllegalStateException.class, () -> connector.getResponse("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"));
// Check completed 200 has close
exchanger1.exchange(null);
response = endp.getResponse();
assertThat(response, containsString("200 OK"));
assertThat(response, Matchers.containsString("Connection: close"));
stopped.get(10, TimeUnit.SECONDS);
}
// Check new connections rejected!
assertThrows(IllegalStateException.class, () -> connector.getResponse("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"));
// Check completed 200 has close
exchanger1.exchange(null);
response = endp.getResponse();
assertThat(response, containsString("200 OK"));
assertThat(response, Matchers.containsString("Connection: close"));
stopped.get(10, TimeUnit.SECONDS);
}
@Test
@ -374,85 +365,81 @@ public class StopTest
LocalConnector connector = new LocalConnector(server);
server.addConnector(connector);
ContextHandler context = new ContextHandler(server, "/");
ContextHandler context = new ContextHandler("/");
server.setHandler(context);
StatisticsHandler stats = new StatisticsHandler();
context.setHandler(stats);
Exchanger<Void> exchanger0 = new Exchanger<>();
Exchanger<Void> exchanger1 = new Exchanger<>();
/* TODO
stats.setHandler(new AbstractHandler()
stats.setHandler(new Handler.Abstract()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
try
{
exchanger0.exchange(null);
exchanger1.exchange(null);
response.setStatus(200);
Content.Sink.write(response, true, "The Response", callback);
}
catch (Throwable x)
{
throw new ServletException(x);
callback.failed(x);
}
baseRequest.setHandled(true);
response.setStatus(200);
response.getWriter().println("The Response");
response.getWriter().close();
return true;
}
});
*/
server.start();
LocalEndPoint endp = connector.executeRequest(
"GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"\r\n"
);
exchanger0.exchange(null);
exchanger1.exchange(null);
String response = endp.getResponse();
assertThat(response, containsString("200 OK"));
assertThat(response, Matchers.not(containsString("Connection: close")));
endp.addInputAndExecute(BufferUtil.toBuffer("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"));
exchanger0.exchange(null);
CountDownLatch latch = new CountDownLatch(1);
new Thread(() ->
try (LocalEndPoint endp = connector.executeRequest(
"""
GET / HTTP/1.1\r
Host: localhost\r
\r
"""
))
{
try
exchanger0.exchange(null);
exchanger1.exchange(null);
String response = endp.getResponse();
assertThat(response, containsString("200 OK"));
assertThat(response, Matchers.not(containsString("Connection: close")));
endp.addInputAndExecute(BufferUtil.toBuffer("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"));
exchanger0.exchange(null);
CountDownLatch latch = new CountDownLatch(1);
new Thread(() ->
{
context.stop();
latch.countDown();
}
catch (Exception e)
{
e.printStackTrace();
}
}).start();
while (context.isStarted())
{
Thread.sleep(10);
try
{
context.stop();
latch.countDown();
}
catch (Exception e)
{
e.printStackTrace();
}
}).start();
await().atMost(10, TimeUnit.SECONDS).until(context::isStopped);
// Check new connections accepted, but don't find context!
String unavailable = connector.getResponse("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n");
assertThat(unavailable, containsString(" 404 Not Found"));
// Check completed 200 does not have close
exchanger1.exchange(null);
response = endp.getResponse();
assertThat(response, containsString("200 OK"));
assertThat(response, Matchers.not(Matchers.containsString("Connection: close")));
assertTrue(latch.await(10, TimeUnit.SECONDS));
}
// Check new connections accepted, but don't find context!
String unavailable = connector.getResponse("GET / HTTP/1.1\r\nHost:localhost\r\n\r\n");
assertThat(unavailable, containsString(" 404 Not Found"));
// Check completed 200 does not have close
exchanger1.exchange(null);
response = endp.getResponse();
assertThat(response, containsString("200 OK"));
assertThat(response, Matchers.not(Matchers.containsString("Connection: close")));
assertTrue(latch.await(10, TimeUnit.SECONDS));
}
@Test
@ -486,12 +473,12 @@ public class StopTest
ContextHandler context2 = new ContextHandler("/two")
{
@Override
protected void doStart() throws Exception
protected void doStart()
{
context2Started.set(true);
}
};
contexts.setHandlers(new Handler[]{context0, context1, context2});
contexts.setHandlers(context0, context1, context2);
try
{
@ -535,30 +522,19 @@ public class StopTest
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.getHeaders().put(HttpHeader.CONTENT_LENGTH, 2);
response.write(true, ByteBuffer.wrap("a".getBytes()), new Callback()
request.getContext().run(() ->
{
@Override
public void succeeded()
try
{
try
{
latchA.countDown();
latchB.await();
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
response.write(true, ByteBuffer.wrap("b".getBytes()), callback);
latchA.countDown();
latchB.await();
}
@Override
public void failed(Throwable x)
catch (InterruptedException e)
{
callback.failed(x);
throw new RuntimeException(e);
}
response.write(true, ByteBuffer.wrap("ab".getBytes()), callback);
});
return true;
}
}

View File

@ -17,6 +17,7 @@ import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
@ -105,9 +106,9 @@ public class DumpHandler extends Handler.Abstract
}
}
if (chunk instanceof Content.Chunk.Error error)
if (Content.Chunk.isFailure(chunk))
{
callback.failed(error.getCause());
callback.failed(chunk.getFailure());
return true;
}
@ -151,6 +152,7 @@ public class DumpHandler extends Handler.Abstract
writer.write("<pre>httpURI.path=" + httpURI.getPath() + "</pre><br/>\n");
writer.write("<pre>httpURI.query=" + httpURI.getQuery() + "</pre><br/>\n");
writer.write("<pre>httpURI.pathQuery=" + httpURI.getPathQuery() + "</pre><br/>\n");
writer.write("<pre>locales=" + Request.getLocales(request).stream().map(Locale::toLanguageTag).toList() + "</pre><br/>\n");
writer.write("<pre>pathInContext=" + Request.getPathInContext(request) + "</pre><br/>\n");
writer.write("<pre>contentType=" + request.getHeaders().get(HttpHeader.CONTENT_TYPE) + "</pre><br/>\n");
writer.write("<pre>servername=" + Request.getServerName(request) + "</pre><br/>\n");

View File

@ -1,645 +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.io.File;
import java.nio.file.Path;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@Disabled // TODO
@ExtendWith(WorkDirExtension.class)
public class FileBufferedResponseHandlerTest
{
private static final Logger LOG = LoggerFactory.getLogger(FileBufferedResponseHandlerTest.class);
public WorkDir _workDir;
private final CountDownLatch _disposeLatch = new CountDownLatch(1);
private Server _server;
private LocalConnector _localConnector;
private ServerConnector _serverConnector;
private Path _testDir;
private FileBufferedResponseHandler _bufferedHandler;
/* TODO
@BeforeEach
public void before() throws Exception
{
_testDir = _workDir.getEmptyPathDir();
_server = new Server();
HttpConfiguration config = new HttpConfiguration();
config.setOutputBufferSize(1024);
config.setOutputAggregationSize(256);
_localConnector = new LocalConnector(_server, new HttpConnectionFactory(config));
_localConnector.setIdleTimeout(Duration.ofMinutes(1).toMillis());
_server.addConnector(_localConnector);
_serverConnector = new ServerConnector(_server, new HttpConnectionFactory(config));
_server.addConnector(_serverConnector);
_bufferedHandler = new FileBufferedResponseHandler()
{
@Override
protected BufferedInterceptor newBufferedInterceptor(HttpChannel httpChannel, HttpOutput.Interceptor interceptor)
{
return new FileBufferedInterceptor(httpChannel, interceptor)
{
@Override
protected void dispose()
{
super.dispose();
_disposeLatch.countDown();
}
};
}
};
_bufferedHandler.setTempDir(_testDir);
_bufferedHandler.getPathIncludeExclude().include("/include/*");
_bufferedHandler.getPathIncludeExclude().exclude("*.exclude");
_bufferedHandler.getMimeIncludeExclude().exclude("text/excluded");
_server.setHandler(_bufferedHandler);
}
@AfterEach
public void after() throws Exception
{
_server.stop();
}
@Test
public void testPathNotIncluded() throws Exception
{
_bufferedHandler.setHandler(new Handler.Abstract()
{
@Override
public void handle(request request, Response response, Callback callback) throws Exception
{
response.setBufferSize(10);
PrintWriter writer = response.getWriter();
writer.println("a string larger than the buffer size");
writer.println("Committed: " + response.isCommitted());
writer.println("NumFiles: " + getNumFiles());
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The response was committed after the first write and we never created a file to buffer the response into.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, containsString("Committed: true"));
assertThat(responseContent, containsString("NumFiles: 0"));
assertThat(getNumFiles(), is(0));
}
@Test
public void testIncludedByPath() throws Exception
{
_bufferedHandler.setHandler(new Handler.Abstract()
{
@Override
public void handle(request request, Response response, Callback callback) throws Exception
{
response.setBufferSize(10);
PrintWriter writer = response.getWriter();
writer.println("a string larger than the buffer size");
writer.println("Committed: " + response.isCommitted());
writer.println("NumFiles: " + getNumFiles());
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The response was not committed after the first write and a file was created to buffer the response.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, containsString("Committed: false"));
assertThat(responseContent, containsString("NumFiles: 1"));
// Unable to verify file deletion on windows, as immediate delete not possible.
// only after a GC has occurred.
if (!OS.WINDOWS.isCurrentOs())
{
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
assertThat(getNumFiles(), is(0));
}
}
@Test
public void testExcludedByPath() throws Exception
{
_bufferedHandler.setHandler(new Handler.Abstract()
{
@Override
public void handle(request request, Response response, Callback callback) throws Exception
{
response.setBufferSize(10);
PrintWriter writer = response.getWriter();
writer.println("a string larger than the buffer size");
writer.println("Committed: " + response.isCommitted());
writer.println("NumFiles: " + getNumFiles());
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path.exclude HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The response was committed after the first write and we never created a file to buffer the response into.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, containsString("Committed: true"));
assertThat(responseContent, containsString("NumFiles: 0"));
assertThat(getNumFiles(), is(0));
}
@Test
public void testExcludedByMime() throws Exception
{
String excludedMimeType = "text/excluded";
_bufferedHandler.setHandler(new Handler.Abstract()
{
@Override
public void handle(request request, Response response, Callback callback) throws Exception
{
response.setContentType(excludedMimeType);
response.setBufferSize(10);
PrintWriter writer = response.getWriter();
writer.println("a string larger than the buffer size");
writer.println("Committed: " + response.isCommitted());
writer.println("NumFiles: " + getNumFiles());
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The response was committed after the first write and we never created a file to buffer the response into.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, containsString("Committed: true"));
assertThat(responseContent, containsString("NumFiles: 0"));
assertThat(getNumFiles(), is(0));
}
@Test
public void testFlushed() throws Exception
{
_bufferedHandler.setHandler(new Handler.Abstract()
{
@Override
public void handle(request request, Response response, Callback callback) throws Exception
{
response.setBufferSize(1024);
PrintWriter writer = response.getWriter();
writer.println("a string smaller than the buffer size");
writer.println("NumFilesBeforeFlush: " + getNumFiles());
writer.flush();
writer.println("Committed: " + response.isCommitted());
writer.println("NumFiles: " + getNumFiles());
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The response was not committed after the buffer was flushed and a file was created to buffer the response.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, containsString("NumFilesBeforeFlush: 0"));
assertThat(responseContent, containsString("Committed: false"));
assertThat(responseContent, containsString("NumFiles: 1"));
// Unable to verify file deletion on windows, as immediate delete not possible.
// only after a GC has occurred.
if (!OS.WINDOWS.isCurrentOs())
{
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
assertThat(getNumFiles(), is(0));
}
}
@Test
public void testClosed() throws Exception
{
_bufferedHandler.setHandler(new Handler.Abstract()
{
@Override
public void handle(request request, Response response, Callback callback) throws Exception
{
response.setBufferSize(10);
PrintWriter writer = response.getWriter();
writer.println("a string larger than the buffer size");
writer.println("NumFiles: " + getNumFiles());
writer.close();
writer.println("writtenAfterClose");
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The content written after close was not sent.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, not(containsString("writtenAfterClose")));
assertThat(responseContent, containsString("NumFiles: 1"));
// Unable to verify file deletion on windows, as immediate delete not possible.
// only after a GC has occurred.
if (!OS.WINDOWS.isCurrentOs())
{
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
assertThat(getNumFiles(), is(0));
}
}
@Test
public void testBufferSizeBig() throws Exception
{
int bufferSize = 4096;
String largeContent = generateContent(bufferSize - 64);
_bufferedHandler.setHandler(new Handler.Abstract()
{
@Override
public void handle(request request, Response response, Callback callback) throws Exception
{
response.setBufferSize(bufferSize);
PrintWriter writer = response.getWriter();
writer.println(largeContent);
writer.println("Committed: " + response.isCommitted());
writer.println("NumFiles: " + getNumFiles());
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The content written was not buffered as a file as it was less than the buffer size.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, not(containsString("writtenAfterClose")));
assertThat(responseContent, containsString("Committed: false"));
assertThat(responseContent, containsString("NumFiles: 0"));
assertThat(getNumFiles(), is(0));
}
@Test
public void testFlushEmpty() throws Exception
{
_bufferedHandler.setHandler(new Handler.Abstract()
{
@Override
public void handle(request request, Response response, Callback callback) throws Exception
{
response.setBufferSize(1024);
PrintWriter writer = response.getWriter();
writer.flush();
int numFiles = getNumFiles();
writer.println("NumFiles: " + numFiles);
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The flush should not create the file unless there is content to write.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, containsString("NumFiles: 0"));
// Unable to verify file deletion on windows, as immediate delete not possible.
// only after a GC has occurred.
if (!OS.WINDOWS.isCurrentOs())
{
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
assertThat(getNumFiles(), is(0));
}
}
@Test
public void testReset() throws Exception
{
_bufferedHandler.setHandler(new Handler.Abstract()
{
@Override
public void handle(request request, Response response, Callback callback) throws Exception
{
response.setBufferSize(8);
PrintWriter writer = response.getWriter();
writer.println("THIS WILL BE RESET");
writer.flush();
writer.println("THIS WILL BE RESET");
int numFilesBeforeReset = getNumFiles();
response.resetBuffer();
int numFilesAfterReset = getNumFiles();
writer.println("NumFilesBeforeReset: " + numFilesBeforeReset);
writer.println("NumFilesAfterReset: " + numFilesAfterReset);
writer.println("a string larger than the buffer size");
writer.println("NumFilesAfterWrite: " + getNumFiles());
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// Resetting the response buffer will delete the file.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, not(containsString("THIS WILL BE RESET")));
assertThat(responseContent, containsString("NumFilesBeforeReset: 1"));
assertThat(responseContent, containsString("NumFilesAfterReset: 0"));
assertThat(responseContent, containsString("NumFilesAfterWrite: 1"));
// Unable to verify file deletion on windows, as immediate delete not possible.
// only after a GC has occurred.
if (!OS.WINDOWS.isCurrentOs())
{
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
assertThat(getNumFiles(), is(0));
}
}
@Test
public void testFileLargerThanMaxInteger() throws Exception
{
long fileSize = Integer.MAX_VALUE + 1234L;
byte[] bytes = randomBytes(1024 * 1024);
_bufferedHandler.setHandler(new Handler.Abstract()
{
@Override
public void handle(request request, Response response, Callback callback) throws Exception
{
ServletOutputStream outputStream = response.getOutputStream();
long written = 0;
while (written < fileSize)
{
int length = Math.toIntExact(Math.min(bytes.length, fileSize - written));
outputStream.write(bytes, 0, length);
written += length;
}
outputStream.flush();
response.getHeaders().put("NumFiles", Integer.toString(getNumFiles()));
response.getHeaders().put("FileSize", Long.toString(getFileSize()));
}
});
_server.start();
AtomicLong received = new AtomicLong();
HttpTester.Response response = new HttpTester.Response()
{
@Override
public boolean content(ByteBuffer ref)
{
// Verify the content is what was sent.
while (ref.hasRemaining())
{
byte byteFromBuffer = ref.get();
long totalReceived = received.getAndIncrement();
int bytesIndex = (int)(totalReceived % bytes.length);
byte byteFromArray = bytes[bytesIndex];
if (byteFromBuffer != byteFromArray)
{
LOG.warn("Mismatch at index {} received bytes {}, {}!={}", bytesIndex, totalReceived, byteFromBuffer, byteFromArray, new IllegalStateException());
return true;
}
}
return false;
}
};
try (Socket socket = new Socket("localhost", _serverConnector.getLocalPort()))
{
OutputStream output = socket.getOutputStream();
String request = "GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n";
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
HttpTester.Input input = HttpTester.from(socket.getInputStream());
HttpTester.parseResponse(input, response);
}
assertTrue(response.isComplete());
assertThat(response.get("NumFiles"), is("1"));
assertThat(response.get("FileSize"), is(Long.toString(fileSize)));
assertThat(received.get(), is(fileSize));
// Unable to verify file deletion on windows, as immediate delete not possible.
// only after a GC has occurred.
if (!OS.WINDOWS.isCurrentOs())
{
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
assertThat(getNumFiles(), is(0));
}
}
@Test
public void testNextInterceptorFailed() throws Exception
{
AbstractHandler failingInterceptorHandler = new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
HttpOutput httpOutput = baseRequest.getResponse().getHttpOutput();
HttpOutput.Interceptor nextInterceptor = httpOutput.getInterceptor();
httpOutput.setInterceptor(new HttpOutput.Interceptor()
{
@Override
public void write(ByteBuffer content, boolean last, Callback callback)
{
callback.failed(new Throwable("intentionally throwing from interceptor"));
}
@Override
public HttpOutput.Interceptor getNextInterceptor()
{
return nextInterceptor;
}
});
}
};
_server.setHandler(new HandlerCollection(failingInterceptorHandler, _server.getHandler()));
CompletableFuture<Throwable> errorFuture = new CompletableFuture<>();
_bufferedHandler.setHandler(new Handler.Abstract()
{
@Override
public void handle(request request, Response response, Callback callback) throws Exception
{
byte[] chunk1 = "this content will ".getBytes();
byte[] chunk2 = "be buffered in a file".getBytes();
response.setContentLength(chunk1.length + chunk2.length);
ServletOutputStream outputStream = response.getOutputStream();
// Write chunk1 and then flush so it is written to the file.
outputStream.write(chunk1);
outputStream.flush();
assertThat(getNumFiles(), is(1));
try
{
// ContentLength is set so it knows this is the last write.
// This will cause the file to be written to the next interceptor which will fail.
outputStream.write(chunk2);
}
catch (Throwable t)
{
errorFuture.complete(t);
throw t;
}
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
// Response was aborted.
assertThat(response.getStatus(), is(0));
// We failed because of the next interceptor.
Throwable error = errorFuture.get(5, TimeUnit.SECONDS);
assertThat(error.getMessage(), containsString("intentionally throwing from interceptor"));
// Unable to verify file deletion on windows, as immediate delete not possible.
// only after a GC has occurred.
if (!OS.WINDOWS.isCurrentOs())
{
// All files were deleted.
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
assertThat(getNumFiles(), is(0));
}
}
@Test
public void testFileWriteFailed() throws Exception
{
// Set the temp directory to an empty directory so that the file cannot be created.
File tempDir = MavenTestingUtils.getTargetTestingDir(getClass().getSimpleName());
FS.ensureDeleted(tempDir);
_bufferedHandler.setTempDir(tempDir.toPath());
CompletableFuture<Throwable> errorFuture = new CompletableFuture<>();
_bufferedHandler.setHandler(new Handler.Abstract()
{
@Override
public void handle(request request, Response response, Callback callback) throws Exception
{
ServletOutputStream outputStream = response.getOutputStream();
byte[] content = "this content will be buffered in a file".getBytes();
try
{
// Write the content and flush it to the file.
// This should throw as it cannot create the file to aggregate into.
outputStream.write(content);
outputStream.flush();
}
catch (Throwable t)
{
errorFuture.complete(t);
throw t;
}
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
// Response was aborted.
assertThat(response.getStatus(), is(0));
// We failed because cannot create the file.
Throwable error = errorFuture.get(5, TimeUnit.SECONDS);
assertThat(error, instanceOf(NoSuchFileException.class));
// No files were created.
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
assertThat(getNumFiles(), is(0));
}
*/
private int getNumFiles()
{
File[] files = _testDir.toFile().listFiles();
if (files == null)
return 0;
return files.length;
}
private long getFileSize()
{
File[] files = _testDir.toFile().listFiles();
assertNotNull(files);
assertThat(files.length, is(1));
return files[0].length();
}
private static String generateContent(int size)
{
Random random = new Random();
StringBuilder stringBuilder = new StringBuilder(size);
for (int i = 0; i < size; i++)
{
stringBuilder.append((char)Math.abs(random.nextInt(0x7F)));
}
return stringBuilder.toString();
}
@SuppressWarnings("SameParameterValue")
private byte[] randomBytes(int size)
{
byte[] data = new byte[size];
new Random().nextBytes(data);
return data;
}
}

View File

@ -17,8 +17,6 @@ import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.file.Path;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
@ -44,7 +42,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import static java.nio.charset.StandardCharsets.UTF_8;
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;
@ -79,7 +76,8 @@ public class MultiPartFormDataHandlerTest
public boolean handle(Request request, Response response, Callback callback)
{
String boundary = MultiPart.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE));
new MultiPartFormData(boundary).parse(request)
new MultiPartFormData.Parser(boundary)
.parse(request)
.whenComplete((parts, failure) ->
{
if (parts != null)
@ -118,72 +116,6 @@ public class MultiPartFormDataHandlerTest
}
}
@Test
public void testDelayedUntilFormData() throws Exception
{
DelayedHandler delayedHandler = new DelayedHandler();
CountDownLatch processLatch = new CountDownLatch(1);
delayedHandler.setHandler(new Handler.Abstract.NonBlocking()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
processLatch.countDown();
MultiPartFormData.Parts parts = (MultiPartFormData.Parts)request.getAttribute(MultiPartFormData.Parts.class.getName());
assertNotNull(parts);
MultiPart.Part part = parts.get(0);
Content.copy(part.getContentSource(), response, callback);
return true;
}
});
start(delayedHandler);
try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort())))
{
String contentBegin = """
--A1B2C3
Content-Disposition: form-data; name="part"
""";
String contentMiddle = """
0123456789\
""";
String contentEnd = """
ABCDEF
--A1B2C3--
""";
String header = """
POST / HTTP/1.1
Host: localhost
Content-Type: multipart/form-data; boundary=A1B2C3
Content-Length: $L
""".replace("$L", String.valueOf(contentBegin.length() + contentMiddle.length() + contentEnd.length()));
client.write(UTF_8.encode(header));
client.write(UTF_8.encode(contentBegin));
// Verify that the handler has not been called yet.
assertFalse(processLatch.await(1, TimeUnit.SECONDS));
client.write(UTF_8.encode(contentMiddle));
// Verify that the handler has not been called yet.
assertFalse(processLatch.await(1, TimeUnit.SECONDS));
// Finish to send the content.
client.write(UTF_8.encode(contentEnd));
// Verify that the handler has been called.
assertTrue(processLatch.await(5, TimeUnit.SECONDS));
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client));
assertNotNull(response);
assertEquals(HttpStatus.OK_200, response.getStatus());
assertEquals("0123456789ABCDEF", response.getContent());
}
}
@Test
public void testEchoMultiPart() throws Exception
{
@ -193,13 +125,15 @@ public class MultiPartFormDataHandlerTest
public boolean handle(Request request, Response response, Callback callback)
{
String boundary = MultiPart.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE));
new MultiPartFormData(boundary).parse(request)
new MultiPartFormData.Parser(boundary)
.parse(request)
.whenComplete((parts, failure) ->
{
if (parts != null)
{
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "multipart/form-data; boundary=\"%s\"".formatted(parts.getMultiPartFormData().getBoundary()));
MultiPartFormData.ContentSource source = new MultiPartFormData.ContentSource(parts.getMultiPartFormData().getBoundary());
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "multipart/form-data; boundary=\"%s\"".formatted(boundary));
MultiPartFormData.ContentSource source = new MultiPartFormData.ContentSource(boundary);
source.setPartHeadersMaxLength(1024);
parts.forEach(source::addPart);
source.close();
@ -310,22 +244,22 @@ public class MultiPartFormDataHandlerTest
String boundary = MultiPart.extractBoundary(value);
assertNotNull(boundary);
MultiPartFormData formData = new MultiPartFormData(boundary);
ByteBufferContentSource byteBufferContentSource = new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes()));
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
formData.setFilesDirectory(tempDir);
formData.parse(new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes())));
MultiPartFormData.Parts parts = formData.join();
assertEquals(2, parts.size());
MultiPart.Part part1 = parts.get(0);
assertEquals("part1", part1.getName());
assertEquals("hello", part1.getContentAsString(UTF_8));
MultiPart.Part part2 = parts.get(1);
assertEquals("part2", part2.getName());
assertEquals("file2.bin", part2.getFileName());
HttpFields headers2 = part2.getHeaders();
assertEquals(2, headers2.size());
assertEquals("application/octet-stream", headers2.get(HttpHeader.CONTENT_TYPE));
assertEquals(32, part2.getContentSource().getLength());
try (MultiPartFormData.Parts parts = formData.parse(byteBufferContentSource).join())
{
assertEquals(2, parts.size());
MultiPart.Part part1 = parts.get(0);
assertEquals("part1", part1.getName());
assertEquals("hello", part1.getContentAsString(UTF_8));
MultiPart.Part part2 = parts.get(1);
assertEquals("part2", part2.getName());
assertEquals("file2.bin", part2.getFileName());
HttpFields headers2 = part2.getHeaders();
assertEquals(2, headers2.size());
assertEquals("application/octet-stream", headers2.get(HttpHeader.CONTENT_TYPE));
}
}
}
}

View File

@ -175,7 +175,7 @@ public class ResourceHandlerByteRangesTest
String contentType = response.get(HttpHeader.CONTENT_TYPE);
assertThat(contentType, startsWith(responseContentType));
String boundary = MultiPart.extractBoundary(contentType);
MultiPartByteRanges.Parts parts = new MultiPartByteRanges(boundary)
MultiPartByteRanges.Parts parts = new MultiPartByteRanges.Parser(boundary)
.parse(new ByteBufferContentSource(response.getContentByteBuffer()))
.join();
assertEquals(2, parts.size());

View File

@ -100,9 +100,9 @@ public class StatisticsHandlerTest
return true;
}
if (chunk instanceof Content.Chunk.Error errorContent)
if (Content.Chunk.isFailure(chunk))
{
callback.failed(errorContent.getCause());
callback.failed(chunk.getFailure());
return true;
}
@ -158,7 +158,7 @@ public class StatisticsHandlerTest
AtomicReference<Throwable> exceptionRef = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
int expectedContentLength = 1000;
StatisticsHandler.MinimumDataRateHandler mdrh = new StatisticsHandler.MinimumDataRateHandler(0, 1000, new Handler.Abstract.NonBlocking()
StatisticsHandler.MinimumDataRateHandler mdrh = new StatisticsHandler.MinimumDataRateHandler(new Handler.Abstract.NonBlocking()
{
@Override
public boolean handle(Request request, Response response, Callback callback)
@ -217,7 +217,7 @@ public class StatisticsHandlerTest
response.write(true, ByteBuffer.allocate(1), finalCallback);
}
}
});
}, 0, 1000);
_latchHandler.setHandler(mdrh);
_server.start();

View File

@ -284,8 +284,8 @@ public class ThreadLimitHandlerTest
request.demand(this);
return;
}
if (chunk instanceof Error error)
throw error.getCause();
if (Content.Chunk.isFailure(chunk))
throw chunk.getFailure();
if (chunk.hasRemaining())
read.addAndGet(chunk.remaining());

View File

@ -1984,11 +1984,8 @@ public class GzipHandlerTest
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain");
Fields queryParameters = Request.extractQueryParameters(request);
FormFields futureFormFields = new FormFields(request, StandardCharsets.UTF_8, -1, -1);
futureFormFields.run();
Fields formParameters = futureFormFields.get();
Fields formParameters = FormFields.from(request, UTF_8, -1, -1).get();
Fields parameters = Fields.combine(queryParameters, formParameters);
String dump = parameters.stream().map(f -> "%s: %s\n".formatted(f.getName(), f.getValue())).collect(Collectors.joining());

View File

@ -51,9 +51,10 @@ class Timestamp
public Timestamp(TimeZone timeZone)
{
tzFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
zoneId = timeZone.toZoneId();
tzFormatter.withZone(zoneId);
tzFormatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(zoneId);
tick = null;
}

View File

@ -63,7 +63,6 @@ import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -256,8 +255,8 @@ public class HttpClientTest extends AbstractTest
continue;
}
}
if (chunk instanceof Content.Chunk.Error error)
throw IO.rethrow(error.getCause());
if (Content.Chunk.isFailure(chunk))
throw IO.rethrow(chunk.getFailure());
total += chunk.remaining();
if (total >= sleep)
@ -941,7 +940,7 @@ public class HttpClientTest extends AbstractTest
assertThat(chunks2.stream().mapToInt(c -> c.getByteBuffer().remaining()).sum(), is(totalBytes));
assertThat(chunks3.stream().mapToInt(c -> c.getByteBuffer().remaining()).sum(), is(0));
assertThat(chunks3.size(), is(1));
assertThat(chunks3.get(0), instanceOf(Content.Chunk.Error.class));
assertTrue(Content.Chunk.isFailure(chunks3.get(0), true));
chunks1.forEach(Content.Chunk::release);
chunks2.forEach(Content.Chunk::release);
@ -983,7 +982,7 @@ public class HttpClientTest extends AbstractTest
assertThat(chunks3Latch.await(5, TimeUnit.SECONDS), is(true));
assertThat(chunks3.stream().mapToInt(c -> c.getByteBuffer().remaining()).sum(), is(0));
assertThat(chunks3.size(), is(1));
assertThat(chunks3.get(0), instanceOf(Content.Chunk.Error.class));
assertTrue(Content.Chunk.isFailure(chunks3.get(0), true));
chunks1.forEach(Content.Chunk::release);
chunks2.forEach(Content.Chunk::release);

View File

@ -130,8 +130,8 @@ public class ServerTimeoutsTest extends AbstractTest
// Reads should yield the idle timeout.
Content.Chunk chunk = requestRef.get().read();
assertThat(chunk, instanceOf(Content.Chunk.Error.class));
Throwable cause = ((Content.Chunk.Error)chunk).getCause();
assertTrue(Content.Chunk.isFailure(chunk, true));
Throwable cause = chunk.getFailure();
assertThat(cause, instanceOf(TimeoutException.class));
// Complete the callback as the error listener promised.

View File

@ -13,7 +13,6 @@
package org.eclipse.jetty.util;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
@ -23,7 +22,6 @@ import java.util.List;
* Utility methods for Array manipulation
*/
public class ArrayUtil
implements Cloneable, Serializable
{
public static <T> T[] removeFromArray(T[] array, Object item)
@ -34,7 +32,7 @@ public class ArrayUtil
{
if (item.equals(array[i]))
{
Class<?> c = array == null ? item.getClass() : array.getClass().getComponentType();
Class<?> c = array.getClass().getComponentType();
@SuppressWarnings("unchecked")
T[] na = (T[])Array.newInstance(c, Array.getLength(array) - 1);
if (i > 0)
@ -155,5 +153,10 @@ public class ArrayUtil
}
return array;
}
private ArrayUtil()
{
// prevents instantiation
}
}

View File

@ -208,7 +208,6 @@ public class Blocker
/**
* A shared reusable Blocking source.
* TODO Review need for this, as it is currently unused.
*/
public static class Shared
{

View File

@ -132,16 +132,19 @@ public class DateCache
else
_tzFormatString = _formatString;
_zoneId = tz.toZoneId();
if (_locale != null)
{
_tzFormat = DateTimeFormatter.ofPattern(_tzFormatString, _locale);
_tzFormat = DateTimeFormatter
.ofPattern(_tzFormatString, _locale)
.withZone(_zoneId);
}
else
{
_tzFormat = DateTimeFormatter.ofPattern(_tzFormatString);
_tzFormat = DateTimeFormatter
.ofPattern(_tzFormatString)
.withZone(_zoneId);
}
_zoneId = tz.toZoneId();
_tzFormat.withZone(_zoneId);
_tick = null;
}

View File

@ -536,6 +536,11 @@ public class IO
return null;
}
private IO()
{
// prevent instantiation
}
}

View File

@ -400,6 +400,7 @@ public abstract class IteratingCallback implements Callback
break;
case PENDING:
{
_state = State.FAILED;
failure = true;
break;
}

View File

@ -1254,6 +1254,11 @@ public class StringUtil
.collect(StringBuilder::new, (b, c) -> b.append((char)c), StringBuilder::append)
.toString();
}
private StringUtil()
{
// prevent instantiation
}
}

View File

@ -25,6 +25,7 @@ import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
@ -406,7 +407,7 @@ public class TypeUtil
digit = 10 + c - 'a';
}
if (digit < 0 || digit >= base)
throw new NumberFormatException(new String(b, offset, length));
throw new NumberFormatException(new String(b, offset, length, StandardCharsets.US_ASCII));
value = value * base + digit;
}
return value;
@ -671,14 +672,11 @@ public class TypeUtil
try
{
String resourceName = TypeUtil.toClassReference(clazz);
if (loader != null)
URL url = loader.getResource(resourceName);
if (url != null)
{
URL url = loader.getResource(resourceName);
if (url != null)
{
URI uri = url.toURI();
return URIUtil.unwrapContainer(uri);
}
URI uri = url.toURI();
return URIUtil.unwrapContainer(uri);
}
}
catch (URISyntaxException ignored)
@ -819,4 +817,9 @@ public class TypeUtil
{
return StreamSupport.stream(new ServiceLoaderSpliterator<>(serviceLoader), false);
}
private TypeUtil()
{
// prevents instantiation
}
}

View File

@ -60,7 +60,6 @@ public class UrlEncoded
charset = System.getProperty("org.eclipse.jetty.util.UrlEncoding.charset");
if (charset == null)
{
charset = StandardCharsets.UTF_8.toString();
encoding = StandardCharsets.UTF_8;
}
else

View File

@ -19,11 +19,13 @@ import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Path;
import java.time.Instant;
import org.eclipse.jetty.util.FileID;
import org.eclipse.jetty.util.URIUtil;
/**
* {@link ResourceFactory} for {@link java.net.URL} based resources.
@ -109,7 +111,7 @@ public class URLResourceFactory implements ResourceFactory
@Override
public boolean isDirectory()
{
return uri.getPath().endsWith("/");
return uri.getSchemeSpecificPart().endsWith("/");
}
@Override
@ -121,7 +123,7 @@ public class URLResourceFactory implements ResourceFactory
@Override
public URI getURI()
{
return uri;
return URIUtil.correctFileURI(uri);
}
@Override
@ -139,7 +141,7 @@ public class URLResourceFactory implements ResourceFactory
@Override
public Resource resolve(String subUriPath)
{
URI newURI = uri.resolve(subUriPath);
URI newURI = resolve(uri, subUriPath);
try
{
return new URLResource(newURI, this.connectTimeout, this.useCaches);
@ -150,6 +152,25 @@ public class URLResourceFactory implements ResourceFactory
}
}
// This could probably live in URIUtil, but it's awefully specific to URLResourceFactory.
private static URI resolve(URI parent, String path)
{
if (parent.isOpaque() && parent.getPath() == null)
{
URI resolved = resolve(URI.create(parent.getRawSchemeSpecificPart()), path);
return URI.create(parent.getScheme() + ":" + resolved.toASCIIString());
}
else if (parent.getPath() != null)
{
return parent.resolve(path);
}
else
{
// Not possible to use URLs that without a path in Jetty.
throw new RuntimeException("URL without path not supported by Jetty: " + parent);
}
}
@Override
public boolean exists()
{
@ -200,10 +221,9 @@ public class URLResourceFactory implements ResourceFactory
}
@Override
public ReadableByteChannel newReadableByteChannel()
public ReadableByteChannel newReadableByteChannel() throws IOException
{
// not really possible with the URL interface
return null;
return Channels.newChannel(newInputStream());
}
@Override

Some files were not shown because too many files have changed in this diff Show More