From 01518aab3e66d5e13100eb6266119d4876e1884a Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 29 Aug 2023 19:28:54 +0200 Subject: [PATCH] Fixes #10293 - Improve documentation on how to write a response body in Jetty 12. Updated documentation about: * Content.Source * Content.Sink * Handler * Request/Response Updated few APIs to make easier to write applications. Signed-off-by: Simone Bordet --- .../asciidoc/programming-guide/arch-io.adoc | 131 +++++ .../client/http/client-http-api.adoc | 2 + .../migration/migration-11-to-12.adoc | 53 +- .../server/http/server-http-connector.adoc | 26 +- .../http/server-http-handler-implement.adoc | 198 ++++++- .../server/http/server-http-handler-use.adoc | 26 +- .../server/http/server-http-handler.adoc | 11 +- .../server/http/server-http.adoc | 23 +- .../programming-guide/server/server.adoc | 4 +- .../jetty/docs/programming/ContentDocs.java | 454 ++++++++++------ .../server/http/HTTPServerDocs.java | 494 ++++++++++++++---- .../client/EarlyHintsProtocolHandler.java | 2 +- .../org/eclipse/jetty/http/HttpGenerator.java | 2 +- .../org/eclipse/jetty/http/HttpStatus.java | 6 +- .../jetty/http3/client/HTTP3StreamClient.java | 2 +- .../java/org/eclipse/jetty/io/Content.java | 20 +- .../org/eclipse/jetty/proxy/ProxyHandler.java | 2 +- .../jetty/proxy/InterimResponseProxyTest.java | 2 +- .../org/eclipse/jetty/server/Request.java | 10 +- .../org/eclipse/jetty/server/Response.java | 2 +- .../transport/HttpInterimResponseTest.java | 4 +- .../java/org/eclipse/jetty/util/Callback.java | 25 +- .../eclipse/jetty/websocket/api/Callback.java | 2 +- .../ee10/servlet/ServletApiResponse.java | 2 +- .../transport/InformationalResponseTest.java | 4 +- .../eclipse/jetty/ee9/nested/HttpChannel.java | 2 +- .../eclipse/jetty/ee9/nested/Response.java | 2 +- .../transport/InformationalResponseTest.java | 4 +- 28 files changed, 1140 insertions(+), 375 deletions(-) diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/arch-io.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/arch-io.adoc index b142faa738ac..f83b8b666b39 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/arch-io.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/arch-io.adoc @@ -200,3 +200,134 @@ Asynchronous programming is hard. Rely on the Jetty classes to implement `Connection` to avoid mistakes that will be difficult to diagnose and reproduce. ==== + +[[pg-arch-io-content-source]] +==== `Content.Source` + +The high-level abstraction that Jetty offers to read bytes is `org.eclipse.jetty.io.Content.Source`. + +`Content.Source` offers a non-blocking demand/read model where a read returns a `Content.Chunk` (see also xref:pg-arch-io-content-source-chunk[this section]). + +A `Content.Chunk` groups the following information: + +* A `ByteBuffer` with the bytes that have been read; it may be empty. +* Whether the read reached end-of-file. +* A failure that might have happened during the read. + +The ``Content.Chunk``'s `ByteBuffer` is typically a slice of a different `ByteBuffer` that has been read by a lower layer. +There may be multiple layers between the bottom layer (where the initial read typically happens) and the application layer. + +By slicing the `ByteBuffer` (rather than copying its bytes), there is no copy of the bytes between the layers. +However, this comes with the cost that the `ByteBuffer`, and the associated `Content.Chunk`, have an intrinsic lifecycle: the final consumer of a `Content.Chunk` at the application layer must indicate when it has consumed the chunk, so that the bottom layer may reuse/recycle the `ByteBuffer`. + +Consuming the chunk means that the bytes in the `ByteBuffer` are read (or ignored), and that the application will not look at or reference that `ByteBuffer` ever again. + +`Content.Chunk` offers a retain/release model to deal with the `ByteBuffer` lifecycle, with a simple rule: + +IMPORTANT: A `Content.Chunk` returned by a call to `Content.Source.read()` **must** be released. + +The example below is the idiomatic way to read from a `Content.Source`: + +[source,java,indent=0] +---- +include::{doc_code}/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=idiomatic] +---- +<1> The `read()` that must be paired with a `release()`. +<2> The `release()` that pairs the `read()`. + +Note how the reads happens in a loop, consuming the `Content.Source` as soon as it has content available to be read, and therefore no backpressure is applied to the reads. + +An alternative way to read from a `Content.Source`, to use when the chunk is consumed asynchronously, and you don't want to read again until the `Content.Chunk` is consumed, is the following: + +[source,java,indent=0] +---- +include::{doc_code}/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=async] +---- +<1> The `read()` that must be paired with a `release()`. +<2> The `release()` that pairs the `read()`. + +Note how the reads do not happen in a loop, and therefore backpressure is applied to the reads, because there is not a next read until the chunk from the previous read has been consumed (and this may take time). + +You can use `Content.Source` static methods to conveniently read (in a blocking way or non-blocking way), for example via `static Content.Source.asStringAsync(Content.Source, Charset)`, or via an `InputStream` using `static Content.Source.asInputStream(Content.Source source)`. + +For + +Refer to the `Content.Source` link:{javadoc-url}/org/eclipse/jetty/io/Content.Source.html[`javadocs`] for further details. + +[[pg-arch-io-content-source-chunk]] +===== `Content.Chunk` + +`Content.Chunk` offers a retain/release API to control the lifecycle of its `ByteBuffer`. + +When ``Content.Chunk``s are consumed synchronously, no additional retain/release API call is necessary, for example: + +[source,java,indent=0] +---- +include::{doc_code}/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=chunkSync] +---- + +On the other hand, if the `Content.Chunk` is not consumed immediately, then it must be retained, and you must arrange for the `Content.Chunk` to be released at a later time, thus pairing the retain. +For example, you may accumulate the ``Content.Chunk``s in a `List` to convert them to a `String` when all the ``Content.Chunk``s have been read. + +Since reading from a `Content.Source` is asynchronous, the `String` result is produced via a `CompletableFuture`: + +[source,java,indent=0] +---- +include::{doc_code}/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=chunkAsync] +---- +<1> The `read()` that must be paired with a `release()`. +<2> The `release()` that pairs the `read()`. +<3> The `retain()` that must be paired with a `release()`. +<4> The `release()` that pairs the `retain()`. + +Note how method `consume(Content.Chunk)` retains the `Content.Chunk` because it does not consume it, but rather stores it away for later use. +With this additional retain, the retain count is now `2`: one implicitly from the `read()` that returned the `Content.Chunk`, and one explicit in `consume(Content.Chunk)`. + +However, just after returning from `consume(Content.Chunk)` the `Content.Chunk` is released (pairing the implicit retain from `read()`), so that the retain count goes to `1`, and an additional release is still necessary. + +Method `getResult()` arranges to release all the ``Content.Chunk``s that have been accumulated, pairing the retains done in `consume(Content.Chunk)`, so that the retain count for the ``Content.Chunk``s goes finally to `0`. + +[[pg-arch-io-content-sink]] +==== `Content.Sink` + +The high-level abstraction that Jetty offers to write bytes is `org.eclipse.jetty.io.Content.Sink`. + +The primary method to use is `Content.Sink.write(boolean, ByteBuffer, Callback)`, which performs a non-blocking write of the given `ByteBuffer`, with the indication of whether the write is the last. + +The `Callback` parameter is completed, successfully or with a failure, and possibly asynchronously by a different thread, when the write is complete. + +Your application can typically perform zero or more non-last writes, and one final last write. + +However, because the writes may be asynchronous, you cannot start a next write before the previous write is completed. + +This code is wrong: + +[source,java,indent=0] +---- +include::{doc_code}/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=sinkWrong] +---- + +You must initiate a second write only when the first is finished, for example: + +[source,java,indent=0] +---- +include::{doc_code}/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=sinkMany] +---- + +When you need to perform an unknown number of writes, you must use an `IteratingCallback`, explained in xref:pg-arch-io-echo[this section], to avoid ``StackOverFlowError``s. + +For example, to copy from a `Content.Source` to a `Content.Sink` you should use the convenience method `Content.copy(Content.Source, Content.Sink, Callback)`. +For illustrative purposes, below you can find the implementation of `copy(Content.Source, Content.Sink, Callback)` that uses an `IteratingCallback`: + +[source,java,indent=0] +---- +include::{doc_code}/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=copy] +---- + +Non-blocking writes can be easily turned in blocking writes. +This leads to perhaps code that is simpler to read, but that also comes with a price: greater resource usage that may lead to less scalability and less performance. + +[source,java,indent=0] +---- +include::{doc_code}/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=blocking] +---- diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-api.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-api.adoc index 0d776b7e0728..8843893c7d9d 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-api.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-api.adoc @@ -224,6 +224,8 @@ The listener that follows this model is `Response.ContentSourceListener`. After the response headers have been processed by the `HttpClient` implementation, `Response.ContentSourceListener.onContentSource(response, contentSource)` is invoked once and only once. This allows the application to control precisely the read/demand loop: when to read a chunk, how to process it and when to demand the next one. +// TODO: move this section to the IO arch docs. + You must provide a `ContentSourceListener` whose implementation reads a `Content.Chunk` from the provided `Content.Source`, as follows: Then the following cases may happen: diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/migration/migration-11-to-12.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/migration/migration-11-to-12.adoc index 32972a382ad2..2ced8b35846e 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/migration/migration-11-to-12.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/migration/migration-11-to-12.adoc @@ -157,55 +157,4 @@ The old model was a "demand+push" model: the application was demanding content; The new model is a "demand+pull" model: when the content is available, the implementation calls once `Response.ContentSourceListener.onContentSource(Content.Source)`; the application can then pull the content chunks from the `Content.Source`. -Old usage: - -[source, java] ----- -request.onResponseContentDemanded(new Response.DemandedContentListener() -{ - @Override - public void onBeforeContent(Response response, LongConsumer demand) - { - // Demand for first content. - demand.accept(1); - } - - @Override - public void onContent(Response response, LongConsumer demand, ByteBuffer content, Callback callback) - { - // Consume the content. - callback.succeeded(); - - // Demand for more content. - demand.accept(1); - } -}); ----- - -New usage: - -[source, java] ----- -request.onResponseContentSource((response, source) -> -{ - read(source); -} - -private void read(Content.Source source) -{ - while (true) - { - // Pull the content from the source. - Content.Chunk chunk = source.read(); - - if (chunk == null) - { - source.demand(() -> read(source)); - return; - } - - // Consume the content. - chunk.release(); - } -}); ----- +For more information about the new model, see xref:pg-arch-io-content-source[this section]. diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-connector.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-connector.adoc index 6e71f8852c84..cb9563b1f1d8 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-connector.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-connector.adoc @@ -20,8 +20,10 @@ The available implementations are: * `org.eclipse.jetty.server.ServerConnector`, for TCP/IP sockets. * `org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector` for Unix-Domain sockets (requires Java 16 or later). +* `org.eclipse.jetty.quic.server.QuicServerConnector`, for the low-level QUIC protocol. +* `org.eclipse.jetty.http3.server.HTTP3ServerConnector` for the HTTP/3 protocol. -Both use a `java.nio.channels.ServerSocketChannel` to listen to a socket address and to accept socket connections. +The first two use a `java.nio.channels.ServerSocketChannel` to listen to a socket address and to accept socket connections, while last two use a `java.nio.channels.DatagramChannel`. Since `ServerConnector` wraps a `ServerSocketChannel`, it can be configured in a similar way, for example the IP port to listen to, the IP address to bind to, etc.: @@ -30,7 +32,7 @@ Since `ServerConnector` wraps a `ServerSocketChannel`, it can be configured in a include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=configureConnector] ---- -Likewise, `UnixDomainServerConnector` also wraps a `ServerSocketChannel` and can be configured with the Unix-Domain path to listen to: +`UnixDomainServerConnector` also wraps a `ServerSocketChannel` and can be configured with the Unix-Domain path to listen to: [source,java,indent=0] ---- @@ -42,6 +44,13 @@ include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPSer You can use Unix-Domain sockets support only when you run your server with Java 16 or later. ==== +`QuicServerConnector` and its extension `HTTP3ServerConnector` wrap a `DatagramChannel` and can be configured in a similar way: + +[source,java,indent=0] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=configureConnectorQuic] +---- + The _acceptors_ are threads (typically only one) that compete to accept socket connections. When a connection is accepted, `ServerConnector` wraps the accepted `SocketChannel` and passes it to the xref:pg-arch-io-selector-manager[`SelectorManager`]. Therefore, there is a little moment where the acceptor thread is not accepting new connections because it is busy wrapping the just accepted connection to pass it to the `SelectorManager`. @@ -53,12 +62,17 @@ The _selectors_ are components that manage a set of connected sockets, implement Each selector requires one thread and uses the Java NIO mechanism to efficiently handle a set of connected sockets. As a rule of thumb, a single selector can easily manage up to 1000-5000 sockets, although the number may vary greatly depending on the application. -For example, web site applications tend to use sockets for one or more HTTP requests to retrieve resources and then the socket is idle for most of the time. +For example, web applications for websites tend to use sockets for one or more HTTP requests to retrieve resources and then the socket is idle for most of the time. In this case a single selector may be able to manage many sockets because chances are that they will be idle most of the time. -On the contrary, web messaging applications tend to send many small messages at a very high frequency so that sockets are rarely idle. -In this case a single selector may be able to manage less sockets because chances are that many of them will be active at the same time. +On the contrary, web messaging applications or REST applications tend to send many small messages at a very high frequency so that sockets are rarely idle. +In this case a single selector may be able to manage less sockets because chances are that many of them will be active at the same time, so you may need more than one. + +It is possible to configure more than one `Connector` per `Server`. +Typical cases are a `ServerConnector` for clear-text HTTP, and another `ServerConnector` for secure HTTP. +Another case could be a publicly exposed `ServerConnector` for secure HTTP, and an internally exposed `UnixDomainServerConnector` for clear-text HTTP. +Yet another example could be a `ServerConnector` for clear-text HTTP, a `ServerConnector` for secure HTTP/2, and an `HTTP3ServerConnector` for QUIC+HTTP/3. -It is possible to configure more than one `ServerConnector` (each listening on a different port), or more than one `UnixDomainServerConnector` (each listening on a different path), or ``ServerConnector``s and ``UnixDomainServerConnector``s, for example: +For example: [source,java,indent=0] ---- diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-implement.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-implement.adoc index 85ef555db56f..a7113bd4c23c 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-implement.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-implement.adoc @@ -27,7 +27,7 @@ The code that implements the `handle(\...)` method must respect the following co * Returning `false` means that the implementation will not handle the request, and it **must not** complete the `callback` parameter, nor read the request content, nor write response content. * Returning `true` means that the implementation will handle the request, and it **must** eventually complete the `callback` parameter. The completion of the `callback` parameter may happen synchronously within the invocation to `handle(\...)`, or at a later time, asynchronously, possibly from another thread. -If the response has not been explicitly written when the `callback` has been completed, the implementation will write a `200` response with no content if the `callback` has been succeeded, or an error response if the `callback` has been failed. +If the response has not been explicitly written when the `callback` has been completed, the Jetty implementation will write a `200` response with no content if the `callback` has been succeeded, or an error response if the `callback` has been failed. [CAUTION] ==== @@ -38,10 +38,10 @@ For example, returning `true` from `handle(\...)`, but not completing the `callb Similarly, returning `false` from `handle(\...)` but then either writing the response or completing the `callback` parameter will likely result in a garbled response be sent to the client, as the implementation will either invoke another `Handler` (that may write a response) or write a default response. ==== -Applications may wrap the request or response (or both) and forward the wrapped request or response to a child `Handler`. +Applications may wrap the request, the response, or the callback and forward the wrapped request, response and callback to a child `Handler`. [[pg-server-http-handler-impl-hello]] -====== Hello World Handler +====== Hello World `Handler` A simple "Hello World" `Handler` is the following: @@ -50,13 +50,20 @@ A simple "Hello World" `Handler` is the following: include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=handlerHello] ---- -Such a simple `Handler` extends from `Handler.Abstract` and can access the request and response main features, such as reading request headers and content, or writing response headers and content. +Such a simple `Handler` can access the request and response main features, such as xref:pg-server-http-handler-impl-request[reading request headers and content], or xref:pg-server-http-handler-impl-response[writing response headers and content]. -Note how the `callback` parameter is passed to `Content.Sink.write(\...)` (a utility method that eventually calls `Response.write(\...)`), so that when the write completes, also the `callback` parameter is completed. +Note how `HelloWorldHandler` extends from `Handler.Abstract.NonBlocking`. +This is a declaration that `HelloWorldHandler` does not use blocking APIs (of any kind) to perform its logic, allowing Jetty to apply optimizations that are not applied to ``Handler``s that declare themselves as blocking. + +If your `Handler` implementation uses blocking APIs (of any kind), extend from `Handler.Abstract`. + +Note how the `callback` parameter is passed to `Content.Sink.write(\...)` -- a utility method that eventually calls `Response.write(\...)`, so that when the write completes, also the `callback` parameter is completed. Note also that because the `callback` parameter will eventually be completed, the value returned from `handle(\...)` is `true`. +In this way the xref:pg-server-http-handler-impl[`Handler` contract] is fully respected: when `true` is returned, the `callback` will eventually be completed. + [[pg-server-http-handler-impl-filter]] -====== Filtering Handler +====== Filtering `Handler` A filtering `Handler` is a handler that perform some modification to the request or response, and then either forwards the request to another `Handler` or produces an error response: @@ -67,3 +74,182 @@ include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPSer Note how a filtering `Handler` extends from `Handler.Wrapper` and as such needs another handler to forward the request processing to, and how the two ``Handler``s needs to be linked together to work properly. +[[pg-server-http-handler-impl-request]] +====== Using the `Request` + +The `Request` object can be accessed by web applications to inspect the HTTP request URI, the HTTP request headers and read the HTTP request content. + +Since the `Request` object may be wrapped by xref:pg-server-http-handler-impl-filter[filtering ``Handler``s], the design decision for the `Request` APIs was to keep the number of virtual methods at a minimum. +This minimizes the effort necessary to write `Request` wrapper implementations and provides a single source for the data carried by `Request` objects. + +To use the `Request` APIs, you should look up the relevant methods in the following order: + +1. `Request` virtual methods. +For example, `Request.getMethod()` returns the HTTP method used in the request, such as `GET`, `POST`, etc. +2. `Request` `static` methods. +These are utility methods that provide more convenient access to request features. +For example, the HTTP URI query is a string and can be directly accessed via the non-``static`` method `request.getHttpURI().getQuery()`; however, the query string typically holds key/value parameters and applications should not have the burden to parse the query string, so the `static Request.extractQueryParameters(Request)` method is provided. +3. Super class `static` methods. +Since `Request` _is-a_ `Content.Source`, look also for `static` methods in `Content.Source` that take a `Content.Source` as a parameter, so that you can pass the `Request` object as a parameter. + +Below you can find a list of the most common `Request` features and how to access them. +Refer to the `Request` link:{javadoc-url}/org/eclipse/jetty/server/Request.html[javadocs] for the complete list. + +`Request` URI:: +The `Request` URI is accessed via `Request.getHttpURI()` and the link:{javadoc-url}/org/eclipse/jetty/http/HttpURI.html[`HttpURI`] APIs. + +`Request` HTTP headers:: +The `Request` HTTP headers are accessed via `Request.getHeaders()` and the link:{javadoc-url}/org/eclipse/jetty/http/HttpFields.html[`HttpFields`] APIs. + +`Request` cookies:: +The `Request` cookies are accessed via `static Request.getCookies(Request)` and the link:{javadoc-url}/org/eclipse/jetty/http/HttpCookie.html[`HttpCookie`] APIs. + +`Request` parameters:: +The `Request` parameters are accessed via `static Request.extractQueryParameters(Request)` for those present in the HTTP URI query, and via `static Request.getParametersAsync(Request)` for both query parameters and request content parameters received via form upload with `Content-Type: application/x-www-url-form-encoded`, and the link:{javadoc-url}/org/eclipse/jetty/util/Fields.html[`Fields`] APIs. +If you are only interested in the request content parameters received via form upload, you can use `static FormFields.from(Request)`, see also xref:pg-server-http-handler-impl-request-content[this section]. + +`Request` HTTP session:: +The `Request` HTTP session is accessed via `Request.getSession(boolean)` and the link:{javadoc-url}/org/eclipse/jetty/server/Session.html[`Session`] APIs. + +[[pg-server-http-handler-impl-request-content]] +====== Reading the `Request` Content + +Since `Request` _is-a_ `Content.Source`, the xref:pg-arch-io-content-source[section] about reading from `Content.Source` applies to `Request` as well. +The `static Content.Source` utility methods will allow you to read the request content as a string, or as an `InputStream`, for example. + +There are two special cases that are specific to HTTP for the request content: form parameters (sent when submitting an HTML form) and multipart form data (sent when submitting an HTML form with file upload). + +For form parameters, typical of HTML form submissions, you can use the `FormFields` APIs as shown here: + +[source,java,indent=0] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=handlerForm] +---- +<1> If the `Content-Type` is `x-www-form-urlencoded`, read the request content with `FormFields`. +<2> When all the request content has arrived, process the `Fields`. + +[WARNING] +==== +The `Handler` returns `true`, so the `callback` parameter **must** be completed. + +It is therefore mandatory to use `CompletableFuture` APIs that are invoked even when reading the request content failed, such as `whenComplete(BiConsumer)`, `handle(BiFunction)`, `exceptionally(Function)`, etc. + +Failing to do so may result in the `Handler` `callback` parameter to never be completed, causing the request processing to hang forever. +==== + +For multipart form data, typical for HTML form file uploads, you can use the `MultiPartFormData.Parser` APIs as shown here: + +[source,java,indent=0] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=handlerMultiPart] +---- +<1> If the `Content-Type` is `multipart/form-data`, read the request content with `MultiPartFormData.Parser`. +<2> When all the request content has arrived, process the `MultiPartFormData.Parts`. + +[WARNING] +==== +The `Handler` returns `true`, so the `callback` parameter **must** be completed. + +It is therefore mandatory to use `CompletableFuture` APIs that are invoked even when reading the request content failed, such as `whenComplete(BiConsumer)`, `handle(BiFunction)`, `exceptionally(Function)`, etc. + +Failing to do so may result in the `Handler` `callback` parameter to never be completed, causing the request processing to hang forever. +==== + +[[pg-server-http-handler-impl-response]] +====== Using the `Response` + +The `Response` object can be accessed by web applications to set the HTTP response status code, the HTTP response headers and write the HTTP response content. + +The design of the `Response` APIs is similar to that of the `Request` APIs, described in xref:pg-server-http-handler-impl-request[this section]. + +To use the `Response` APIs, you should look up the relevant methods in the following order: + +1. `Response` virtual methods. +For example, `Response.setStatus(int)` to set the HTTP response status code. +2. `Request` `static` methods. +These are utility methods that provide more convenient access to response features. +For example, adding an HTTP cookie could be done by adding a `Set-Cookie` response header, but it would be extremely error-prone. +The utility method `static Response.addCookie(Response, HttpCookie)` is provided instead. +3. Super class `static` methods. +Since `Response` _is-a_ `Content.Sink`, look also for `static` methods in `Content.Sink` that take a `Content.Sink` as a parameter, so that you can pass the `Response` object as a parameter. + +Below you can find a list of the most common `Response` features and how to access them. +Refer to the `Response` link:{javadoc-url}/org/eclipse/jetty/server/Response.html[javadocs] for the complete list. + +`Response` status code:: +The `Response` HTTP status code is accessed via `Response.getStatus()` and `Response.setStatus(int)`. + +`Response` HTTP headers:: +The `Response` HTTP headers are accessed via `Response.getHeaders()` and the link:{javadoc-url}/org/eclipse/jetty/http/HttpFields.Mutable.html[`HttpFields.Mutable`] APIs. +The response headers are mutable until the response is _committed_, as defined in xref:pg-server-http-handler-impl-response-content[this section]. + +`Response` cookies:: +The `Response` cookies are accessed via `static Response.addCookie(Response, HttpCookie)`, `static Response.replaceCookie(Response, HttpCookie)` and the link:{javadoc-url}/org/eclipse/jetty/http/HttpCookie.html[`HttpCookie`] APIs. +Since cookies translate to HTTP headers, they can be added/replaces until the response is _committed_, as defined in xref:pg-server-http-handler-impl-response-content[this section]. + +[[pg-server-http-handler-impl-response-content]] +====== Writing the `Response` Content + +Since `Response` _is-a_ `Content.Sink`, the xref:pg-arch-io-content-sink[section] about writing to `Content.Sink` applies to `Response` as well. +The `static Content.Sink` utility methods will allow you to write the response content as a string, or as an `OutputStream`, for example. + +IMPORTANT: The first call to `Response.write(boolean, ByteBuffer, Callback)` _commits_ the response. + +Committing the response means that the response status code and response headers are sent to the other peer, and therefore cannot be modified anymore. +Trying to modify them may result in an `IllegalStateException` to be thrown, as it is an application mistake to commit the response and then try to modify the headers. + +You can explicitly commit the response by performing an empty, non-last write: + +[source,java,indent=0] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=flush] +---- + +[WARNING] +==== +The `Handler` returns `true`, so the `callback` parameter **must** be completed. + +It is therefore mandatory to use `CompletableFuture` APIs that are invoked even when writing the response content failed, such as `whenComplete(BiConsumer)`, `handle(BiFunction)`, `exceptionally(Function)`, etc. + +Failing to do so may result in the `Handler` `callback` parameter to never be completed, causing the request processing to hang forever. +==== + +Jetty can perform important optimizations for the HTTP/1.1 protocol if the response content length is known before the response is committed: + +[source,java,indent=0] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=contentLength] +---- + +NOTE: Setting the response content length is an _optimization_; Jetty will work well even without it. +If you set the response content length, however, remember that it must specify the number of _bytes_, not the number of characters. + +[[pg-server-http-handler-impl-response-interim]] +====== Sending Interim ``Response``s + +The HTTP protocol (any version) allows applications to write link:https://www.rfc-editor.org/rfc/rfc9110#name-status-codes[interim responses]. + +An interim response has a status code in the `1xx` range (but not `101`), and an application may write zero or more interim response before the final response. + +This is an example of writing an interim `100 Continue` response: + +[source,java,indent=0] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=continue] +---- +<1> Using `Response.writeInterim(\...)` to send the interim response. +<2> The completion of the callback must take into account both success and failure. + +Note how writing an interim response is as asynchronous operation. +As such you must perform subsequent operations using the `CompletableFuture` APIs, and remember to complete the `Handler` `callback` parameter both in case of success or in case of failure. + +This is an example of writing an interim `103 Early Hints` response: + +[source,java,indent=0] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=earlyHints103] +---- +<1> Using `Response.writeInterim(\...)` to send the interim response. +<2> The completion of the callback must take into account both success and failure. + +An interim response may or may not have its own HTTP headers (this depends on the interim response status code), and they are typically different from the final response HTTP headers. 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 36acd0ee0d65..b5f4e839d971 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 @@ -35,11 +35,9 @@ In the first case the contexts were `(domain.com, /shop)` and `(domain.com, /api Server applications using the Jetty Server Libraries create and configure a _context_ for each web application. Many __context__s can be deployed together to enrich the web application offering -- for example a catalog context, a shop context, an API context, an administration context, etc. -Web applications can be written using exclusively the Servlet APIs, since developers know well the Servlet API and because they guarantee better portability across Servlet container implementations. +Web applications can be written using exclusively the Servlet APIs, since developers know well the Servlet API and because they guarantee better portability across Servlet container implementations, as described in xref:pg-server-http-handler-use-servlet[this section]. -Embedded web applications based on the Servlet APIs are described in xref:pg-server-http-handler-use-servlet[this section]. - -Embedded web applications may also require additional features such as access to Jetty specific APIs, or utility features such as redirection from HTTP to HTTPS, support for `gzip` content compression, etc. +On the other hand, web applications can be written using the Jetty APIs, for better performance, or to be able to access to Jetty specific APIs, or to use features such as redirection from HTTP to HTTPS, support for `gzip` content compression, URI rewriting, etc. The Jetty Server Libraries provides a number of out-of-the-box ``Handler``s that implement the most common functionalities and are described in the next sections. [[pg-server-http-handler-use-context]] @@ -333,7 +331,7 @@ Server In the example above, `TotalEventsHandler` may record the total times of request processing, from `SlowHandler` all the way to the `ContextHandler`. On the other hand, `AppEventsHandler` may record both the time it takes for the request to flow from `TotalEventsHandler` to `AppEventsHandler`, therefore effectively measuring the processing time due to `SlowHandler`, and the time it takes to process the request by the `ContextHandler`. -Refere to the `EventsHandler` link:{javadoc-url}/org/eclipse/jetty/server/handler/EventsHandler[javadocs] for further information. +Refer to the `EventsHandler` link:{javadoc-url}/org/eclipse/jetty/server/handler/EventsHandler[javadocs] for further information. [[pg-server-http-handler-use-limit]] ====== QoSHandler @@ -366,7 +364,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 directly set on the server, for example: +`DefaultHandler` is set directly on the server, for example: [source,java,indent=0] ---- @@ -388,7 +386,7 @@ 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. +Jetty will send an HTTP `404` response anyway if `DefaultHandler` has not been set. [[pg-server-http-handler-use-servlet]] ===== Servlet API Handlers @@ -400,6 +398,10 @@ Jetty will send an HTTP `404` response anyway if `DefaultHandler` is not used. `ServletContextHandler` is a `ContextHandler` that provides support for the Servlet APIs and implements the behaviors required by the Servlet specification. +However, differently from xref:pg-server-http-handler-use-webapp-context[`WebAppContext`], it does not require web application to be packaged as a `+*.war+`, nor it requires a `web.xml` for configuration. + +With `ServletContextHandler` you can just put all your Servlet components in a `+*.jar+` and configure each component using the `ServletContextHandler` APIs, in a way equivalent to what you would write in a `web.xml`. + The Maven artifact coordinates depend on the version of Jakarta EE you want to use, and they are: [source,xml,subs=normal] @@ -411,9 +413,9 @@ The Maven artifact coordinates depend on the version of Jakarta EE you want to u ---- -For example, for Jakarta {ee-current-caps} the coordinates are: `org.eclipse.jetty.ee10:jetty-ee10-servlet:{version}`. +For example, for Jakarta {ee-current-caps} the coordinates are: `org.eclipse.jetty.{ee-current}:jetty-{ee-current}-servlet:{version}`. -Below you can find an example of how to setup a Jakarta {ee-current-caps} `ServletContextHandler`: +Below you can find an example of how to set up a Jakarta {ee-current-caps} `ServletContextHandler`: [source,java,indent=0] ---- @@ -436,7 +438,7 @@ Server Note how the Servlet components (they are not ``Handler``s) are represented in _italic_. -Note also how adding a `Servlet` or a `Filter` returns a _holder_ object that can be used to specify additional configuration for that particular `Servlet` or `Filter`. +Note also how adding a `Servlet` or a `Filter` returns a _holder_ object that can be used to specify additional configuration for that particular `Servlet` or `Filter`, for example initialization parameters (equivalent to `` in `web.xml`). When a request arrives to `ServletContextHandler` the request URI will be matched against the ``Filter``s and ``Servlet`` mappings and only those that match will process the request, as dictated by the Servlet specification. @@ -449,7 +451,7 @@ Server applications must be careful when creating the `Handler` tree to put ``Se [[pg-server-http-handler-use-webapp-context]] ====== WebAppContext -`WebAppContext` is a `ServletContextHandler` that auto configures itself by reading a `web.xml` Servlet configuration file. +`WebAppContext` is a `ServletContextHandler` that autoconfigures itself by reading a `web.xml` Servlet configuration file. The Maven artifact coordinates depend on the version of Jakarta EE you want to use, and they are: @@ -464,7 +466,7 @@ The Maven artifact coordinates depend on the version of Jakarta EE you want to u Server applications can specify a `+*.war+` file or a directory with the structure of a `+*.war+` file to `WebAppContext` to deploy a standard Servlet web application packaged as a `war` (as defined by the Servlet specification). -Where server applications using `ServletContextHandler` must manually invoke methods to add ``Servlet``s and ``Filter``s, `WebAppContext` reads `WEB-INF/web.xml` to add ``Servlet``s and ``Filter``s, and also enforces a number of restrictions defined by the Servlet specification, in particular related to class loading. +Where server applications using `ServletContextHandler` must manually invoke methods to add ``Servlet``s and ``Filter``s as described in xref:pg-server-http-handler-use-servlet-context[this section], `WebAppContext` reads `WEB-INF/web.xml` to add ``Servlet``s and ``Filter``s, and also enforces a number of restrictions defined by the Servlet specification, in particular related to class loading. [source,java,indent=0] ---- diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler.adoc index 83acb345dc26..e03b296e5c0f 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler.adoc @@ -19,8 +19,8 @@ An `org.eclipse.jetty.server.Handler` is the component that processes incoming H ``Handler``s can process the HTTP request themselves, or they can be ``Handler.Container``s that delegate HTTP request processing to one or more contained ``Handler``s. This allows ``Handler``s to be organized as a tree comprised of: -* Leaf ``Handler``s that return `true` from the `handle(\...)` method, generate a response and succeed the `Callback`. -* A `Handler.Wrapper` can be used to form a chain of ``Handler``s where request, response or callback objects are wrapped in the `handle(\...)` method before being passed down the chain. +* Leaf ``Handler``s that generate a response, complete the `Callback`, and return `true` from the `handle(\...)` method. +* A `Handler.Wrapper` can be used to form a chain of ``Handler``s where request, response or callback objects may be wrapped in the `handle(\...)` method before being passed down the chain. * A `Handler.Sequence` that contains a sequence of ``Handler``s, with each `Handler` being called in sequence until one returns `true` from its `handle(\...)` method. * A specialized `Handler.Container` that may use properties of the request (for example, the URI, or a header, etc.) to select from one or more contained ``Handler``s to delegate the HTTP request processing to, for example link:{javadoc-url}/org/eclipse/jetty/server/handler/PathMappingsHandler.html[`PathMappingsHandler`]. @@ -42,11 +42,10 @@ Server └── App2Handler ---- -Server applications should rarely write custom ``Handler``s, preferring instead to use existing ``Handler``s provided by the Jetty Server Libraries for managing web application contexts, security, HTTP sessions and Servlet support. -Refer to xref:pg-server-http-handler-use[this section] for more information about how to use the ``Handler``s provided by the Jetty Server Libraries. +You should prefer using existing ``Handler``s provided by the Jetty server libraries for managing web application contexts, security, HTTP sessions and Servlet support. +Refer to xref:pg-server-http-handler-use[this section] for more information about how to use the ``Handler``s provided by the Jetty server libraries. -However, in some cases the additional features are not required, or additional constraints on memory footprint, or performance, or just simplicity must be met. -In these cases, implementing your own `Handler` may be a better solution. +You should write your own leaf ``Handler``s to implement your web application logic. Refer to xref:pg-server-http-handler-impl[this section] for more information about how to write your own ``Handler``s. // TODO: document ScopedHandler? Is this really necessary or just an implementation detail that application will never worry about? diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http.adoc index 9773da7da348..3cdf4159fb59 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http.adoc @@ -61,14 +61,16 @@ A `Server` must be created, configured and started: include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=simple] ---- -The example above shows the simplest HTTP/1.1 server; it has no support for HTTP sessions, for HTTP authentication, or for any of the features required by the Servlet specification. +The example above shows the simplest HTTP/1.1 server; it has no support for HTTP sessions, nor for HTTP authentication, nor for any of the features required by the Servlet specification. -All these features are provided by the Jetty Server Libraries, and server applications only need to put the required components together to provide all the required features. +These features (HTTP session support, HTTP authentication support, etc.) are provided by the Jetty server libraries, but not all of them may be necessary in your web application. +You need to put together the required Jetty components to provide the features required by your web applications. +The advantage is that you do not pay the cost for features that you do not use, saving resources and likely increasing performance. -The ``Handler``s provided by the Jetty Server Libraries allow writing server applications that have functionalities similar to Apache HTTPD or Nginx (for example: URL redirection, URL rewriting, serving static content, reverse proxying, etc.), as well as generating content dynamically by processing incoming requests. +The built-in ``Handler``s provided by the Jetty server libraries allow you to write web applications that have functionalities similar to Apache HTTPD or Nginx (for example: URL redirection, URL rewriting, serving static content, reverse proxying, etc.), as well as generating content dynamically by processing incoming requests. Read xref:pg-server-http-handler[this section] for further details about ``Handler``s. -If you are interested in writing your server application based on the Servlet APIs, jump to xref:pg-server-http-handler-use-servlet[this section]. +If you are interested in writing your web application based on the Servlet APIs, jump to xref:pg-server-http-handler-use-servlet[this section]. [[pg-server-http-request-processing]] ==== Request Processing @@ -112,12 +114,12 @@ This event is converted to a call to `AbstractConnection.onFillable()`, where th The parser emit events that are protocol specific; the HTTP/2 parser, for example, emits events for each HTTP/2 frame that has been parsed, and similarly does the HTTP/3 parser. The parser events are then converted to protocol independent events such as _"request start"_, _"request headers"_, _"request content chunk"_, etc. -When enough of the HTTP request is arrived, the `Connection` calls `HttpChannel.onRequest()` that calls the `Handler` chain starting from the `Server` instance, that eventually calls the server application code. +When enough of the HTTP request is arrived, the `Connection` calls `HttpChannel.onRequest()` that calls the `Handler` chain starting from the `Server` instance, that eventually calls your web application code. [[pg-server-http-request-processing-events]] ===== Request Processing Events -Advanced server applications may be interested in the progress of the processing of an HTTP request/response. +Advanced web applications may be interested in the progress of the processing of an HTTP request/response. A typical case is to know exactly _when_ the HTTP request/response processing starts and when it is complete, for example to monitor processing times. This is conveniently implemented by `org.eclipse.jetty.server.handler.EventsHandler`, described in more details in xref:pg-server-http-handler-use-events[this section]. @@ -137,7 +139,7 @@ Typically, the extended NCSA format is the is enough and it's the standard used To customize the request/response log line format see the link:{javadoc-url}/org/eclipse/jetty/server/CustomRequestLog.html[`CustomRequestLog` javadocs]. ==== -Request logging can be enabled at the server level, or at the web application context level. +Request logging can be enabled at the `Server` level. The request logging output can be directed to an SLF4J logger named `"org.eclipse.jetty.server.RequestLog"` at `INFO` level, and therefore to any logging library implementation of your choice (see also xref:pg-troubleshooting-logging[this section] about logging). @@ -158,13 +160,6 @@ For maximum flexibility, you can log to multiple ``RequestLog``s using class `Re You can use `CustomRequestLog` with a custom `RequestLog.Writer` to direct the request logging output to your custom targets (for example, an RDBMS). You can implement your own `RequestLog` if you want to have functionalities that are not implemented by `CustomRequestLog`. -Request logging can also be enabled at the web application context level, using `RequestLogHandler` (see xref:pg-server-http-handler[this section] about how to organize Jetty ``Handler``s) to wrap a web application `Handler`: - -[source,java,indent=0] ----- -include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=contextRequestLog] ----- - include::server-http-connector.adoc[] include::server-http-handler.adoc[] include::server-http-security.adoc[] diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/server.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/server.adoc index b1d2a8aa6f16..11cbb38b8b43 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/server.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/server.adoc @@ -23,16 +23,16 @@ If you are interested in the low-level details of how the Eclipse Jetty server l The Jetty server-side libraries provide: -* HTTP support for HTTP/1.0, HTTP/1.1, HTTP/2, clear-text or encrypted, HTTP/3, for applications that want to embed Jetty as a generic HTTP server or proxy, via the xref:pg-server-http[HTTP libraries] +* HTTP high-level support for HTTP/1.0, HTTP/1.1, HTTP/2, clear-text or encrypted, HTTP/3, for applications that want to embed Jetty as a generic HTTP server or proxy (no matter the HTTP version), via the xref:pg-server-http[HTTP libraries] * HTTP/2 low-level support, for applications that want to explicitly handle low-level HTTP/2 _sessions_, _streams_ and _frames_, via the xref:pg-server-http2[HTTP/2 libraries] * HTTP/3 low-level support, for applications that want to explicitly handle low-level HTTP/3 _sessions_, _streams_ and _frames_, via the xref:pg-server-http3[HTTP/3 libraries] * WebSocket support, for applications that want to embed a WebSocket server, via the xref:pg-server-websocket[WebSocket libraries] * FCGI support, to delegate requests to PHP, Python, Ruby or similar scripting languages. -include::compliance/server-compliance.adoc[] include::http/server-http.adoc[] include::http2/server-http2.adoc[] include::http3/server-http3.adoc[] +include::compliance/server-compliance.adoc[] include::sessions/sessions.adoc[] include::websocket/server-websocket.adoc[] include::server-io-arch.adoc[] diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java index 3724c4480f84..311025212d53 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java @@ -13,232 +13,368 @@ package org.eclipse.jetty.docs.programming; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.List; 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.CompletableTask; +import org.eclipse.jetty.util.IteratingCallback; import org.eclipse.jetty.util.Utf8StringBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static java.lang.System.Logger.Level.INFO; + @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) + @SuppressWarnings("unused") + class Idiomatic { - Callback echo = new Callback() + // tag::idiomatic[] + public void read(Content.Source source) { - private Content.Chunk chunk; - - public void succeeded() + // Read from the source in a loop. + while (true) { - // release any previous chunk - if (chunk != null) + // Read a chunk, must be eventually released. + Content.Chunk chunk = source.read(); // <1> + + // If no chunk, demand to be called back when there are more chunks. + if (chunk == null) { - chunk.release(); - // complete if it was the last - if (chunk.isLast()) - { - callback.succeeded(); - return; - } + source.demand(() -> read(source)); + return; } - while (true) + // If there is a failure reading, handle it. + if (Content.Chunk.isFailure(chunk)) { - // 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(); + handleFailure(chunk); + return; } + + // A normal chunk of content, consume it. + consume(chunk); + + // Release the chunk. + chunk.release(); // <2> + + // Stop reading if EOF was reached. + if (chunk.isLast()) + return; + + // Loop around to read another chunk. } + } + // end::idiomatic[] + } - public void failed(Throwable x) + @SuppressWarnings("unused") + static class Async + { + // tag::async[] + public void read(Content.Source source) + { + // Read a chunk, must be eventually released. + Content.Chunk chunk = source.read(); // <1> + + // If no chunk, demand to be called back when there are more chunks. + if (chunk == null) { - source.fail(x); - callback.failed(x); + source.demand(() -> read(source)); + return; } - }; - source.demand(echo::succeeded); + + // If there is a failure reading, handle it. + if (Content.Chunk.isFailure(chunk)) + { + handleFailure(chunk); + return; + } + + // Consume the chunk asynchronously, and do not + // read more chunks until this has been consumed. + CompletableFuture consumed = consumeAsync(chunk); + + // Only when the chunk has been consumed + // release it and try to read more. + consumed.whenComplete((result, failure) -> + { + if (failure == null) + { + // Release the chunk. + chunk.release(); // <2> + + // Continue reading if EOF was not reached. + if (!chunk.isLast()) + source.demand(() -> read(source)); + } + else + { + // If there is a failure reading, handle it, + // and stop reading by not demanding. + handleFailure(chunk); + } + }); + } + // end::async[] + + private CompletableFuture consumeAsync(Content.Chunk chunk) + { + return CompletableFuture.completedFuture(null); + } } - // tag::echo[] - public static void testEcho() throws Exception + private static void handleFailure(Content.Chunk chunk) { - 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 + private void consume(Content.Chunk chunk) + { + } + + @SuppressWarnings("unused") + static class ChunkSync + { + private FileChannel fileChannel; + + // tag::chunkSync[] + public void consume(Content.Chunk chunk) throws IOException + { + // Consume the chunk synchronously within this method. + + // For example, parse the bytes into other objects, + // or copy the bytes elsewhere (e.g. the file system). + fileChannel.write(chunk.getByteBuffer()); + + if (chunk.isLast()) + fileChannel.close(); + } + // end::chunkSync[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::chunkAsync[] + // CompletableTask is-a CompletableFuture. + public class ChunksToString extends CompletableTask { - private final CharsetStringBuilder text; + private final List chunks = new ArrayList<>(); private final Content.Source source; - public FutureString(Content.Source source, Charset charset) + public ChunksToString(Content.Source source) { this.source = source; - this.text = CharsetStringBuilder.forCharset(charset); - source.demand(this::onContentAvailable); } - private void onContentAvailable() + @Override + public void run() { while (true) { - Content.Chunk chunk = source.read(); + // Read a chunk, must be eventually released. + Content.Chunk chunk = source.read(); // <1> + if (chunk == null) { - source.demand(this::onContentAvailable); + source.demand(this); return; } - try + if (Content.Chunk.isFailure(chunk)) { - if (Content.Chunk.isFailure(chunk)) - throw chunk.getFailure(); + handleFailure(chunk); + return; + } - if (chunk.hasRemaining()) - text.append(chunk.getByteBuffer()); + // A normal chunk of content, consume it. + consume(chunk); - if (chunk.isLast() && complete(text.build())) - return; - } - catch (Throwable e) - { - completeExceptionally(e); - } - finally + // Release the chunk. + // This pairs the call to read() above. + chunk.release(); // <2> + + if (chunk.isLast()) { - chunk.release(); + // Produce the result. + String result = getResult(); + + // Complete this CompletableFuture with the result. + complete(result); + + // The reading is complete. + return; } } } + + public void consume(Content.Chunk chunk) + { + // The chunk is not consumed within this method, but + // stored away for later use, so it must be retained. + chunk.retain(); // <3> + chunks.add(chunk); + } + + public String getResult() + { + Utf8StringBuilder builder = new Utf8StringBuilder(); + // Iterate over the chunks, copying and releasing. + for (Content.Chunk chunk : chunks) + { + // Copy the chunk bytes into the builder. + builder.append(chunk.getByteBuffer()); + + // The chunk has been consumed, release it. + // This pairs the retain() in consume(). + chunk.release(); // <4> + } + return builder.toCompleteString(); + } } + // end::chunkAsync[] - public static void testFutureString() throws Exception + static class SinkWrong { - 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"); + // tag::sinkWrong[] + public void wrongWrite(Content.Sink sink, ByteBuffer content1, ByteBuffer content2) + { + // Initiate a first write. + sink.write(false, content1, Callback.NOOP); + + // WRONG! Cannot initiate a second write before the first is complete. + sink.write(true, content2, Callback.NOOP); + } + // end::sinkWrong[] } - public static class FutureUtf8String extends ContentSourceCompletableFuture + static class SinkMany { - private final Utf8StringBuilder builder = new Utf8StringBuilder(); + // tag::sinkMany[] + public void manyWrites(Content.Sink sink, ByteBuffer content1, ByteBuffer content2) + { + // Initiate a first write. + Callback.Completable resultOfWrites = Callback.Completable.with(callback1 -> sink.write(false, content1, callback1)) + // Chain a second write only when the first is complete. + .compose(callback2 -> sink.write(true, content2, callback2)); + + // Use the resulting Callback.Completable as you would use a CompletableFuture. + // For example: + resultOfWrites.whenComplete((ignored, failure) -> + { + if (failure == null) + System.getLogger("sink").log(INFO, "writes completed successfully"); + else + System.getLogger("sink").log(INFO, "writes failed", failure); + }); + } + // end::sinkMany[] + } + + // tag::copy[] + @SuppressWarnings("InnerClassMayBeStatic") + class Copy extends IteratingCallback + { + private final Content.Source source; + private final Content.Sink sink; + private final Callback callback; + private Content.Chunk chunk; - public FutureUtf8String(Content.Source content) + public Copy(Content.Source source, Content.Sink sink, Callback callback) { - super(content); + this.source = source; + this.sink = sink; + // The callback to notify when the copy is completed. + this.callback = callback; } @Override - protected String parse(Content.Chunk chunk) throws Throwable + protected Action process() throws Throwable { - if (chunk.hasRemaining()) - builder.append(chunk.getByteBuffer()); - return chunk.isLast() ? builder.takeCompleteString(IllegalStateException::new) : null; + // If the last write completed, succeed this IteratingCallback, + // causing onCompleteSuccess() to be invoked. + if (chunk != null && chunk.isLast()) + return Action.SUCCEEDED; + + // Read a chunk. + chunk = source.read(); + + // No chunk, demand to be called back when there will be more chunks. + if (chunk == null) + { + source.demand(this::iterate); + return Action.IDLE; + } + + // The read failed, re-throw the failure + // causing onCompleteFailure() to be invoked. + if (Content.Chunk.isFailure(chunk)) + throw chunk.getFailure(); + + // Copy the chunk. + sink.write(chunk.isLast(), chunk.getByteBuffer(), this); + return Action.SCHEDULED; + } + + @Override + public void succeeded() + { + // After every successful write, release the chunk. + chunk.release(); + super.succeeded(); + } + + @Override + public void failed(Throwable x) + { + super.failed(x); + } + + @Override + protected void onCompleteSuccess() + { + // The copy is succeeded, succeed the callback. + callback.succeeded(); + } + + @Override + protected void onCompleteFailure(Throwable failure) + { + // In case of a failure, either on the + // read or on the write, release the chunk. + chunk.release(); + + // The copy is failed, fail the callback. + callback.failed(failure); + } + + @Override + public InvocationType getInvocationType() + { + return InvocationType.NON_BLOCKING; } } + // end::copy[] - public static void main(String... args) throws Exception + static class Blocking { - testEcho(); - testFutureString(); + // tag::blocking[] + public void blockingWrite(Content.Sink sink, ByteBuffer content1, ByteBuffer content2) throws IOException + { + // First blocking write, returns only when the write is complete. + Content.Sink.write(sink, false, content1); + + // Second blocking write, returns only when the write is complete. + // It is legal to perform the writes sequentially, since they are blocking. + Content.Sink.write(sink, true, content2); + } + // end::blocking[] } } diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java index 96fca3a75854..59277227c7ed 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java @@ -14,10 +14,12 @@ package org.eclipse.jetty.docs.programming.server.http; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.file.Path; import java.util.EnumSet; import java.util.List; import java.util.TimeZone; +import java.util.concurrent.CompletableFuture; import jakarta.servlet.DispatcherType; import jakarta.servlet.ServletInputStream; @@ -32,11 +34,15 @@ import org.eclipse.jetty.ee10.servlets.CrossOriginFilter; import org.eclipse.jetty.ee10.webapp.WebAppContext; import org.eclipse.jetty.http.HttpCompliance; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.http.MultiPart; +import org.eclipse.jetty.http.MultiPartFormData; import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory; @@ -48,6 +54,7 @@ import org.eclipse.jetty.rewrite.handler.RewriteRegexRule; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.CustomRequestLog; +import org.eclipse.jetty.server.FormFields; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; @@ -70,13 +77,16 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.NanoTime; +import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import static java.lang.System.Logger.Level.INFO; +import static java.nio.charset.StandardCharsets.UTF_8; @SuppressWarnings("unused") public class HTTPServerDocs @@ -103,13 +113,14 @@ public void simple() throws Exception @Override public boolean handle(Request request, Response response, Callback callback) { - // Succeed the callback to write the response. + // Succeed the callback to signal that the + // request/response processing is complete. callback.succeeded(); return true; } }); - // Start the Server so it starts accepting connections from clients. + // Start the Server to start accepting connections from clients. server.start(); // end::simple[] } @@ -141,33 +152,6 @@ public void serverRequestLogFile() // end::serverRequestLogFile[] } - public void contextRequestLog() - { - // tag::contextRequestLog[] - Server server = new Server(); - - // Create a first ServletContextHandler for your main application. - ServletContextHandler mainContext = new ServletContextHandler(); - mainContext.setContextPath("/main"); - - // Create a RequestLogHandler to log requests for your main application. - // TODO: RequestLogHandler may need to be re-introduced, to allow per-context logging? -/* - RequestLogHandler requestLogHandler = new RequestLogHandler(); - requestLogHandler.setRequestLog(new CustomRequestLog()); - // Wrap the main application with the request log handler. - requestLogHandler.setHandler(mainContext); - - // Create a second ServletContextHandler for your other application. - // No request logging for this application. - ServletContextHandler otherContext = new ServletContextHandler(); - mainContext.setContextPath("/other"); - - server.setHandler(new Handler.Collection(requestLogHandler, otherContext)); -*/ - // end::contextRequestLog[] - } - public void configureConnector() throws Exception { // tag::configureConnector[] @@ -224,6 +208,29 @@ public void configureConnectorUnix() throws Exception // end::configureConnectorUnix[] } + public void configureConnectorQuic() throws Exception + { + // tag::configureConnectorQuic[] + Server server = new Server(); + + // Configure the SslContextFactory with the keyStore information. + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("/path/to/keystore"); + sslContextFactory.setKeyStorePassword("secret"); + + // Create an HTTP3ServerConnector instance. + HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactory, new HTTP3ServerConnectionFactory()); + + // The port to listen to. + connector.setPort(8080); + // The address to bind to. + connector.setHost("127.0.0.1"); + + server.addConnector(connector); + server.start(); + // end::configureConnectorQuic[] + } + public void configureConnectors() throws Exception { // tag::configureConnectors[] @@ -325,7 +332,7 @@ public void tlsHttp11() throws Exception // The HTTP configuration object. HttpConfiguration httpConfig = new HttpConfiguration(); - // Add the SecureRequestCustomizer because we are using TLS. + // Add the SecureRequestCustomizer because TLS is used. httpConfig.addCustomizer(new SecureRequestCustomizer()); // The ConnectionFactory for HTTP/1.1. @@ -378,7 +385,7 @@ public void tlsALPNHTTP() throws Exception // The HTTP configuration object. HttpConfiguration httpConfig = new HttpConfiguration(); - // Add the SecureRequestCustomizer because we are using TLS. + // Add the SecureRequestCustomizer because TLS is used. httpConfig.addCustomizer(new SecureRequestCustomizer()); // The ConnectionFactory for HTTP/1.1. @@ -435,7 +442,7 @@ public void handlerTree() class LoggingHandler extends Handler.Abstract { @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception + public boolean handle(Request request, Response response, Callback callback) { callback.succeeded(); return true; @@ -445,7 +452,7 @@ public boolean handle(Request request, Response response, Callback callback) thr class App1Handler extends Handler.Abstract { @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception + public boolean handle(Request request, Response response, Callback callback) { callback.succeeded(); return true; @@ -455,7 +462,7 @@ public boolean handle(Request request, Response response, Callback callback) thr class App2Handler extends Handler.Abstract { @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception + public boolean handle(Request request, Response response, Callback callback) { callback.succeeded(); return true; @@ -465,14 +472,14 @@ public boolean handle(Request request, Response response, Callback callback) thr // 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[] } @@ -493,7 +500,7 @@ public boolean handle(Request request, Response response, Callback callback) thr public void handlerHello() throws Exception { // tag::handlerHello[] - class HelloWorldHandler extends Handler.Abstract + class HelloWorldHandler extends Handler.Abstract.NonBlocking { @Override public boolean handle(Request request, Response response, Callback callback) @@ -558,7 +565,7 @@ public boolean handle(Request request, Response response, Callback callback) thr String newPath = "/new_path/" + path.substring("/old_path/".length()); HttpURI newURI = HttpURI.build(uri).path(newPath).asImmutable(); - // Modify the request object by wrapping the HttpURI + // Modify the request object by wrapping the HttpURI. Request newRequest = Request.serveAs(request, newURI); // Forward to the next Handler using the wrapped Request. @@ -576,20 +583,321 @@ public boolean handle(Request request, Response response, Callback callback) thr Connector connector = new ServerConnector(server); server.addConnector(connector); - // Link the Handlers. + // Link the Handlers in a chain. server.setHandler(new FilterHandler(new HelloWorldHandler())); server.start(); // end::handlerFilter[] } + public void handlerForm() + { + // tag::handlerForm[] + class FormHandler extends Handler.Abstract.NonBlocking + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE); + if (MimeTypes.Type.FORM_ENCODED.is(contentType)) + { + // Convert the request content into Fields. + CompletableFuture completableFields = FormFields.from(request); // <1> + + // When all the request content has arrived, process the fields. + completableFields.whenComplete((fields, failure) -> // <2> + { + if (failure == null) + { + processFields(fields); + // Send a simple 200 response, completing the callback. + response.setStatus(HttpStatus.OK_200); + callback.succeeded(); + } + else + { + // Reading the request content failed. + // Send an error response, completing the callback. + Response.writeError(request, response, callback, failure); + } + }); + + // The callback will be eventually completed in all cases, return true. + return true; + } + else + { + // Send an error response, completing the callback, and returning true. + Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400, "invalid request"); + return true; + } + } + } + // end::handlerForm[] + } + + private static void processFields(Fields fields) + { + } + + public void handlerMultiPart() + { + // tag::handlerMultiPart[] + class MultiPartFormDataHandler extends Handler.Abstract.NonBlocking + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE); + if (MimeTypes.Type.MULTIPART_FORM_DATA.is(contentType)) + { + // Extract the multipart boundary. + String boundary = MultiPart.extractBoundary(contentType); + + // Create and configure the multipart parser. + MultiPartFormData.Parser parser = new MultiPartFormData.Parser(boundary); + // By default, uploaded files are stored in this directory, to + // avoid to read the file content (which can be large) in memory. + parser.setFilesDirectory(Path.of("/tmp")); + // Convert the request content into parts. + CompletableFuture completableParts = parser.parse(request); // <1> + + // When all the request content has arrived, process the parts. + completableParts.whenComplete((parts, failure) -> // <2> + { + if (failure == null) + { + // Use the Parts API to process the parts. + processParts(parts); + // Send a simple 200 response, completing the callback. + response.setStatus(HttpStatus.OK_200); + callback.succeeded(); + } + else + { + // Reading the request content failed. + // Send an error response, completing the callback. + Response.writeError(request, response, callback, failure); + } + }); + + // The callback will be eventually completed in all cases, return true. + return true; + } + else + { + // Send an error response, completing the callback, and returning true. + Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400, "invalid request"); + return true; + } + } + } + // end::handlerMultiPart[] + } + + private void processParts(MultiPartFormData.Parts parts) + { + } + + public void flush() + { + // tag::flush[] + class FlushingHandler extends Handler.Abstract.NonBlocking + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Set the response status code. + response.setStatus(HttpStatus.OK_200); + // Set the response headers. + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain"); + + // Commit the response with a "flush" write. + Callback.Completable.with(flush -> response.write(false, null, flush)) + // When the flush is finished, send the content and complete the callback. + .whenComplete((ignored, failure) -> + { + if (failure == null) + response.write(true, UTF_8.encode("HELLO"), callback); + else + callback.failed(failure); + }); + + // Return true because the callback will eventually be completed. + return true; + } + } + // end::flush[] + } + + public void contentLength() + { + // tag::contentLength[] + class ContentLengthHandler extends Handler.Abstract.NonBlocking + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Set the response status code. + response.setStatus(HttpStatus.OK_200); + + String content = """ + { + "result": 0, + "advice": { + "message": "Jetty Rocks!" + } + } + """; + // Must count the bytes, not the characters! + byte[] bytes = content.getBytes(UTF_8); + long contentLength = bytes.length; + + // Set the response headers before the response is committed. + HttpFields.Mutable responseHeaders = response.getHeaders(); + // Set the content type. + responseHeaders.put(HttpHeader.CONTENT_TYPE, "application/json; charset=UTF-8"); + // Set the response content length. + responseHeaders.put(HttpHeader.CONTENT_LENGTH, contentLength); + + // Commit the response. + response.write(true, ByteBuffer.wrap(bytes), callback); + + // Return true because the callback will eventually be completed. + return true; + } + } + // end::contentLength[] + } + + public void handlerContinue100() + { + // tag::continue[] + class Continue100Handler extends Handler.Wrapper + { + public Continue100Handler(Handler handler) + { + super(handler); + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + HttpFields requestHeaders = request.getHeaders(); + if (requestHeaders.contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString())) + { + // Analyze the request and decide whether to receive the content. + long contentLength = requestHeaders.getLongField(HttpHeader.CONTENT_LENGTH); + if (contentLength > 0 && contentLength < 1024) + { + // Small request content, ask to send it by + // sending a 100 Continue interim response. + CompletableFuture processing = response.writeInterim(HttpStatus.CONTINUE_100, HttpFields.EMPTY) // <1> + // Then read the request content into a ByteBuffer. + .thenCompose(ignored -> Promise.Completable.with(p -> Content.Source.asByteBuffer(request, p))) + // Then store the ByteBuffer somewhere. + .thenCompose(byteBuffer -> store(byteBuffer)); + + // At the end of the processing, complete + // the callback with the CompletableFuture, + // a simple 200 response in case of success, + // or a 500 response in case of failure. + callback.completeWith(processing); // <2> + return true; + } + else + { + // The request content is too large, send an error. + Response.writeError(request, response, callback, HttpStatus.PAYLOAD_TOO_LARGE_413); + return true; + } + } + else + { + return super.handle(request, response, callback); + } + } + } + // end::continue[] + } + + private static CompletableFuture store(ByteBuffer byteBuffer) + { + return new CompletableFuture<>(); + } + + public void earlyHints() + { + // tag::earlyHints103[] + class EarlyHints103Handler extends Handler.Wrapper + { + public EarlyHints103Handler(Handler handler) + { + super(handler); + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + String pathInContext = Request.getPathInContext(request); + + // Simple logic that assumes that every HTML + // file has associated the same CSS stylesheet. + if (pathInContext.endsWith(".html")) + { + // Tell the client that a Link is coming + // sending a 103 Early Hints interim response. + HttpFields.Mutable interimHeaders = HttpFields.build() + .put(HttpHeader.LINK, "; rel=preload; as=style"); + + response.writeInterim(HttpStatus.EARLY_HINTS_103, interimHeaders) // <1> + .whenComplete((ignored, failure) -> // <2> + { + if (failure == null) + { + try + { + // Delegate the handling to the child Handler. + boolean handled = super.handle(request, response, callback); + if (!handled) + { + // The child Handler did not produce a final response, do it here. + Response.writeError(request, response, callback, HttpStatus.NOT_FOUND_404); + } + } + catch (Throwable x) + { + callback.failed(x); + } + } + else + { + callback.failed(failure); + } + }); + + // This Handler sent an interim response, so this Handler + // (or its descendants) must produce a final response, so return true. + return true; + } + else + { + // Not a request for an HTML page, delegate + // the handling to the child Handler. + return super.handle(request, response, callback); + } + } + } + // end::earlyHints103[] + } + public void contextHandler() throws Exception { // tag::contextHandler[] class ShopHandler extends Handler.Abstract { @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception + public boolean handle(Request request, Response response, Callback callback) { // Implement the shop, remembering to complete the callback. return true; @@ -616,7 +924,7 @@ public void contextHandlerCollection() throws Exception class ShopHandler extends Handler.Abstract { @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception + public boolean handle(Request request, Response response, Callback callback) { // Implement the shop, remembering to complete the callback. return true; @@ -626,7 +934,7 @@ public boolean handle(Request request, Response response, Callback callback) thr class RESTHandler extends Handler.Abstract { @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception + public boolean handle(Request request, Response response, Callback callback) { // Implement the REST APIs, remembering to complete the callback. return true; @@ -677,6 +985,8 @@ public void servletContextHandler() throws Exception // Create a ServletContextHandler with contextPath. ServletContextHandler context = new ServletContextHandler(); context.setContextPath("/shop"); + // Link the context to the server. + server.setHandler(context); // Add the Servlet implementing the cart functionality to the context. ServletHolder servletHolder = context.addServlet(ShopCartServlet.class, "/cart/*"); @@ -688,9 +998,6 @@ public void servletContextHandler() throws Exception // Configure the filter. filterHolder.setAsyncSupported(true); - // Link the context to the server. - server.setHandler(context); - server.start(); // end::servletContextHandler-setup[] } @@ -704,14 +1011,14 @@ public void webAppContextHandler() throws Exception // Create a WebAppContext. WebAppContext context = new WebAppContext(); + // Link the context to the server. + server.setHandler(context); + // Configure the path of the packaged web application (file or directory). context.setWar("/path/to/webapp.war"); // Configure the contextPath. context.setContextPath("/app"); - // Link the context to the server. - server.setHandler(context); - server.start(); // end::webAppContextHandler[] } @@ -728,8 +1035,7 @@ public void resourceHandler() throws Exception // Configure the directory where static resources are located. handler.setBaseResource(ResourceFactory.of(handler).newResource("/path/to/static/resources/")); // Configure directory listing. - // TODO: is the directoriesListed feature still present? -// handler.setDirectoriesListed(false); + handler.setDirAllowed(false); // Configure welcome files. handler.setWelcomeFiles(List.of("index.html")); // Configure whether to accept range requests. @@ -742,7 +1048,7 @@ public void resourceHandler() throws Exception // end::resourceHandler[] } - public void multipleResourcesHandler() throws Exception + public void multipleResourcesHandler() { // tag::multipleResourcesHandler[] ResourceHandler handler = new ResourceHandler(); @@ -778,10 +1084,9 @@ public void serverGzipHandler() throws Exception Connector connector = new ServerConnector(server); server.addConnector(connector); - // Create a ContextHandlerCollection to manage contexts. - ContextHandlerCollection contexts = new ContextHandlerCollection(); - // Create and configure GzipHandler linked to the ContextHandlerCollection. - GzipHandler gzipHandler = new GzipHandler(contexts); + // Create and configure GzipHandler. + GzipHandler gzipHandler = new GzipHandler(); + server.setHandler(gzipHandler); // Only compress response content larger than this. gzipHandler.setMinGzipSize(1024); // Do not compress these URI paths. @@ -791,8 +1096,9 @@ public void serverGzipHandler() throws Exception // Do not compress these mime types. gzipHandler.addExcludedMimeTypes("font/ttf"); - // Link the GzipHandler to the Server. - server.setHandler(gzipHandler); + // Create a ContextHandlerCollection to manage contexts. + ContextHandlerCollection contexts = new ContextHandlerCollection(); + gzipHandler.setHandler(contexts); server.start(); // end::serverGzipHandler[] @@ -803,7 +1109,7 @@ public void contextGzipHandler() throws Exception class ShopHandler extends Handler.Abstract { @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception + public boolean handle(Request request, Response response, Callback callback) { // Implement the shop, remembering to complete the callback. return true; @@ -813,20 +1119,22 @@ public boolean handle(Request request, Response response, Callback callback) thr class RESTHandler extends Handler.Abstract { @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception + public boolean handle(Request request, Response response, Callback callback) { // Implement the REST APIs, remembering to complete the callback. return true; } } + // tag::contextGzipHandler[] Server server = new Server(); ServerConnector connector = new ServerConnector(server); server.addConnector(connector); - // 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 wrapped with GzipHandler so only the shop will do gzip. GzipHandler shopGzipHandler = new GzipHandler(new ContextHandler(new ShopHandler(), "/shop")); @@ -840,11 +1148,8 @@ public boolean handle(Request request, Response response, Callback callback) thr // Add it to ContextHandlerCollection. contextCollection.addHandler(apiContext); - // Link the ContextHandlerCollection to the Server. - server.setHandler(contextCollection); - // end::contextGzipHandler[] - server.start(); + // end::contextGzipHandler[] } public void rewriteHandler() throws Exception @@ -854,10 +1159,10 @@ public void rewriteHandler() throws Exception ServerConnector connector = new ServerConnector(server); server.addConnector(connector); - // Create a ContextHandlerCollection to hold contexts. - ContextHandlerCollection contextCollection = new ContextHandlerCollection(); - // Link the ContextHandlerCollection to the RewriteHandler. - RewriteHandler rewriteHandler = new RewriteHandler(contextCollection); + // Create and link the RewriteHandler to the Server. + RewriteHandler rewriteHandler = new RewriteHandler(); + server.setHandler(rewriteHandler); + // Compacts URI paths with double slashes, e.g. /ctx//path/to//resource. rewriteHandler.addRule(new CompactPathRule()); // Rewrites */products/* to */p/*. @@ -867,8 +1172,9 @@ public void rewriteHandler() throws Exception redirectRule.setStatusCode(HttpStatus.MOVED_PERMANENTLY_301); rewriteHandler.addRule(redirectRule); - // Link the RewriteHandler to the Server. - server.setHandler(rewriteHandler); + // Create a ContextHandlerCollection to hold contexts. + ContextHandlerCollection contextCollection = new ContextHandlerCollection(); + rewriteHandler.setHandler(contextCollection); server.start(); // end::rewriteHandler[] @@ -881,14 +1187,13 @@ public void statisticsHandler() throws Exception ServerConnector connector = new ServerConnector(server); server.addConnector(connector); + // Create and link the StatisticsHandler to the Server. + StatisticsHandler statsHandler = new StatisticsHandler(); + server.setHandler(statsHandler); + // 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); + statsHandler.setHandler(contextCollection); server.start(); // end::statisticsHandler[] @@ -901,14 +1206,14 @@ public void dataRateHandler() throws Exception ServerConnector connector = new ServerConnector(server); server.addConnector(connector); + // Create and link the MinimumDataRateHandler to the Server. + // 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); + server.setHandler(dataRateHandler); + // 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); + dataRateHandler.setHandler(contextCollection); server.start(); // end::dataRateHandler[] @@ -971,12 +1276,12 @@ public void securedHandler() throws Exception connector.setPort(8080); server.addConnector(connector); - // Configure the HttpConfiguration for the encrypted connector. + // Configure the HttpConfiguration for the secure connector. HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); - // Add the SecureRequestCustomizer because we are using TLS. + // Add the SecureRequestCustomizer because TLS is used. httpConfig.addCustomizer(new SecureRequestCustomizer()); - // The HttpConnectionFactory for the encrypted connector. + // The HttpConnectionFactory for the secure connector. HttpConnectionFactory http11 = new HttpConnectionFactory(httpsConfig); // Configure the SslContextFactory with the keyStore information. @@ -987,19 +1292,18 @@ public void securedHandler() throws Exception // The ConnectionFactory for TLS. SslConnectionFactory tls = new SslConnectionFactory(sslContextFactory, http11.getProtocol()); - // The encrypted connector. + // The secure connector. ServerConnector secureConnector = new ServerConnector(server, tls, http11); secureConnector.setPort(8443); server.addConnector(secureConnector); + // Create and link the SecuredRedirectHandler to the Server. + SecuredRedirectHandler securedHandler = new SecuredRedirectHandler(); + server.setHandler(securedHandler); + // 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); + securedHandler.setHandler(contextCollection); server.start(); // end::securedHandler[] diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/EarlyHintsProtocolHandler.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/EarlyHintsProtocolHandler.java index e4fa0440a879..390a9e7bf7d6 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/EarlyHintsProtocolHandler.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/EarlyHintsProtocolHandler.java @@ -37,7 +37,7 @@ public String getName() @Override public boolean accept(Request request, Response response) { - return response.getStatus() == HttpStatus.EARLY_HINT_103; + return response.getStatus() == HttpStatus.EARLY_HINTS_103; } @Override diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java index 8148c7b56a06..619c4017d3ee 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java @@ -381,7 +381,7 @@ public Result generateResponse(MetaData.Response info, boolean head, ByteBuffer { case HttpStatus.SWITCHING_PROTOCOLS_101: break; - case HttpStatus.EARLY_HINT_103: + case HttpStatus.EARLY_HINTS_103: generateHeaders(header, content, last); _state = State.COMPLETING_1XX; return Result.FLUSH; diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java index ab0278ae865d..71ed7ac544be 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java @@ -25,7 +25,9 @@ public class HttpStatus public static final int CONTINUE_100 = 100; public static final int SWITCHING_PROTOCOLS_101 = 101; public static final int PROCESSING_102 = 102; + @Deprecated(forRemoval = true) public static final int EARLY_HINT_103 = 103; + public static final int EARLY_HINTS_103 = 103; public static final int OK_200 = 200; public static final int CREATED_201 = 201; @@ -104,7 +106,9 @@ public enum Code CONTINUE(CONTINUE_100, "Continue"), SWITCHING_PROTOCOLS(SWITCHING_PROTOCOLS_101, "Switching Protocols"), PROCESSING(PROCESSING_102, "Processing"), + @Deprecated(forRemoval = true) EARLY_HINT(EARLY_HINT_103, "Early Hint"), + EARLY_HINTS(EARLY_HINTS_103, "Early Hints"), OK(OK_200, "OK"), CREATED(CREATED_201, "Created"), @@ -123,7 +127,7 @@ public enum Code NOT_MODIFIED(NOT_MODIFIED_304, "Not Modified"), USE_PROXY(USE_PROXY_305, "Use Proxy"), TEMPORARY_REDIRECT(TEMPORARY_REDIRECT_307, "Temporary Redirect"), - // Keeping the typo for backward compatibility for a while + @Deprecated(forRemoval = true) PERMANET_REDIRECT(PERMANENT_REDIRECT_308, "Permanent Redirect"), PERMANENT_REDIRECT(PERMANENT_REDIRECT_308, "Permanent Redirect"), diff --git a/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/HTTP3StreamClient.java b/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/HTTP3StreamClient.java index 0d1229ee787e..3ec67fa2d2b0 100644 --- a/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/HTTP3StreamClient.java +++ b/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/HTTP3StreamClient.java @@ -61,7 +61,7 @@ public void onResponse(HeadersFrame frame) { case HttpStatus.CONTINUE_100 -> validateAndUpdate(EnumSet.of(FrameState.INITIAL), FrameState.INFORMATIONAL); case HttpStatus.PROCESSING_102, - HttpStatus.EARLY_HINT_103 -> validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.INFORMATIONAL), FrameState.INFORMATIONAL); + HttpStatus.EARLY_HINTS_103 -> validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.INFORMATIONAL), FrameState.INFORMATIONAL); default -> validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.INFORMATIONAL), FrameState.HEADER); }; if (valid) diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Content.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Content.java index 9870f279736b..96785bc7e3fd 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Content.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Content.java @@ -20,6 +20,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Objects; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Flow; import java.util.function.Consumer; @@ -199,9 +200,7 @@ static String asString(Source source, Charset charset) throws IOException { try { - FuturePromise promise = new FuturePromise<>(); - asString(source, charset, promise); - return promise.get(); + return asStringAsync(source, charset).get(); } catch (Throwable x) { @@ -209,6 +208,21 @@ static String asString(Source source, Charset charset) throws IOException } } + /** + *

Read, non-blocking, the whole content source into a {@link String}, converting + * the bytes using the given {@link Charset}.

+ * + * @param source the source to read + * @param charset the charset to use to decode bytes + * @return the {@link CompletableFuture} to notify when the whole content has been read + */ + static CompletableFuture asStringAsync(Source source, Charset charset) + { + Promise.Completable completable = new Promise.Completable<>(); + asString(source, charset, completable); + return completable; + } + /** *

Wraps the given content source with an {@link InputStream}.

* diff --git a/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java b/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java index 308df24b104d..9f004e3692c1 100644 --- a/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java +++ b/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java @@ -460,7 +460,7 @@ protected void onServerToProxyResponse103EarlyHints(Request clientToProxyRequest { if (LOG.isDebugEnabled()) LOG.debug("{} P2C 103 interim response {}", requestId(clientToProxyRequest), serverToProxyResponseHeaders); - proxyToClientResponse.writeInterim(HttpStatus.EARLY_HINT_103, serverToProxyResponseHeaders); + proxyToClientResponse.writeInterim(HttpStatus.EARLY_HINTS_103, serverToProxyResponseHeaders); } protected void onProxyToClientResponseComplete(Request clientToProxyRequest, org.eclipse.jetty.client.Request proxyToServerRequest, org.eclipse.jetty.client.Response serverToProxyResponse, Response proxyToClientResponse, Callback proxyToClientCallback) diff --git a/jetty-core/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/InterimResponseProxyTest.java b/jetty-core/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/InterimResponseProxyTest.java index 0665900201b1..3f016e0cede7 100644 --- a/jetty-core/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/InterimResponseProxyTest.java +++ b/jetty-core/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/InterimResponseProxyTest.java @@ -46,7 +46,7 @@ public boolean handle(Request request, Response response, Callback callback) CompletableFuture completable = response.writeInterim(HttpStatus.CONTINUE_100, HttpFields.EMPTY) .thenCompose(ignored -> Promise.Completable.with(p -> Content.Source.asString(request, StandardCharsets.UTF_8, p))) .thenCompose(content -> response.writeInterim(HttpStatus.PROCESSING_102, HttpFields.EMPTY).thenApply(ignored -> content)) - .thenCompose(content -> response.writeInterim(HttpStatus.EARLY_HINT_103, HttpFields.EMPTY).thenApply(ignored -> content)) + .thenCompose(content -> response.writeInterim(HttpStatus.EARLY_HINTS_103, HttpFields.EMPTY).thenApply(ignored -> content)) .thenCompose(content -> Callback.Completable.with(c -> Content.Sink.write(response, true, content, c))); callback.completeWith(completable); return true; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 3c41426f1d7e..963ccb1b0587 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.function.Function; @@ -521,10 +522,15 @@ static Fields extractQueryParameters(Request request, Charset charset) } static Fields getParameters(Request request) throws Exception + { + return getParametersAsync(request).get(); + } + + static CompletableFuture getParametersAsync(Request request) { Fields queryFields = Request.extractQueryParameters(request); - Fields formFields = FormFields.from(request).get(); - return Fields.combine(queryFields, formFields); + CompletableFuture contentFields = FormFields.from(request); + return contentFields.thenApply(formFields -> Fields.combine(queryFields, formFields)); } @SuppressWarnings("unchecked") diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 77ac7e9a0cfd..f2deaed25379 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -129,7 +129,7 @@ public interface Response extends Content.Sink *

Writes an {@link HttpStatus#isInterim(int) HTTP interim response}, * with the given HTTP status code and HTTP headers.

*

It is possible to write more than one interim response, for example - * in case of {@link HttpStatus#EARLY_HINT_103}.

+ * in case of {@link HttpStatus#EARLY_HINTS_103}.

*

The returned {@link CompletableFuture} is notified of the result * of this write, whether it succeeded or failed.

* diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpInterimResponseTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpInterimResponseTest.java index 73f868d10b0f..fd08502058ca 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpInterimResponseTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpInterimResponseTest.java @@ -93,14 +93,14 @@ public boolean handle(Request request, Response response, Callback callback) thr { HttpFields.Mutable hints = HttpFields.build(); hints.put(HttpHeader.LINK, "; rel=preload"); - return response.writeInterim(HttpStatus.EARLY_HINT_103, hints) + return response.writeInterim(HttpStatus.EARLY_HINTS_103, hints) .thenApply(i -> hints); }) .thenCompose(hints1 -> { HttpFields.Mutable hints = HttpFields.build(); hints.put(HttpHeader.LINK, "; rel=preload"); - return response.writeInterim(HttpStatus.EARLY_HINT_103, hints) + return response.writeInterim(HttpStatus.EARLY_HINTS_103, hints) .thenApply(i -> HttpFields.build(hints1).add(hints)); }) .thenCompose(hints -> diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Callback.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Callback.java index 22cb55ee241d..d394ab4c0d91 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Callback.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/Callback.java @@ -495,7 +495,7 @@ public InvocationType getInvocationType() } /** - *

A CompletableFuture that is also a Callback.

+ *

A {@link CompletableFuture} that is also a {@link Callback}.

*/ class Completable extends CompletableFuture implements Callback { @@ -568,5 +568,28 @@ public InvocationType getInvocationType() { return invocation; } + + /** + *

Returns a new {@link Completable} that, when this {@link Completable} + * succeeds, is passed to the given consumer and then returned.

+ *

If this {@link Completable} fails, the new {@link Completable} is + * also failed, and the consumer is not invoked.

+ * + * @param consumer the consumer that receives the {@link Completable} + * @return a new {@link Completable} passed to the consumer + * @see #with(Consumer) + */ + public Completable compose(Consumer consumer) + { + Completable completable = new Completable(); + whenComplete((r, x) -> + { + if (x == null) + consumer.accept(completable); + else + completable.failed(x); + }); + return completable; + } } } diff --git a/jetty-core/jetty-websocket/jetty-websocket-jetty-api/src/main/java/org/eclipse/jetty/websocket/api/Callback.java b/jetty-core/jetty-websocket/jetty-websocket-jetty-api/src/main/java/org/eclipse/jetty/websocket/api/Callback.java index 00fc7a11f16c..a07eca3e1258 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-jetty-api/src/main/java/org/eclipse/jetty/websocket/api/Callback.java +++ b/jetty-core/jetty-websocket/jetty-websocket-jetty-api/src/main/java/org/eclipse/jetty/websocket/api/Callback.java @@ -137,7 +137,7 @@ public void fail(Throwable x) *

Returns a new {@link Completable} that, when this {@link Completable} * succeeds, is passed to the given consumer and then returned.

*

If this {@link Completable} fails, the new {@link Completable} is - * also failed.

+ * also failed, and the consumer is not invoked.

*

For example:

*
{@code
          * Callback.Completable.with(completable1 -> session.sendPartialText("hello", false, completable1))
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java
index 8998b8459c39..c694176b8f75 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java
@@ -143,7 +143,7 @@ public void sendError(int sc, String msg) throws IOException
         switch (sc)
         {
             case -1 -> getServletChannel().abort(new IOException(msg));
-            case HttpStatus.PROCESSING_102, HttpStatus.EARLY_HINT_103 ->
+            case HttpStatus.PROCESSING_102, HttpStatus.EARLY_HINTS_103 ->
             {
                 if (!isCommitted())
                 {
diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/InformationalResponseTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/InformationalResponseTest.java
index d8867cbccbf0..6bc8991aed5f 100644
--- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/InformationalResponseTest.java
+++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/InformationalResponseTest.java
@@ -84,9 +84,9 @@ public void test103EarlyHint(Transport transport) throws Exception
             protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
             {
                 response.addHeader("Hint", "one");
-                response.sendError(HttpStatus.EARLY_HINT_103);
+                response.sendError(HttpStatus.EARLY_HINTS_103);
                 response.addHeader("Hint", "two");
-                response.sendError(HttpStatus.EARLY_HINT_103);
+                response.sendError(HttpStatus.EARLY_HINTS_103);
                 response.addHeader("Hint", "three");
                 response.setStatus(200);
                 response.getOutputStream().print("OK");
diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpChannel.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpChannel.java
index 3441b45fe04b..b7af3a429e87 100644
--- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpChannel.java
+++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpChannel.java
@@ -510,7 +510,7 @@ public void send103EarlyHints(HttpFields headers) throws IOException
     {
         try
         {
-            _coreResponse.writeInterim(HttpStatus.EARLY_HINT_103, headers).get();
+            _coreResponse.writeInterim(HttpStatus.EARLY_HINTS_103, headers).get();
         }
         catch (Throwable x)
         {
diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java
index f8c0be18c572..5392497d325c 100644
--- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java
+++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java
@@ -469,7 +469,7 @@ public void sendError(int code, String message) throws IOException
         {
             case -1 -> _channel.abort(new IOException(message));
             case HttpStatus.PROCESSING_102 -> sendProcessing();
-            case HttpStatus.EARLY_HINT_103 -> sendEarlyHint();
+            case HttpStatus.EARLY_HINTS_103 -> sendEarlyHint();
             default -> _channel.getState().sendError(code, message);
         }
     }
diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/InformationalResponseTest.java b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/InformationalResponseTest.java
index 1b501d81c786..f3498e7e58e7 100644
--- a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/InformationalResponseTest.java
+++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/InformationalResponseTest.java
@@ -84,9 +84,9 @@ public void test103EarlyHint(Transport transport) throws Exception
             protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
             {
                 response.addHeader("Hint", "one");
-                response.sendError(HttpStatus.EARLY_HINT_103);
+                response.sendError(HttpStatus.EARLY_HINTS_103);
                 response.addHeader("Hint", "two");
-                response.sendError(HttpStatus.EARLY_HINT_103);
+                response.sendError(HttpStatus.EARLY_HINTS_103);
                 response.addHeader("Hint", "three");
                 response.setStatus(200);
                 response.getOutputStream().print("OK");