Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #12612 - Use Compression classes for client decoding. #12613

Merged
merged 21 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f53dd63
Fixes #12612 - Use Compression classes for client decoding.
sbordet Dec 5, 2024
7a06103
Updated OSGi dependencies, now that jetty-client depends on jetty-com…
sbordet Dec 5, 2024
f6ce763
Now using ServiceLoader to load the Compression implementations.
sbordet Dec 5, 2024
8abb27d
Reorganized the Jetty modules so that the client can use them without…
sbordet Dec 5, 2024
05d6b15
* Removed EncoderSink.canEncode(), since it is too late to skip encod…
sbordet Dec 6, 2024
4463087
Fixes #12618 - Update jetty-compression for OSGi.
sbordet Dec 6, 2024
a1ed64d
Fixed client.mod dependencies.
sbordet Dec 6, 2024
f7806b8
Updated test dependencies.
sbordet Dec 7, 2024
4fb2116
Updated documentation and migration guide.
sbordet Dec 8, 2024
8e2b8ac
Updates from reviews.
sbordet Dec 9, 2024
f2eb965
Fixed typo.
sbordet Dec 9, 2024
601b940
Fixed typos.
sbordet Dec 9, 2024
c55a3d4
Updates from review.
sbordet Dec 9, 2024
e3cf38f
Updated test after implementing minCompressSize.
sbordet Dec 10, 2024
72bb0b1
Updates from review.
sbordet Dec 12, 2024
cb2c159
Updates from review.
sbordet Dec 13, 2024
1c32a08
Fixed ContentSourceTransformer.
sbordet Dec 13, 2024
78bfe48
Merged branch 'jetty-12.1.x' into 'fix/jetty-12.1.x/12612/client-comp…
sbordet Dec 13, 2024
fc23160
Implemented support for quality values in Accept-Encoding.
sbordet Dec 15, 2024
50067d7
Added module compression-all and correspondent distribution test.
sbordet Dec 15, 2024
9660e12
Added module documentation.
sbordet Dec 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions documentation/jetty/modules/code/examples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util-ajax</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.compression</groupId>
<artifactId>jetty-compression-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee11</groupId>
<artifactId>jetty-ee11-servlet</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,20 @@ public void outputStreamRequestContent() throws Exception
// end::outputStreamRequestContent[]
}

public void removeDecoders() throws Exception
{
// tag::removeDecoders[]
HttpClient httpClient = new HttpClient();

// Starting HttpClient will discover response content decoders
// implementations from the module-path or class-path via ServiceLoader.
httpClient.start();

// Remove all response content decoders.
httpClient.getContentDecoderFactories().clear();
// end::removeDecoders[]
}

public void futureResponseListener() throws Exception
{
HttpClient httpClient = new HttpClient();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.compression.server.CompressionConfig;
import org.eclipse.jetty.compression.server.CompressionHandler;
import org.eclipse.jetty.ee11.servlet.DefaultServlet;
import org.eclipse.jetty.ee11.servlet.ResourceServlet;
import org.eclipse.jetty.ee11.servlet.ServletContextHandler;
Expand Down Expand Up @@ -1400,6 +1402,83 @@ public boolean handle(Request request, Response response, Callback callback)
// end::contextGzipHandler[]
}

public void serverCompressionHandler() throws Exception
{
// tag::serverCompressionHandler[]
Server server = new Server();
Connector connector = new ServerConnector(server);
server.addConnector(connector);

// Create and configure CompressionHandler.
CompressionHandler compressionHandler = new CompressionHandler();
server.setHandler(compressionHandler);

CompressionConfig compressionConfig = CompressionConfig.builder()
// Do not compress these URI paths.
.compressExcludePath("/uncompressed")
// Also compress POST responses.
.compressIncludeMethod("POST")
// Do not compress these mime types.
.compressExcludeMimeType("font/ttf")
.build();
compressionHandler.putConfiguration("/*", compressionConfig);

// Create a ContextHandlerCollection to manage contexts.
ContextHandlerCollection contexts = new ContextHandlerCollection();
compressionHandler.setHandler(contexts);

server.start();
// end::serverCompressionHandler[]
}

public void contextCompressionHandler() throws Exception
{
class ShopHandler extends Handler.Abstract
{
@Override
public boolean handle(Request request, Response response, Callback callback)
{
// Implement the shop, remembering to complete the callback.
return true;
}
}

class RESTHandler extends Handler.Abstract
{
@Override
public boolean handle(Request request, Response response, Callback callback)
{
// Implement the REST APIs, remembering to complete the callback.
return true;
}
}

// tag::contextCompressionHandler[]
Server server = new Server();
ServerConnector connector = new ServerConnector(server);
server.addConnector(connector);

// 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 CompressionHandler so only the shop will do compression.
CompressionHandler shopCompressionHandler = new CompressionHandler(new ContextHandler(new ShopHandler(), "/shop"));

// Add it to ContextHandlerCollection.
contextCollection.addHandler(shopCompressionHandler);

// Create the context for the API web application.
ContextHandler apiContext = new ContextHandler(new RESTHandler(), "/api");

// Add it to ContextHandlerCollection.
contextCollection.addHandler(apiContext);

server.start();
// end::contextCompressionHandler[]
}

public void rewriteHandler() throws Exception
{
// tag::rewriteHandler[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Out of the box features that you get with the Jetty HTTP client include:
* Cookies support -- cookies sent by servers are stored and sent back to servers in matching requests.
* Authentication support -- HTTP "Basic", "Digest" and "SPNEGO" authentications are supported, others are pluggable.
* Forward proxy support -- HTTP proxying, SOCKS4 and SOCKS5 proxying.
* Response content decoding -- response content compressed by the server is transparently decompressed.

[[start]]
== Starting HttpClient
Expand Down Expand Up @@ -114,7 +115,8 @@ A `HttpClient` instance can be thought as a browser instance, and it manages the
* A `CookieStore` (see <<cookie,this section>>).
* A `AuthenticationStore` (see <<authentication,this section>>).
* A `ProxyConfiguration` (see <<proxy,this section>>).
* A set of ``Destination``s
* A set of ``ContentDecoder.Factory``s (see <<content-response,this section>>).
* A set of ``Destination``s, where each `Destination` manages a <<connection-pool,pool of connections>>.

A `Destination` is the client-side component that represents an _origin_ server, and manages a queue of requests for that origin, and a <<connection-pool,pool of connections>> to that origin.

Expand Down Expand Up @@ -441,6 +443,47 @@ include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/ht
[[content-response]]
=== Response Content Handling

Response content may be _encoded_ by the server, and this is signaled by the presence of the `Content-Encoding` response header.

Typically, encoding the response content means that it is compressed by the server, to save bytes transmitted over the network.
The most common encodings are:

* `gzip`, using the link:https://en.wikipedia.org/wiki/Gzip[gzip] algorithm.
* `br`, using the link:https://github.com/google/brotli[brotli] algorithm.
* `zstd`, using the link:https://github.com/facebook/zstd[zstdandard] algorithm.

Jetty's `HttpClient` supports by default the `gzip` encoding, but the support is pluggable and discovered via Java's `ServiceLoader` mechanism.

In order to add support for brotli and zstandard, it is enough to add to your classpath the correspondent Jetty artifacts with the implementation, respectively:

* `jetty-compression-brotli`
* `jetty-compression-zstandard`
gregw marked this conversation as resolved.
Show resolved Hide resolved

The Maven artifact coordinates are the following:

[,xml,subs=attributes+]
----
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.compression</groupId>
<artifactId>jetty-compression-brotli</artifactId>
<version>{jetty-version}</version>
sbordet marked this conversation as resolved.
Show resolved Hide resolved
</dependency>
<dependency>
<groupId>org.eclipse.jetty.compression</groupId>
<artifactId>jetty-compression-zstandard</artifactId>
<version>{jetty-version}</version>
sbordet marked this conversation as resolved.
Show resolved Hide resolved
</dependency>
</dependencies>
----

If you want to remove support for automatic response content decoding (for example in proxies), use the following code:

[,java,indent=0]
----
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=removeDecoders]
----

Jetty's `HttpClient` allows applications to handle response content in different ways.

You can buffer the response content in memory; this is done when using the <<blocking,blocking APIs>> and the content is buffered within a `ContentResponse` up to 2 MiB.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
[[api-changes]]
== APIs Changes

=== `HttpClient`

In Jetty 12.0.x, applications could configure response content decoding through `HttpClient.getContentDecoderFactories()`, and implement their own by implementing `org.eclipse.jetty.client.ContentDecoder`.

In Jetty 12.1.x, applications can configure response content decoding through `HttpClient.getContentDecoderFactories()` as before, but the decoding is based on the `org.eclipse.jetty.compression.Compression` classes.
sbordet marked this conversation as resolved.
Show resolved Hide resolved
Applications can implement their own response content decoding by implementing a `Compression` subclass and the corresponding `DecoderSource`, based on the xref:arch/io.adoc#content-source[`Content.Source`] and xref:arch/io.adoc#content-source-chunk[`Content.Chunk`] APIs.

=== `IteratingCallback`

Class `IteratingCallback` underwent refinements that changed the behavior of the `onCompleteFailure(Throwable)` method.
Expand Down
105 changes: 102 additions & 3 deletions documentation/jetty/modules/programming-guide/pages/server/http.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -852,10 +852,111 @@ For example, you can specify to match only the specific HTTP request method `DEL

Notable subclasses of `ConditionalHandler` are, for example, <<handler-use-qos,`QoSHandler`>> and <<handler-use-thread-limit,`ThreadLimitHandler`>>.

[[handler-use-compression]]
==== CompressionHandler

`CompressionHandler` provides support for automatic decompression of compressed request content and automatic compression of response content.

The Maven artifact coordinates are:

[,xml,subs=attributes+]
----
<dependency>
<groupId>org.eclipse.jetty.compression</groupId>
<artifactId>jetty-compression-server</artifactId>
<version>{jetty-version}</version>
</dependency>
----

The compression algorithms supported are:

* link:https://github.com/google/brotli[brotli]; identified by `br` in `Accept-Encoding` and `Content-Encoding` HTTP headers.
* link:https://en.wikipedia.org/wiki/Gzip[gzip]; identified by `gzip` in `Accept-Encoding` and `Content-Encoding` HTTP headers.
* link:https://github.com/facebook/zstd[zstdandard]; identified by `zstd` in `Accept-Encoding` and `Content-Encoding` HTTP headers.

`CompressionHandler` discovers compression algorithm implementations via Java's `ServiceLoader` mechanism, so it is enough to put the Jetty artifacts with the compression implementation in the module-path or class-path, respectively:

* `jetty-compression-brotli`
* `jetty-compression-gzip`
* `jetty-compression-zstandard`

The Maven artifact coordinates are the following:

[,xml,subs=attributes+]
----
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.compression</groupId>
<artifactId>jetty-compression-brotli</artifactId>
<version>{jetty-version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.compression</groupId>
<artifactId>jetty-compression-gzip</artifactId>
<version>{jetty-version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.compression</groupId>
<artifactId>jetty-compression-zstandard</artifactId>
<version>{jetty-version}</version>
</dependency>
</dependencies>
----

`CompressionHandler` is a `Handler.Wrapper` that inspects the request and, if the conditions are met, it wraps the request and the response to eventually perform decompression of the request content or compression of the response content.
The decompression/compression is not performed until the web application reads request content or writes response content.

`CompressionHandler` comes with a default configuration, but can be explicitly configured through a `CompressionConfig` object that can be constructed via the builder pattern.

Furthermore, `CompressionHandler` can be configured either at the server level, or at the context level.

`CompressionHandler` can be configured at the server level in this way:

[,java,indent=0]
----
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=serverCompressionHandler]
----

The `Handler` tree structure looks like the following:

[,screen]
----
Server
└── CompressionHandler
└── ContextHandlerCollection
├── ContextHandler 1
:── ...
└── ContextHandler N
----

However, in less common cases, you can configure `CompressionHandler` on a per-context basis, for example because you want to configure `CompressionHandler` with different parameters for each context, or because you want only some contexts to have compression support:

[,java,indent=0]
----
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=contextCompressionHandler]
----

The `Handler` tree structure looks like the following:

[,screen]
----
Server
└── ContextHandlerCollection
└── ContextHandlerCollection
├── CompressionHandler
│ └── ContextHandler /shop
│ └── ShopHandler
└── ContextHandler /api
└── RESTHandler
----

[[handler-use-gzip]]
==== GzipHandler

`GzipHandler` provides supports for automatic decompression of compressed request content and automatic compression of response content.
`GzipHandler` provides supports for automatic decompression of gzip compressed request content and automatic gzip compression of response content.

`GzipHandler` is now obsoleted by <<handler-use-compression,`CompressionHandler`>>, but still maintained for backwards compatibility.
`CompressionHandler` currently supports `gzip` but also other compression algorithms such as `brotli` and `zstandard`.

`GzipHandler` is a `Handler.Wrapper` that inspects the request and, if the request matches the `GzipHandler` configuration, just installs the required components to eventually perform decompression of the request content or compression of the response content.
The decompression/compression is not performed until the web application reads request content or writes response content.
Expand Down Expand Up @@ -900,8 +1001,6 @@ Server
└── RESTHandler
----

// TODO: does ServletContextHandler really need a special configuration?

[[handler-use-rewrite]]
==== RewriteHandler

Expand Down
19 changes: 19 additions & 0 deletions jetty-core/jetty-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@
<artifactId>jetty-jmx</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.compression</groupId>
<artifactId>jetty-compression-common</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.compression</groupId>
<artifactId>jetty-compression-gzip</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
Expand Down Expand Up @@ -77,6 +85,17 @@

<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<instructions>
<Import-Package>${osgi.slf4j.import.packages},org.eclipse.jetty.compression;resolution:=optional,*</Import-Package>
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional, osgi.serviceloader; filter:="(osgi.serviceloader=org.eclipse.jetty.compression.Compression)";resolution:=optional;cardinality:=multiple</Require-Capability>
</instructions>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
Expand Down
3 changes: 3 additions & 0 deletions jetty-core/jetty-client/src/main/config/modules/client.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Adds the Jetty HTTP client dependencies to the server classpath.
[tags]
client

[depends]
gzip-compression

[lib]
lib/jetty-alpn-client-${jetty.version}.jar
lib/jetty-alpn-java-client-${jetty.version}.jar
Expand Down
4 changes: 4 additions & 0 deletions jetty-core/jetty-client/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
requires org.eclipse.jetty.alpn.client;
requires org.slf4j;

requires org.eclipse.jetty.compression;
requires org.eclipse.jetty.compression.gzip;
gregw marked this conversation as resolved.
Show resolved Hide resolved
requires transitive org.eclipse.jetty.http;

// Only required if using JMX.
Expand All @@ -30,4 +32,6 @@

exports org.eclipse.jetty.client.jmx to
org.eclipse.jetty.jmx;

uses org.eclipse.jetty.compression.Compression;
}
Loading
Loading