diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc index 6df521a9340..e7742c5a0a5 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc @@ -224,6 +224,27 @@ Server └── ContextHandler N ---- +[[pg-server-http-handler-use-sizelimit]] +====== SizeLimitHandler + +`SizeLimitHandler` tracks the sizes of request content and response content, and fails the request processing with an HTTP status code of link:https://www.rfc-editor.org/rfc/rfc9110.html#name-413-content-too-large[`413 Content Too Large`]. + +Server applications can set up the `SizeLimitHandler` before or after handlers that modify the request content or response content such as xref:pg-server-http-handler-use-gzip[`GzipHandler`]. +When `SizeLimitHandler` is before `GzipHandler` in the `Handler` tree, it will limit the compressed content; when it is after, it will limit the uncompressed content. + +The `Handler` tree structure look like the following, to limit uncompressed content: + +[source,screen] +---- +Server +└── GzipHandler + └── SizeLimitHandler + └── ContextHandlerCollection + ├── ContextHandler 1 + :── ... + └── ContextHandler N +---- + [[pg-server-http-handler-use-statistics]] ====== StatisticsHandler diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SizeLimitHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SizeLimitHandler.java index 862f1459015..68728f4af0c 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SizeLimitHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SizeLimitHandler.java @@ -21,27 +21,24 @@ import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; import org.eclipse.jetty.util.Callback; /** - * A handler that can limit the size of message bodies in requests and responses. - * + *
A {@link Handler} that can limit the size of message bodies in requests and responses.
*The optional request and response limits are imposed by checking the {@code Content-Length} - * header or observing the actual bytes seen by the handler. Handler order is important, in as much - * as if this handler is before a the {@link org.eclipse.jetty.server.handler.gzip.GzipHandler}, - * then it will limit compressed sized, if it as after the {@link - * org.eclipse.jetty.server.handler.gzip.GzipHandler} then the limit is applied to uncompressed - * bytes. If a size limit is exceeded then {@link BadMessageException} is thrown with a {@link - * org.eclipse.jetty.http.HttpStatus#PAYLOAD_TOO_LARGE_413} status. + * header or observing the actual bytes seen by this Handler.
+ *Handler order is important; for example, if this handler is before the {@link GzipHandler}, + * then it will limit compressed sizes, if it as after the {@link GzipHandler} then it will limit + * uncompressed sizes.
+ *If a size limit is exceeded then {@link BadMessageException} is thrown with a + * {@link HttpStatus#PAYLOAD_TOO_LARGE_413} status.
*/ public class SizeLimitHandler extends Handler.Wrapper { private final long _requestLimit; private final long _responseLimit; - private long _read = 0; - private long _written = 0; /** * @param requestLimit The request body size limit in bytes or -1 for no limit @@ -68,76 +65,92 @@ public class SizeLimitHandler extends Handler.Wrapper } } - HttpFields.Mutable.Wrapper httpFields = new HttpFields.Mutable.Wrapper(response.getHeaders()) + SizeLimitRequestWrapper wrappedRequest = new SizeLimitRequestWrapper(request); + SizeLimitResponseWrapper wrappedResponse = new SizeLimitResponseWrapper(wrappedRequest, response); + return super.handle(wrappedRequest, wrappedResponse, callback); + } + + private class SizeLimitRequestWrapper extends Request.Wrapper + { + private long _read = 0; + + public SizeLimitRequestWrapper(Request wrapped) { - @Override - public HttpField onAddField(HttpField field) - { - if (field.getHeader().is(HttpHeader.CONTENT_LENGTH.asString())) - { - long contentLength = field.getLongValue(); - if (_responseLimit >= 0 && contentLength > _responseLimit) - throw new HttpException.RuntimeException(HttpStatus.INTERNAL_SERVER_ERROR_500, "Response body is too large: " + contentLength + ">" + _responseLimit); - } - return super.onAddField(field); - } - }; + super(wrapped); + } - response = new Response.Wrapper(request, response) + @Override + public Content.Chunk read() { - @Override - public HttpFields.Mutable getHeaders() - { - return httpFields; - } - }; - - request.addHttpStreamWrapper(httpStream -> new HttpStream.Wrapper(httpStream) - { - @Override - public Content.Chunk read() - { - Content.Chunk chunk = super.read(); - if (chunk == null) - return null; - if (chunk.getFailure() != null) - return chunk; - - // Check request content limit. - ByteBuffer content = chunk.getByteBuffer(); - if (content != null && content.remaining() > 0) - { - _read += content.remaining(); - if (_requestLimit >= 0 && _read > _requestLimit) - { - BadMessageException e = new BadMessageException(HttpStatus.PAYLOAD_TOO_LARGE_413, "Request body is too large: " + _read + ">" + _requestLimit); - request.fail(e); - return null; - } - } - + Content.Chunk chunk = super.read(); + if (chunk == null) + return null; + if (chunk.getFailure() != null) return chunk; - } - @Override - public void send(MetaData.Request request, MetaData.Response response, boolean last, ByteBuffer content, Callback callback) + // Check request content limit. + ByteBuffer content = chunk.getByteBuffer(); + if (content != null && content.remaining() > 0) { - // Check response content limit. - if (content != null && content.remaining() > 0) + _read += content.remaining(); + if (_requestLimit >= 0 && _read > _requestLimit) { - if (_responseLimit >= 0 && (_written + content.remaining()) > _responseLimit) - { - callback.failed(new HttpException.RuntimeException(HttpStatus.INTERNAL_SERVER_ERROR_500, "Response body is too large: " + - _written + content.remaining() + ">" + _responseLimit)); - return; - } - _written += content.remaining(); + BadMessageException e = new BadMessageException(HttpStatus.PAYLOAD_TOO_LARGE_413, "Request body is too large: " + _read + ">" + _requestLimit); + getWrapped().fail(e); + return null; } - - super.send(request, response, last, content, callback); } - }); - return super.handle(request, response, callback); + return chunk; + } + } + + private class SizeLimitResponseWrapper extends Response.Wrapper + { + private final HttpFields.Mutable _httpFields; + private long _written = 0; + + public SizeLimitResponseWrapper(Request request, Response wrapped) + { + super(request, wrapped); + + _httpFields = new HttpFields.Mutable.Wrapper(wrapped.getHeaders()) + { + @Override + public HttpField onAddField(HttpField field) + { + if (field.getHeader().is(HttpHeader.CONTENT_LENGTH.asString())) + { + long contentLength = field.getLongValue(); + if (_responseLimit >= 0 && contentLength > _responseLimit) + throw new HttpException.RuntimeException(HttpStatus.INTERNAL_SERVER_ERROR_500, "Response body is too large: " + contentLength + ">" + _responseLimit); + } + return super.onAddField(field); + } + }; + } + + @Override + public HttpFields.Mutable getHeaders() + { + return _httpFields; + } + + @Override + public void write(boolean last, ByteBuffer content, Callback callback) + { + if (content != null && content.remaining() > 0) + { + if (_responseLimit >= 0 && (_written + content.remaining()) > _responseLimit) + { + callback.failed(new HttpException.RuntimeException(HttpStatus.INTERNAL_SERVER_ERROR_500, "Response body is too large: " + + _written + content.remaining() + ">" + _responseLimit)); + return; + } + _written += content.remaining(); + } + + super.write(last, content, callback); + } } } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SizeLimitHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SizeLimitHandlerTest.java index e226a949e71..d54af550ed2 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SizeLimitHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SizeLimitHandlerTest.java @@ -267,4 +267,29 @@ public class SizeLimitHandlerTest assertThat(response.getContent(), containsString(">8192")); } } + + @Test + public void testMultipleRequests() throws Exception + { + String message = "x".repeat(1024); + _contextHandler.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + response.write(true, BufferUtil.toBuffer(message), callback); + return true; + } + }); + + _server.start(); + + for (int i = 0; i < 1000; i++) + { + HttpTester.Response response = HttpTester.parseResponse( + _local.getResponse("GET /ctx/hello HTTP/1.0\r\n\r\n")); + assertThat(response.getStatus(), equalTo(200)); + assertThat(response.getContent(), equalTo(message)); + } + } } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SizeLimitHandlerServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SizeLimitHandlerServletTest.java index 07d8ccb7dcf..19f981f5ec2 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SizeLimitHandlerServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SizeLimitHandlerServletTest.java @@ -15,12 +15,10 @@ package org.eclipse.jetty.ee10.servlet; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.EOFException; import java.io.IOException; import java.net.URI; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -31,6 +29,7 @@ import org.eclipse.jetty.client.BytesRequestContent; import org.eclipse.jetty.client.ContentResponse; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.client.Result; import org.eclipse.jetty.client.StringRequestContent; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; @@ -38,16 +37,18 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SizeLimitHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.IO; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.lessThan; +import static org.junit.jupiter.api.Assertions.assertNotNull; public class SizeLimitHandlerServletTest { @@ -137,21 +138,23 @@ public class SizeLimitHandlerServletTest String content = "x".repeat(SIZE_LIMIT * 2); URI uri = URI.create("http://localhost:" + _connector.getLocalPort()); - AtomicInteger contentReceived = new AtomicInteger(); - CompletableFuture