Skip to content

Commit 8aae5ff

Browse files
committed
Support Brotli compression in JDK HTTP Client
Enable Brotli/ZStandard in Jetty HTTP Client Add tests for compressed responses. This closes #1744
1 parent bca6186 commit 8aae5ff

File tree

11 files changed

+315
-30
lines changed

11 files changed

+315
-30
lines changed

maven-resolver-test-http/pom.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@
8181
<groupId>org.eclipse.jetty.compression</groupId>
8282
<artifactId>jetty-compression-server</artifactId>
8383
</dependency>
84+
<dependency>
85+
<groupId>org.eclipse.jetty.compression</groupId>
86+
<artifactId>jetty-compression-zstandard</artifactId>
87+
</dependency>
88+
<dependency>
89+
<groupId>org.eclipse.jetty.compression</groupId>
90+
<artifactId>jetty-compression-brotli</artifactId>
91+
</dependency>
92+
<dependency>
93+
<groupId>org.eclipse.jetty.compression</groupId>
94+
<artifactId>jetty-compression-gzip</artifactId>
95+
</dependency>
8496
<dependency>
8597
<groupId>jakarta.servlet</groupId>
8698
<artifactId>jakarta.servlet-api</artifactId>
@@ -98,6 +110,10 @@
98110
<groupId>org.junit.jupiter</groupId>
99111
<artifactId>junit-jupiter-api</artifactId>
100112
</dependency>
113+
<dependency>
114+
<groupId>org.junit.jupiter</groupId>
115+
<artifactId>junit-jupiter-params</artifactId>
116+
</dependency>
101117
<dependency>
102118
<groupId>com.google.code.gson</groupId>
103119
<artifactId>gson</artifactId>

maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java

Lines changed: 156 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,17 @@
4343
import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmHelper;
4444
import org.eclipse.aether.spi.connector.transport.http.RFC9457.RFC9457Payload;
4545
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
46+
import org.eclipse.jetty.compression.server.CompressionConfig;
47+
import org.eclipse.jetty.compression.server.CompressionHandler;
4648
import org.eclipse.jetty.http.DateGenerator;
4749
import org.eclipse.jetty.http.HttpField;
50+
import org.eclipse.jetty.http.HttpFields;
4851
import org.eclipse.jetty.http.HttpHeader;
4952
import org.eclipse.jetty.http.HttpMethod;
53+
import org.eclipse.jetty.http.HttpURI;
54+
import org.eclipse.jetty.http.pathmap.MatchedResource;
55+
import org.eclipse.jetty.http.pathmap.PathMappings;
56+
import org.eclipse.jetty.http.pathmap.PathSpec;
5057
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
5158
import org.eclipse.jetty.io.Content;
5259
import org.eclipse.jetty.server.Handler;
@@ -72,12 +79,16 @@ public static class LogEntry {
7279

7380
private final String path;
7481

75-
private final Map<String, String> headers;
82+
private final Map<String, String> requestHeaders;
7683

77-
public LogEntry(String method, String path, Map<String, String> headers) {
84+
private final Map<String, String> responseHeaders;
85+
86+
public LogEntry(
87+
String method, String path, Map<String, String> requestHeaders, Map<String, String> responseHeaders) {
7888
this.method = method;
7989
this.path = path;
80-
this.headers = headers;
90+
this.requestHeaders = requestHeaders;
91+
this.responseHeaders = responseHeaders;
8192
}
8293

8394
public String getMethod() {
@@ -88,8 +99,12 @@ public String getPath() {
8899
return path;
89100
}
90101

91-
public Map<String, String> getHeaders() {
92-
return headers;
102+
public Map<String, String> getRequestHeaders() {
103+
return requestHeaders;
104+
}
105+
106+
public Map<String, String> getResponseHeaders() {
107+
return responseHeaders;
93108
}
94109

95110
@Override
@@ -290,15 +305,15 @@ public HttpServer start() throws Exception {
290305
server = new Server();
291306
httpConnector = new ServerConnector(server);
292307
server.addConnector(httpConnector);
293-
server.setHandler(new Handler.Sequence(
308+
309+
server.setHandler(new LogHandler(new CompressionEnforcingHandler(new Handler.Sequence(
294310
new ConnectionClosingHandler(),
295311
new ServerErrorHandler(),
296-
new LogHandler(),
297312
new ProxyAuthHandler(),
298313
new AuthHandler(),
299314
new RedirectHandler(),
300315
new RepoHandler(),
301-
new RFC9457Handler()));
316+
new RFC9457Handler()))));
302317
server.start();
303318

304319
return this;
@@ -313,6 +328,112 @@ public void stop() throws Exception {
313328
}
314329
}
315330

331+
private class CompressionEnforcingHandler extends CompressionHandler {
332+
// duplicate of CompressionHandler.pathConfigs which is private
333+
private final PathMappings<CompressionConfig> pathConfigs = new PathMappings<>();
334+
335+
CompressionEnforcingHandler(Handler handler) {
336+
super(handler);
337+
this.putConfiguration(
338+
"/br/*",
339+
CompressionConfig.builder().compressIncludeEncoding("br").build());
340+
this.putConfiguration(
341+
"/zstd/*",
342+
CompressionConfig.builder().compressIncludeEncoding("zstd").build());
343+
this.putConfiguration(
344+
"/gzip/*",
345+
CompressionConfig.builder().compressIncludeEncoding("gzip").build());
346+
this.putConfiguration(
347+
"/deflate/*",
348+
CompressionConfig.builder()
349+
.compressIncludeEncoding("deflate")
350+
.build());
351+
}
352+
353+
@Override
354+
public CompressionConfig putConfiguration(PathSpec pathSpec, CompressionConfig config) {
355+
// deliberately not set it in the super class yet
356+
return pathConfigs.put(pathSpec, config);
357+
}
358+
359+
@Override
360+
public boolean handle(Request request, Response response, Callback callback) throws Exception {
361+
Handler next = getHandler();
362+
if (next == null) {
363+
return false;
364+
}
365+
String pathInContext = Request.getPathInContext(request);
366+
MatchedResource<CompressionConfig> matchedConfig = this.pathConfigs.getMatched(pathInContext);
367+
if (matchedConfig == null) {
368+
if (LOGGER.isDebugEnabled()) {
369+
LOGGER.debug("skipping compression: path {} has no matching compression config", pathInContext);
370+
}
371+
// No configuration, skip
372+
return next.handle(request, response, callback);
373+
}
374+
375+
// set the matched config in the super class for further processing, but for all paths
376+
// no need to reset it later as this handler is not used among multiple requests
377+
super.putConfiguration(PathSpec.from("/*"), matchedConfig.getResource());
378+
// first path segment determines the encoding, remove it from the request path for further processing
379+
return super.handle(new StripLeadingPathSegmentsRequestWrapper(request, 1), response, callback);
380+
}
381+
}
382+
383+
private static class StripLeadingPathSegmentsRequestWrapper extends Request.Wrapper {
384+
private final HttpURI modifiedURI;
385+
386+
StripLeadingPathSegmentsRequestWrapper(Request wrapped, int segmentsToStrip) {
387+
super(wrapped);
388+
this.modifiedURI = stripPathSegments(wrapped.getHttpURI(), segmentsToStrip);
389+
}
390+
391+
private static org.eclipse.jetty.http.HttpURI stripPathSegments(
392+
org.eclipse.jetty.http.HttpURI originalURI, int segmentsToStrip) {
393+
if (segmentsToStrip <= 0) {
394+
return originalURI;
395+
}
396+
397+
String originalPath = originalURI.getPath();
398+
if (originalPath == null || originalPath.isEmpty()) {
399+
return originalURI;
400+
}
401+
402+
// Split path into segments
403+
String[] segments = originalPath.split("/");
404+
StringBuilder newPath = new StringBuilder();
405+
406+
// Skip empty first segment (from leading /) and the specified number of segments
407+
int skipCount = 0;
408+
for (int i = 0; i < segments.length; i++) {
409+
if (segments[i].isEmpty() && i == 0) {
410+
// Skip leading empty segment from leading /
411+
continue;
412+
}
413+
if (skipCount < segmentsToStrip) {
414+
skipCount++;
415+
continue;
416+
}
417+
newPath.append("/").append(segments[i]);
418+
}
419+
420+
// If we stripped everything, return root path
421+
if (newPath.isEmpty()) {
422+
newPath.append("/");
423+
}
424+
425+
// Build new URI with modified path
426+
return org.eclipse.jetty.http.HttpURI.build(originalURI)
427+
.path(newPath.toString())
428+
.asImmutable();
429+
}
430+
431+
@Override
432+
public HttpURI getHttpURI() {
433+
return modifiedURI;
434+
}
435+
}
436+
316437
private class ConnectionClosingHandler extends Handler.Abstract {
317438

318439
@Override
@@ -337,22 +458,41 @@ public boolean handle(Request request, Response response, Callback callback) thr
337458
}
338459
}
339460

340-
private class LogHandler extends Handler.Abstract {
461+
private class LogHandler extends Handler.Wrapper {
462+
463+
LogHandler(Handler handler) {
464+
super(handler);
465+
}
466+
341467
@Override
342-
public boolean handle(Request req, Response response, Callback callback) {
468+
public boolean handle(Request req, Response response, Callback callback) throws Exception {
469+
343470
LOGGER.info(
344471
"{} {}{}",
345472
req.getMethod(),
346473
req.getHttpURI().getDecodedPath(),
347474
req.getHttpURI().getQuery() != null ? "?" + req.getHttpURI().getQuery() : "");
348475

349-
Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
350-
for (HttpField header : req.getHeaders()) {
351-
headers.put(header.getName(), header.getValueList().stream().collect(Collectors.joining(", ")));
476+
Map<String, String> requestHeaders =
477+
toUnmodifiableMap(req.getHeaders()); // capture request headers before other handlers modify them
478+
try {
479+
return super.handle(req, response, callback);
480+
} finally {
481+
// capture response headers after other handlers modified them
482+
logEntries.add(new LogEntry(
483+
req.getMethod(),
484+
req.getHttpURI().getPathQuery(),
485+
requestHeaders,
486+
toUnmodifiableMap(response.getHeaders())));
352487
}
353-
logEntries.add(new LogEntry(
354-
req.getMethod(), req.getHttpURI().getPathQuery(), Collections.unmodifiableMap(headers)));
355-
return false;
488+
}
489+
490+
Map<String, String> toUnmodifiableMap(HttpFields headers) {
491+
Map<String, String> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
492+
for (HttpField header : headers) {
493+
map.put(header.getName(), header.getValueList().stream().collect(Collectors.joining(", ")));
494+
}
495+
return Collections.unmodifiableMap(map);
356496
}
357497
}
358498

0 commit comments

Comments
 (0)