Merge remote-tracking branch 'upstream/3.0' into 'upstream/3.1'
Signed-off-by: Maxim Nesen <maxim.nesen@oracle.com>
diff --git a/connectors/jetty11-connector/pom.xml b/connectors/jetty11-connector/pom.xml
index 32b76c4..8862fb8 100644
--- a/connectors/jetty11-connector/pom.xml
+++ b/connectors/jetty11-connector/pom.xml
@@ -36,6 +36,25 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <version>${jetty11.version}</version>
+ </dependency><dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-client</artifactId>
+ <version>${jetty11.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-util</artifactId>
+ <version>${jetty11.version}</version>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
@@ -46,10 +65,6 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
- <exclusion>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-util</artifactId>
- </exclusion>
</exclusions>
</dependency>
<dependency>
diff --git a/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11Connector.java b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11Connector.java
index 24f10fc..beb723d 100644
--- a/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11Connector.java
+++ b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11Connector.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2023 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,43 +16,33 @@
package org.glassfish.jersey.jetty11.connector;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.FilterInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.CookieStore;
-import java.net.URI;
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.core.Configuration;
import jakarta.ws.rs.core.MultivaluedMap;
-
-import javax.net.ssl.SSLContext;
-
+import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpClientTransport;
-import org.eclipse.jetty.client.HttpRequest;
+import org.eclipse.jetty.client.HttpProxy;
+import org.eclipse.jetty.client.ProxyConfiguration;
+import org.eclipse.jetty.client.api.AuthenticationStore;
+import org.eclipse.jetty.client.api.ContentProvider;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
-import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.client.util.BasicAuthentication;
import org.eclipse.jetty.client.util.BytesContentProvider;
import org.eclipse.jetty.client.util.FutureResponseListener;
import org.eclipse.jetty.client.util.OutputStreamContentProvider;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.io.ClientConnector;
+import org.eclipse.jetty.util.HttpCookieStore;
+import org.eclipse.jetty.util.Jetty;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.ClientRequest;
import org.glassfish.jersey.client.ClientResponse;
@@ -65,22 +55,30 @@
import org.glassfish.jersey.message.internal.OutboundMessageContext;
import org.glassfish.jersey.message.internal.Statuses;
-import org.eclipse.jetty.client.HttpClient;
-import org.eclipse.jetty.client.HttpProxy;
-import org.eclipse.jetty.client.ProxyConfiguration;
-import org.eclipse.jetty.client.api.AuthenticationStore;
-import org.eclipse.jetty.client.api.ContentProvider;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.api.Request;
-import org.eclipse.jetty.client.api.Response;
-import org.eclipse.jetty.client.api.Result;
-import org.eclipse.jetty.http.HttpField;
-import org.eclipse.jetty.http.HttpFields;
-import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.util.HttpCookieStore;
-import org.eclipse.jetty.util.Jetty;
-import org.eclipse.jetty.util.ssl.SslContextFactory;
-import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import javax.net.ssl.SSLContext;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CookieStore;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
/**
* A {@link Connector} that utilizes the Jetty HTTP Client to send and receive
@@ -109,7 +107,7 @@
* <pre>
* {@code
* ClientConfig config = new ClientConfig();
- * Connector connector = new JettyConnector(config);
+ * Connector connector = new Jetty11Connector(config);
* config.connector(connector);
* Client client = ClientBuilder.newClient(config);
*
@@ -131,7 +129,7 @@
* @author Arul Dhesiaseelan (aruld at acm.org)
* @author Marek Potociar
*/
-class Jetty11Connector implements Connector {
+public class Jetty11Connector implements Connector {
private static final Logger LOGGER = Logger.getLogger(Jetty11Connector.class.getName());
@@ -146,23 +144,17 @@
* @param jaxrsClient JAX-RS client instance, for which the connector is created.
* @param config client configuration.
*/
- Jetty11Connector(final Client jaxrsClient, final Configuration config) {
+ protected Jetty11Connector(final Client jaxrsClient, final Configuration config) {
this.configuration = config;
- HttpClient httpClient = null;
- if (config.isRegistered(Jetty11HttpClientSupplier.class)) {
- Optional<Object> contract = config.getInstances().stream()
- .filter(a-> Jetty11HttpClientSupplier.class.isInstance(a)).findFirst();
- if (contract.isPresent()) {
- httpClient = ((Jetty11HttpClientSupplier) contract.get()).getHttpClient();
- }
- }
+ HttpClient httpClient = getRegisteredHttpClient(config);
+
if (httpClient == null) {
final SSLContext sslContext = jaxrsClient.getSslContext();
final SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(false);
sslContextFactory.setSslContext(sslContext);
final ClientConnector connector = new ClientConnector();
connector.setSslContextFactory(sslContextFactory);
- final HttpClientTransport transport = new HttpClientTransportOverHTTP(connector);
+ final HttpClientTransport transport = initClientTransport(connector);
httpClient = new HttpClient(transport);
}
this.client = httpClient;
@@ -201,7 +193,7 @@
proxy.ifPresent(clientProxy -> {
final ProxyConfiguration proxyConfig = client.getProxyConfiguration();
final URI u = clientProxy.uri();
- proxyConfig.getProxies().add(new HttpProxy(u.getHost(), u.getPort()));
+ proxyConfig.addProxy(new HttpProxy(u.getHost(), u.getPort()));
if (clientProxy.userName() != null) {
auth.addAuthentication(new BasicAuthentication(u, "<<ANY_REALM>>",
@@ -214,9 +206,9 @@
}
final Object slResponseMaxSize = configuration.getProperties()
- .get(Jetty11ClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE);
+ .get(Jetty11ClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE);
if (slResponseMaxSize != null && slResponseMaxSize instanceof Integer
- && (Integer) slResponseMaxSize > 0) {
+ && (Integer) slResponseMaxSize > 0) {
this.syncListenerResponseMaxSize = Optional.of((Integer) slResponseMaxSize);
}
else {
@@ -232,6 +224,37 @@
}
/**
+ * provides required HTTP client transport for client
+ *
+ * the default transport is {@link HttpClientTransportOverHTTP}
+ *
+ * @return instance of {@link HttpClientTransport}
+ * @since 2.41
+ */
+ protected HttpClientTransport initClientTransport(ClientConnector clientConnector) {
+ return new HttpClientTransportOverHTTP(clientConnector);
+ }
+
+ /**
+ * provides custom registered {@link HttpClient} if any (or NULL)
+ *
+ * @param config configuration where {@link HttpClient} could be registered
+ * @return {@link HttpClient} instance if any was previously registered or NULL
+ *
+ * @since 2.41
+ */
+ protected HttpClient getRegisteredHttpClient(Configuration config) {
+ if (config.isRegistered(Jetty11HttpClientSupplier.class)) {
+ Optional<Object> contract = config.getInstances().stream()
+ .filter(a-> Jetty11HttpClientSupplier.class.isInstance(a)).findFirst();
+ if (contract.isPresent()) {
+ return ((Jetty11HttpClientSupplier) contract.get()).getHttpClient();
+ }
+ }
+ return null;
+ }
+
+ /**
* Get the {@link HttpClient}.
*
* @return the {@link HttpClient}.
@@ -245,7 +268,7 @@
* Get the {@link CookieStore}.
*
* @return the {@link CookieStore} instance or null when
- * JettyClientProperties.DISABLE_COOKIES set to true.
+ * Jetty11ClientProperties.DISABLE_COOKIES set to true.
*/
public CookieStore getCookieStore() {
return cookieStore;
@@ -254,10 +277,13 @@
@Override
public ClientResponse apply(final ClientRequest jerseyRequest) throws ProcessingException {
final Request jettyRequest = translateRequest(jerseyRequest);
- final Map<String, String> clientHeadersSnapshot = writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest);
- final ContentProvider entity = getBytesProvider(jerseyRequest);
+ final Map<String, String> clientHeadersSnapshot = new HashMap<>();
+ final ContentProvider entity =
+ getBytesProvider(jerseyRequest, jerseyRequest.getHeaders(), clientHeadersSnapshot, jettyRequest);
if (entity != null) {
jettyRequest.content(entity);
+ } else {
+ clientHeadersSnapshot.putAll(writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest));
}
try {
@@ -267,12 +293,12 @@
}
else {
final FutureResponseListener listener
- = new FutureResponseListener(jettyRequest, syncListenerResponseMaxSize.get());
+ = new FutureResponseListener(jettyRequest, syncListenerResponseMaxSize.get());
jettyRequest.send(listener);
jettyResponse = listener.get();
}
HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, jerseyRequest.getHeaders(),
- Jetty11Connector.this.getClass().getName(), jerseyRequest.getConfiguration());
+ Jetty11Connector.this.getClass().getName(), jerseyRequest.getConfiguration());
final jakarta.ws.rs.core.Response.StatusType status = jettyResponse.getReason() == null
? Statuses.from(jettyResponse.getStatus())
@@ -338,19 +364,22 @@
private Map<String, String> writeOutBoundHeaders(final MultivaluedMap<String, Object> headers, final Request request) {
final Map<String, String> stringHeaders = HeaderUtils.asStringHeadersSingleValue(headers, configuration);
-
- // remove User-agent header set by Jetty; Jersey already sets this in its request (incl. Jetty version)
- request.headers(httpFields -> httpFields.remove(HttpHeader.USER_AGENT));
- if (request instanceof HttpRequest) {
- final HttpRequest httpRequest = (HttpRequest) request;
+ final Consumer<HttpFields.Mutable> mutableConsumer = httpFields -> {
+ // remove User-agent header set by Jetty; Jersey already sets this in its request (incl. Jetty version)
+ httpFields.remove(HttpHeader.USER_AGENT);
for (final Map.Entry<String, String> e : stringHeaders.entrySet()) {
- httpRequest.addHeader(new HttpField(e.getKey(), e.getValue()));
+ httpFields.put(e.getKey(), e.getValue());
}
- }
+ };
+ request.headers(mutableConsumer);
+
return stringHeaders;
}
- private ContentProvider getBytesProvider(final ClientRequest clientRequest) {
+ private ContentProvider getBytesProvider(final ClientRequest clientRequest,
+ final MultivaluedMap<String, Object> headers,
+ final Map<String, String> snapshot,
+ final Request request) {
final Object entity = clientRequest.getEntity();
if (entity == null) {
@@ -361,6 +390,7 @@
clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {
@Override
public OutputStream getOutputStream(final int contentLength) throws IOException {
+ snapshot.putAll(writeOutBoundHeaders(headers, request));
return outputStream;
}
});
@@ -414,13 +444,13 @@
try {
final CompletableFuture<ClientResponse> responseFuture = new CompletableFuture<ClientResponse>();
responseFuture.whenComplete(
- (clientResponse, throwable) -> {
- if (throwable != null && throwable instanceof CancellationException) {
- // take care of future cancellation
- jettyRequest.abort(throwable);
+ (clientResponse, throwable) -> {
+ if (throwable != null && throwable instanceof CancellationException) {
+ // take care of future cancellation
+ jettyRequest.abort(throwable);
- }
- });
+ }
+ });
final AtomicReference<ClientResponse> jerseyResponse = new AtomicReference<>();
final ByteBufferInputStream entityStream = new ByteBufferInputStream();
@@ -429,7 +459,7 @@
@Override
public void onHeaders(final Response jettyResponse) {
HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, jerseyRequest.getHeaders(),
- Jetty11Connector.this.getClass().getName(), jerseyRequest.getConfiguration());
+ Jetty11Connector.this.getClass().getName(), jerseyRequest.getConfiguration());
if (responseFuture.isDone()) {
if (!callbackInvoked.compareAndSet(false, true)) {
@@ -498,7 +528,7 @@
}
private static ClientResponse translateResponse(final ClientRequest jerseyRequest,
- final Response jettyResponse,
+ final org.eclipse.jetty.client.api.Response jettyResponse,
final NonBlockingInputStream entityStream) {
final ClientResponse jerseyResponse = new ClientResponse(Statuses.from(jettyResponse.getStatus()), jerseyRequest);
processResponseHeaders(jettyResponse.getHeaders(), jerseyResponse);
@@ -519,4 +549,4 @@
throw new ProcessingException("Failed to stop the client.", e);
}
}
-}
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/pom.xml b/connectors/jetty11-http2-connector/pom.xml
new file mode 100644
index 0000000..c3633ed
--- /dev/null
+++ b/connectors/jetty11-http2-connector/pom.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0"?>
+<!--
+
+ Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+
+ This program and the accompanying materials are made available under the
+ terms of the Eclipse Public License v. 2.0, which is available at
+ http://www.eclipse.org/legal/epl-2.0.
+
+ This Source Code may also be made available under the following Secondary
+ Licenses when the conditions for such availability set forth in the
+ Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ version 2 with the GNU Classpath Exception, which is available at
+ https://www.gnu.org/software/classpath/license.html.
+
+ SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.glassfish.jersey.connectors</groupId>
+ <artifactId>project</artifactId>
+ <version>3.1.99-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>jersey-jetty11-http2-connector</artifactId>
+ <packaging>jar</packaging>
+ <name>jersey-connectors-jetty11-http2</name>
+
+ <description>Jersey Client Transport via Jetty 11</description>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <java8.build.outputDirectory>${project.basedir}/target</java8.build.outputDirectory>
+ <java8.sourceDirectory>${project.basedir}/src/main/java8</java8.sourceDirectory>
+ <java11.build.outputDirectory>${project.basedir}/target11</java11.build.outputDirectory>
+ <java11.sourceDirectory>${project.basedir}/src/main/java11</java11.sourceDirectory>
+ </properties>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <version>${jetty11.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-client</artifactId>
+ <version>${jetty11.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-util</artifactId>
+ <version>${jetty11.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.http2</groupId>
+ <artifactId>http2-server</artifactId>
+ <version>${jetty11.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-alpn-conscrypt-server</artifactId>
+ <version>${jetty11.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.http2</groupId>
+ <artifactId>http2-client</artifactId>
+ <version>${jetty11.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.http2</groupId>
+ <artifactId>http2-http-client-transport</artifactId>
+ <version>${jetty11.version}</version>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-client</artifactId>
+ <version>${jetty11.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.http2</groupId>
+ <artifactId>http2-client</artifactId>
+ <version>${jetty11.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.http2</groupId>
+ <artifactId>http2-http-client-transport</artifactId>
+ <version>${jetty11.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-util</artifactId>
+ <version>${jetty11.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish.jersey.connectors</groupId>
+ <artifactId>jersey-jetty11-connector</artifactId>
+ <version>${project.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-client</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish.jersey.media</groupId>
+ <artifactId>jersey-media-jaxb</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish.jersey.containers</groupId>
+ <artifactId>jersey-container-jetty11-http2</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>http2-server</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.media</groupId>
+ <artifactId>jersey-media-json-jackson</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+ <artifactId>jersey-test-framework-provider-jetty11-http2</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.sun.xml.bind</groupId>
+ <artifactId>jaxb-osgi</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.sun.istack</groupId>
+ <artifactId>istack-commons-maven-plugin</artifactId>
+ <inherited>true</inherited>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ <inherited>true</inherited>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <inherited>true</inherited>
+ <configuration>
+ <instructions>
+ <Import-Package>
+ ${jetty.osgi.version},
+ *
+ </Import-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2ClientSupplier.java b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2ClientSupplier.java
new file mode 100644
index 0000000..f13ef3d
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2ClientSupplier.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpClientTransport;
+import org.eclipse.jetty.http2.client.HTTP2Client;
+import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
+import org.glassfish.jersey.jetty11.connector.Jetty11Connector;
+import org.glassfish.jersey.jetty11.connector.Jetty11HttpClientContract;
+import org.glassfish.jersey.jetty11.connector.Jetty11HttpClientSupplier;
+
+/**
+ * HTTP/2 enabled version of the {@link Jetty11HttpClientSupplier}
+ *
+ * @since 2.41
+ */
+public class Jetty11Http2ClientSupplier implements Jetty11HttpClientContract {
+ private final HttpClient http2Client;
+
+ /**
+ * default Http2Client created for the supplier.
+ */
+ public Jetty11Http2ClientSupplier() {
+ this(createHttp2Client());
+ }
+ /**
+ * supplier for the {@code HttpClient} with {@code HttpClientTransportOverHTTP2} to be optionally registered
+ * to a {@link org.glassfish.jersey.client.ClientConfig}
+ * @param http2Client a HttpClient to be supplied when {@link Jetty11Connector#getHttpClient()} is called.
+ */
+ public Jetty11Http2ClientSupplier(HttpClient http2Client) {
+ this.http2Client = http2Client;
+ }
+
+ private static final HttpClient createHttp2Client() {
+ final HttpClientTransport transport = new HttpClientTransportOverHTTP2(new HTTP2Client());
+ return new HttpClient(transport);
+ }
+
+ @Override
+ public HttpClient getHttpClient() {
+ return http2Client;
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2Connector.java b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2Connector.java
new file mode 100644
index 0000000..35dc504
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2Connector.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Configuration;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpClientTransport;
+import org.eclipse.jetty.http2.client.HTTP2Client;
+import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
+import org.eclipse.jetty.io.ClientConnector;
+import org.glassfish.jersey.jetty11.connector.Jetty11Connector;
+
+import java.util.Optional;
+
+/**
+ * Extends {@link Jetty11Connector} with HTTP/2 transport support
+ *
+ * @since 2.41
+ */
+class Jetty11Http2Connector extends Jetty11Connector {
+
+
+ /**
+ * Create the new Jetty HTTP/2 client connector.
+ *
+ * @param jaxrsClient JAX-RS client instance, for which the connector is created.
+ * @param config client configuration.
+ */
+ Jetty11Http2Connector(Client jaxrsClient, Configuration config) {
+ super(jaxrsClient, config);
+ }
+
+ /**
+ * provides required {@link HttpClientTransport} for client
+ *
+ * The overriden method provides {@link HttpClientTransportOverHTTP2} with initialized {@link HTTP2Client}
+ *
+ * @return {@link HttpClientTransportOverHTTP2}
+ * @since 2.41
+ */
+ @Override
+ protected HttpClientTransport initClientTransport(ClientConnector clientConnector) {
+ return new HttpClientTransportOverHTTP2(new HTTP2Client(clientConnector));
+ }
+
+ /**
+ * provides custom registered {@link HttpClient} (if any) with HTTP/2 support
+ *
+ * @param config configuration where {@link HttpClient} could be registered
+ * @return {@link HttpClient} instance if any was previously registered or NULL
+ *
+ * @since 2.41
+ */
+ @Override
+ protected HttpClient getRegisteredHttpClient(Configuration config) {
+ if (config.isRegistered(Jetty11Http2ClientSupplier.class)) {
+ Optional<Object> contract = config.getInstances().stream()
+ .filter(a-> Jetty11Http2ClientSupplier.class.isInstance(a)).findFirst();
+ if (contract.isPresent()) {
+ return ((Jetty11Http2ClientSupplier) contract.get()).getHttpClient();
+ }
+ }
+ return null;
+ }
+}
diff --git a/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2ConnectorProvider.java b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2ConnectorProvider.java
new file mode 100644
index 0000000..a4d02d5
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2ConnectorProvider.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Configurable;
+import jakarta.ws.rs.core.Configuration;
+import org.eclipse.jetty.client.HttpClient;
+import org.glassfish.jersey.client.Initializable;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.jetty11.connector.Jetty11ConnectorProvider;
+import org.glassfish.jersey.jetty11.connector.LocalizationMessages;
+
+/**
+ * Provides HTTP2 enabled version of the {@link Jetty11ConnectorProvider} for a client
+ *
+ * @since 2.41
+ */
+public class Jetty11Http2ConnectorProvider extends Jetty11ConnectorProvider {
+ @Override
+ public Connector getConnector(Client client, Configuration runtimeConfig) {
+ return new Jetty11Http2Connector(client, runtimeConfig);
+ }
+
+ public static HttpClient getHttpClient(Configurable<?> component) {
+ if (!(component instanceof Initializable)) {
+ throw new IllegalArgumentException(
+ LocalizationMessages.INVALID_CONFIGURABLE_COMPONENT_TYPE(component.getClass().getName()));
+ }
+
+ final Initializable<?> initializable = (Initializable<?>) component;
+ Connector connector = initializable.getConfiguration().getConnector();
+ if (connector == null) {
+ initializable.preInitialize();
+ connector = initializable.getConfiguration().getConnector();
+ }
+
+ if (connector instanceof Jetty11Http2Connector) {
+ return ((Jetty11Http2Connector) connector).getHttpClient();
+ }
+
+ throw new IllegalArgumentException(LocalizationMessages.EXPECTED_CONNECTOR_PROVIDER_NOT_USED());
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/package-info.java b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/package-info.java
new file mode 100644
index 0000000..0caf270
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+/**
+ * Jersey HTTP2 client {@link org.glassfish.jersey.client.spi.Connector connector} based on the
+ * Jetty Client.
+ */
+package org.glassfish.jersey.jetty11.http2.connector;
diff --git a/connectors/jetty11-http2-connector/src/main/resources/org/glassfish/jersey/jetty11/http2/connector/localization.properties b/connectors/jetty11-http2-connector/src/main/resources/org/glassfish/jersey/jetty11/http2/connector/localization.properties
new file mode 100644
index 0000000..b219ef9
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/main/resources/org/glassfish/jersey/jetty11/http2/connector/localization.properties
@@ -0,0 +1,21 @@
+#
+# Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Eclipse Public License v. 2.0, which is available at
+# http://www.eclipse.org/legal/epl-2.0.
+#
+# This Source Code may also be made available under the following Secondary
+# Licenses when the conditions for such availability set forth in the
+# Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+# version 2 with the GNU Classpath Exception, which is available at
+# https://www.gnu.org/software/classpath/license.html.
+#
+# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+#
+
+# {0} - HTTP method, e.g. GET, DELETE
+method.not.supported=Method {0} not supported.
+invalid.configurable.component.type=The supplied component "{0}" is not assignable from Jersey11Client or JerseyWebTarget.
+expected.connector.provider.not.used=The supplied component is not configured to use a Jetty11ConnectorProvider.
+not.supported=Jetty connector is not supported on JDK version less than 11.
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AsyncTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AsyncTest.java
new file mode 100644
index 0000000..e938d43
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AsyncTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.container.AsyncResponse;
+import jakarta.ws.rs.container.Suspended;
+import jakarta.ws.rs.container.TimeoutHandler;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class AsyncTest extends JerseyTest {
+ private static final Logger LOGGER = Logger.getLogger(AsyncTest.class.getName());
+ private static final String PATH = "async";
+
+ /**
+ * Asynchronous test resource.
+ */
+ @Path(PATH)
+ public static class AsyncResource {
+ /**
+ * Typical long-running operation duration.
+ */
+ public static final long OPERATION_DURATION = 1000;
+
+ /**
+ * Long-running asynchronous post.
+ *
+ * @param asyncResponse async response.
+ * @param id post request id (received as request payload).
+ */
+ @POST
+ public void asyncPost(@Suspended final AsyncResponse asyncResponse, final String id) {
+ LOGGER.info("Long running post operation called with id " + id + " on thread " + Thread.currentThread().getName());
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ String result = veryExpensiveOperation();
+ asyncResponse.resume(result);
+ }
+
+ private String veryExpensiveOperation() {
+ // ... very expensive operation that typically finishes within 1 seconds, simulated using sleep()
+ try {
+ Thread.sleep(OPERATION_DURATION);
+ return "DONE-" + id;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return "INTERRUPTED-" + id;
+ } finally {
+ LOGGER.info("Long running post operation finished on thread " + Thread.currentThread().getName());
+ }
+ }
+ }, "async-post-runner-" + id).start();
+ }
+
+ /**
+ * Long-running async get request that times out.
+ *
+ * @param asyncResponse async response.
+ */
+ @GET
+ @Path("timeout")
+ public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) {
+ LOGGER.info("Async long-running get with timeout called on thread " + Thread.currentThread().getName());
+ asyncResponse.setTimeoutHandler(new TimeoutHandler() {
+
+ @Override
+ public void handleTimeout(AsyncResponse asyncResponse) {
+ asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE)
+ .entity("Operation time out.").build());
+ }
+ });
+ asyncResponse.setTimeout(1, TimeUnit.SECONDS);
+ asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE)
+ .entity("Operation time out.").build());
+
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ String result = veryExpensiveOperation();
+ asyncResponse.resume(result);
+ }
+
+ private String veryExpensiveOperation() {
+ // very expensive operation that typically finishes within 1 second but can take up to 5 seconds,
+ // simulated using sleep()
+ try {
+ Thread.sleep(5 * OPERATION_DURATION);
+ return "DONE";
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return "INTERRUPTED";
+ } finally {
+ LOGGER.info("Async long-running get with timeout finished on thread " + Thread.currentThread().getName());
+ }
+ }
+ }).start();
+ }
+
+ }
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(AsyncResource.class)
+ .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.HEADERS_ONLY));
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ }
+
+ /**
+ * Test asynchronous POST.
+ *
+ * Send 3 async POST requests and wait to receive the responses. Check the response content and
+ * assert that the operation did not take more than twice as long as a single long operation duration
+ * (this ensures async request execution).
+ *
+ * @throws Exception in case of a test error.
+ */
+ @Test
+ public void testAsyncPost() throws Exception {
+ final long tic = System.currentTimeMillis();
+
+ // Submit requests asynchronously.
+ final Future<Response> rf1 = target(PATH).request().async().post(Entity.text("1"));
+ final Future<Response> rf2 = target(PATH).request().async().post(Entity.text("2"));
+ final Future<Response> rf3 = target(PATH).request().async().post(Entity.text("3"));
+ // get() waits for the response
+ final String r1 = rf1.get().readEntity(String.class);
+ final String r2 = rf2.get().readEntity(String.class);
+ final String r3 = rf3.get().readEntity(String.class);
+
+ final long toc = System.currentTimeMillis();
+
+ assertEquals("DONE-1", r1);
+ assertEquals("DONE-2", r2);
+ assertEquals("DONE-3", r3);
+
+ assertThat("Async processing took too long.", toc - tic, Matchers.lessThan(3 * AsyncResource.OPERATION_DURATION));
+ }
+
+ /**
+ * Test accessing an operation that times out on the server.
+ *
+ * @throws Exception in case of a test error.
+ */
+ @Test
+ public void testAsyncGetWithTimeout() throws Exception {
+ final Future<Response> responseFuture = target(PATH).path("timeout").request().async().get();
+ // Request is being processed asynchronously.
+ final Response response = responseFuture.get();
+
+ // get() waits for the response
+ assertEquals(503, response.getStatus());
+ assertEquals("Operation time out.", response.readEntity(String.class));
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AuthFilterTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AuthFilterTest.java
new file mode 100644
index 0000000..2038901
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AuthFilterTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class AuthFilterTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(AuthFilterTest.class.getName());
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(AuthTest.AuthResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ }
+
+ @Test
+ public void testAuthGetWithClientFilter() {
+ client().register(HttpAuthenticationFeature.basic("name", "password"));
+ Response response = target("test/filter").request().get();
+ assertEquals("GET", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testAuthPostWithClientFilter() {
+ client().register(HttpAuthenticationFeature.basic("name", "password"));
+ Response response = target("test/filter").request().post(Entity.text("POST"));
+ assertEquals("POST", response.readEntity(String.class));
+ }
+
+
+ @Test
+ public void testAuthDeleteWithClientFilter() {
+ client().register(HttpAuthenticationFeature.basic("name", "password"));
+ Response response = target("test/filter").request().delete();
+ assertEquals(204, response.getStatus());
+ }
+
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AuthTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AuthTest.java
new file mode 100644
index 0000000..e594c20
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AuthTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.eclipse.jetty.client.util.BasicAuthentication;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.jetty11.connector.Jetty11ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.inject.Singleton;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class AuthTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(AuthTest.class.getName());
+ private static final String PATH = "test";
+
+ @Path("/test")
+ @Singleton
+ public static class AuthResource {
+
+ int requestCount = 0;
+
+ @GET
+ public String get(@Context HttpHeaders h) {
+ requestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ assertEquals(1, requestCount);
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ } else {
+ assertTrue(requestCount > 1);
+ }
+
+ return "GET";
+ }
+
+ @GET
+ @Path("filter")
+ public String getFilter(@Context HttpHeaders h) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+
+ return "GET";
+ }
+
+ @POST
+ public String post(@Context HttpHeaders h, String e) {
+ requestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ assertEquals(1, requestCount);
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ } else {
+ assertTrue(requestCount > 1);
+ }
+
+ return e;
+ }
+
+ @POST
+ @Path("filter")
+ public String postFilter(@Context HttpHeaders h, String e) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+
+ return e;
+ }
+
+ @DELETE
+ public void delete(@Context HttpHeaders h) {
+ requestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ assertEquals(1, requestCount);
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ } else {
+ assertTrue(requestCount > 1);
+ }
+ }
+
+ @DELETE
+ @Path("filter")
+ public void deleteFilter(@Context HttpHeaders h) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+ }
+
+ @DELETE
+ @Path("filter/withEntity")
+ public String deleteFilterWithEntity(@Context HttpHeaders h, String e) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+
+ return e;
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(AuthResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Test
+ public void testAuthGet() {
+ ClientConfig config = new ClientConfig();
+ config.property(Jetty11ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION,
+ new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password"));
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ Client client = ClientBuilder.newClient(config);
+
+ Response response = client.target(getBaseUri()).path(PATH).request().get();
+ assertEquals("GET", response.readEntity(String.class));
+ client.close();
+ }
+
+ @Test
+ public void testAuthPost() {
+ ClientConfig config = new ClientConfig();
+ config.property(Jetty11ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION,
+ new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password"));
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ Client client = ClientBuilder.newClient(config);
+
+ Response response = client.target(getBaseUri()).path(PATH).request().post(Entity.text("POST"));
+ assertEquals("POST", response.readEntity(String.class));
+ client.close();
+ }
+
+ @Test
+ public void testAuthDelete() {
+ ClientConfig config = new ClientConfig();
+ config.property(Jetty11ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION,
+ new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password"));
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ Client client = ClientBuilder.newClient(config);
+
+ Response response = client.target(getBaseUri()).path(PATH).request().delete();
+ assertEquals(response.getStatus(), 204);
+ client.close();
+ }
+
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/CookieTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/CookieTest.java
new file mode 100644
index 0000000..b5cd635
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/CookieTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.JerseyClient;
+import org.glassfish.jersey.client.JerseyClientBuilder;
+import org.glassfish.jersey.jetty11.connector.Jetty11ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.Cookie;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.NewCookie;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class CookieTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(CookieTest.class.getName());
+
+ @Path("/")
+ public static class CookieResource {
+ @GET
+ public Response get(@Context HttpHeaders h) {
+ Cookie c = h.getCookies().get("name");
+ String e = (c == null) ? "NO-COOKIE" : c.getValue();
+ return Response.ok(e)
+ .cookie(new NewCookie("name", "value")).build();
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(CookieResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Test
+ public void testCookieResource() {
+ ClientConfig config = new ClientConfig();
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ Client client = ClientBuilder.newClient(config);
+ WebTarget r = client.target(getBaseUri());
+
+
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+ assertEquals("value", r.request().get(String.class));
+ client.close();
+ }
+
+ @Test
+ public void testDisabledCookies() {
+ ClientConfig cc = new ClientConfig();
+ cc.property(Jetty11ClientProperties.DISABLE_COOKIES, true);
+ cc.connectorProvider(new Jetty11Http2ConnectorProvider());
+ JerseyClient client = JerseyClientBuilder.createClient(cc);
+ WebTarget r = client.target(getBaseUri());
+
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+
+ final Jetty11Http2Connector connector = (Jetty11Http2Connector) client.getConfiguration().getConnector();
+ if (connector.getCookieStore() != null) {
+ assertTrue(connector.getCookieStore().getCookies().isEmpty());
+ } else {
+ assertNull(connector.getCookieStore());
+ }
+ client.close();
+ }
+
+ @Test
+ public void testCookies() {
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new Jetty11Http2ConnectorProvider());
+ JerseyClient client = JerseyClientBuilder.createClient(cc);
+ WebTarget r = client.target(getBaseUri());
+
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+ assertEquals("value", r.request().get(String.class));
+
+ final Jetty11Http2Connector connector = (Jetty11Http2Connector) client.getConfiguration().getConnector();
+ assertNotNull(connector.getCookieStore().getCookies());
+ assertEquals(1, connector.getCookieStore().getCookies().size());
+ assertEquals("value", connector.getCookieStore().getCookies().get(0).getValue());
+ client.close();
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/CustomLoggingFilter.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/CustomLoggingFilter.java
new file mode 100644
index 0000000..45708b6
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/CustomLoggingFilter.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.client.ClientResponseFilter;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.ContainerResponseContext;
+import jakarta.ws.rs.container.ContainerResponseFilter;
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class CustomLoggingFilter implements ContainerRequestFilter, ContainerResponseFilter,
+ ClientRequestFilter, ClientResponseFilter {
+
+ static int preFilterCalled = 0;
+ static int postFilterCalled = 0;
+
+ @Override
+ public void filter(ClientRequestContext context) throws IOException {
+ System.out.println("CustomLoggingFilter.preFilter called");
+ assertEquals("bar", context.getConfiguration().getProperty("foo"));
+ preFilterCalled++;
+ }
+
+ @Override
+ public void filter(ClientRequestContext context, ClientResponseContext clientResponseContext) throws IOException {
+ System.out.println("CustomLoggingFilter.postFilter called");
+ assertEquals("bar", context.getConfiguration().getProperty("foo"));
+ postFilterCalled++;
+ }
+
+ @Override
+ public void filter(ContainerRequestContext context) throws IOException {
+ System.out.println("CustomLoggingFilter.preFilter called");
+ assertEquals("bar", context.getProperty("foo"));
+ preFilterCalled++;
+ }
+
+ @Override
+ public void filter(ContainerRequestContext context, ContainerResponseContext containerResponseContext) throws IOException {
+ System.out.println("CustomLoggingFilter.postFilter called");
+ assertEquals("bar", context.getProperty("foo"));
+ postFilterCalled++;
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/EntityTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/EntityTest.java
new file mode 100644
index 0000000..9c276e8
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/EntityTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class EntityTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(EntityTest.class.getName());
+
+ private static final String PATH = "test";
+
+ @Path("/test")
+ public static class EntityResource {
+
+ @GET
+ public Person get() {
+ return new Person("John", "Doe");
+ }
+
+ @POST
+ public Person post(Person entity) {
+ return entity;
+ }
+
+ }
+
+ @XmlRootElement
+ public static class Person {
+
+ private String firstName;
+ private String lastName;
+
+ public Person() {
+ }
+
+ public Person(String firstName, String lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ @Override
+ public String toString() {
+ return firstName + " " + lastName;
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(EntityResource.class, JacksonFeature.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Jetty11Http2ConnectorProvider())
+ .register(JacksonFeature.class);
+ }
+
+ @Test
+ public void testGet() {
+ Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).get();
+ Person person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).get();
+ person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ }
+
+ @Test
+ public void testGetAsync() throws ExecutionException, InterruptedException {
+ Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).async().get().get();
+ Person person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).async().get().get();
+ person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ }
+
+ @Test
+ public void testPost() {
+ Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).post(Entity.xml(new Person("John", "Doe")));
+ Person person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).post(Entity.xml(new Person("John", "Doe")));
+ person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ }
+
+ @Test
+ public void testPostAsync() throws ExecutionException, InterruptedException, TimeoutException {
+ Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).async()
+ .post(Entity.xml(new Person("John", "Doe"))).get();
+ Person person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).async().post(Entity.xml(new Person("John", "Doe")))
+ .get();
+ person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/ErrorTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/ErrorTest.java
new file mode 100644
index 0000000..bc534fe
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/ErrorTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.ClientErrorException;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ErrorTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(ErrorTest.class.getName());
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(ErrorResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ }
+
+
+ @Path("/test")
+ public static class ErrorResource {
+ @POST
+ public Response post(String entity) {
+ return Response.serverError().build();
+ }
+
+ @Path("entity")
+ @POST
+ public Response postWithEntity(String entity) {
+ return Response.serverError().entity("error").build();
+ }
+ }
+
+ @Test
+ public void testPostError() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 100; i++) {
+ try {
+ r.request().post(Entity.text("POST"));
+ } catch (ClientErrorException ex) {
+ }
+ }
+ }
+
+ @Test
+ public void testPostErrorWithEntity() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 100; i++) {
+ try {
+ r.request().post(Entity.text("POST"));
+ } catch (ClientErrorException ex) {
+ String s = ex.getResponse().readEntity(String.class);
+ assertEquals("error", s);
+ }
+ }
+ }
+
+ @Test
+ public void testPostErrorAsync() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 100; i++) {
+ try {
+ r.request().async().post(Entity.text("POST"));
+ } catch (ClientErrorException ex) {
+ }
+ }
+ }
+
+ @Test
+ public void testPostErrorWithEntityAsync() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 100; i++) {
+ try {
+ r.request().async().post(Entity.text("POST"));
+ } catch (ClientErrorException ex) {
+ String s = ex.getResponse().readEntity(String.class);
+ assertEquals("error", s);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/FollowRedirectsTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/FollowRedirectsTest.java
new file mode 100644
index 0000000..121a826
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/FollowRedirectsTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientResponse;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.client.ClientResponseFilter;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.UriBuilder;
+import java.io.IOException;
+import java.net.URI;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class FollowRedirectsTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(FollowRedirectsTest.class.getName());
+
+ @Path("/test")
+ public static class RedirectResource {
+ @GET
+ public String get() {
+ return "GET";
+ }
+
+ @GET
+ @Path("redirect")
+ public Response redirect() {
+ return Response.seeOther(UriBuilder.fromResource(RedirectResource.class).build()).build();
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(RedirectResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.property(ClientProperties.FOLLOW_REDIRECTS, false);
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ }
+
+ private static class RedirectTestFilter implements ClientResponseFilter {
+ public static final String RESOLVED_URI_HEADER = "resolved-uri";
+
+ @Override
+ public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
+ if (responseContext instanceof ClientResponse) {
+ ClientResponse clientResponse = (ClientResponse) responseContext;
+ responseContext.getHeaders().putSingle(RESOLVED_URI_HEADER, clientResponse.getResolvedRequestUri().toString());
+ }
+ }
+ }
+
+ @Test
+ public void testDoFollow() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.FOLLOW_REDIRECTS, true);
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ Response r = t.path("test/redirect")
+ .register(RedirectTestFilter.class)
+ .request().get();
+ assertEquals(200, r.getStatus());
+ assertEquals("GET", r.readEntity(String.class));
+ c.close();
+ }
+
+ @Test
+ public void testDoFollowPerRequestOverride() {
+ WebTarget t = target("test/redirect");
+ t.property(ClientProperties.FOLLOW_REDIRECTS, true);
+ Response r = t.request().get();
+ assertEquals(200, r.getStatus());
+ assertEquals("GET", r.readEntity(String.class));
+ }
+
+ @Test
+ public void testDontFollow() {
+ WebTarget t = target("test/redirect");
+ assertEquals(303, t.request().get().getStatus());
+ }
+
+ @Test
+ public void testDontFollowPerRequestOverride() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.FOLLOW_REDIRECTS, true);
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ Client client = ClientBuilder.newClient(config);
+ WebTarget t = client.target(u);
+ t.property(ClientProperties.FOLLOW_REDIRECTS, false);
+ Response r = t.path("test/redirect").request().get();
+ assertEquals(303, r.getStatus());
+ client.close();
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/GZIPContentEncodingTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/GZIPContentEncodingTest.java
new file mode 100644
index 0000000..8139643
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/GZIPContentEncodingTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.message.GZipEncoder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.util.Arrays;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class GZIPContentEncodingTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(EntityTest.class.getName());
+
+ @Path("/")
+ public static class Resource {
+
+ @POST
+ public byte[] post(byte[] content) {
+ return content;
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(Resource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.register(GZipEncoder.class);
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ }
+
+ @Test
+ public void testPost() {
+ WebTarget r = target();
+ byte[] content = new byte[1024 * 1024];
+ assertTrue(Arrays.equals(content,
+ r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class)));
+
+ Response cr = r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE));
+ assertTrue(cr.hasEntity());
+ cr.close();
+ }
+
+ @Test
+ public void testPostChunked() {
+ ClientConfig config = new ClientConfig();
+ config.property(ClientProperties.CHUNKED_ENCODING_SIZE, 1024);
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+
+ Client client = ClientBuilder.newClient(config);
+ WebTarget r = client.target(getBaseUri());
+
+ byte[] content = new byte[1024 * 1024];
+ assertTrue(Arrays.equals(content,
+ r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class)));
+
+ Response cr = r.request().post(Entity.text("POST"));
+ assertTrue(cr.hasEntity());
+ cr.close();
+
+ client.close();
+ }
+
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/HelloWorldTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/HelloWorldTest.java
new file mode 100644
index 0000000..0f674ac
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/HelloWorldTest.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.InvocationCallback;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HelloWorldTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(HelloWorldTest.class.getName());
+ private static final String ROOT_PATH = "helloworld";
+
+ @Path("helloworld")
+ public static class HelloWorldResource {
+ public static final String CLICHED_MESSAGE = "Hello World!";
+
+ @GET
+ @Produces("text/plain")
+ public String getHello() {
+ return CLICHED_MESSAGE;
+ }
+
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HelloWorldResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ }
+
+ @Test
+ public void testConnection() {
+ Response response = target().path(ROOT_PATH).request("text/plain").get();
+ assertEquals(200, response.getStatus());
+ }
+
+ @Test
+ public void testClientStringResponse() {
+ String s = target().path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ }
+
+ @Test
+ public void testAsyncClientRequests() throws InterruptedException {
+ final int REQUESTS = 20;
+ final CountDownLatch latch = new CountDownLatch(REQUESTS);
+ final long tic = System.currentTimeMillis();
+ for (int i = 0; i < REQUESTS; i++) {
+ final int id = i;
+ target().path(ROOT_PATH).request().async().get(new InvocationCallback<Response>() {
+ @Override
+ public void completed(Response response) {
+ try {
+ final String result = response.readEntity(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, result);
+ } finally {
+ latch.countDown();
+ }
+ }
+
+ @Override
+ public void failed(Throwable error) {
+ error.printStackTrace();
+ latch.countDown();
+ }
+ });
+ }
+ latch.await(10 * getAsyncTimeoutMultiplier(), TimeUnit.SECONDS);
+ final long toc = System.currentTimeMillis();
+ Logger.getLogger(HelloWorldTest.class.getName()).info("Executed in: " + (toc - tic));
+ }
+
+ @Test
+ public void testHead() {
+ Response response = target().path(ROOT_PATH).request().head();
+ assertEquals(200, response.getStatus());
+ assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType());
+ }
+
+ @Test
+ public void testFooBarOptions() {
+ Response response = target().path(ROOT_PATH).request().header("Accept", "foo/bar").options();
+ assertEquals(200, response.getStatus());
+ final String allowHeader = response.getHeaderString("Allow");
+ _checkAllowContent(allowHeader);
+ assertEquals("foo/bar", response.getMediaType().toString());
+ assertEquals(0, response.getLength());
+ }
+
+ @Test
+ public void testTextPlainOptions() {
+ Response response = target().path(ROOT_PATH).request().header("Accept", MediaType.TEXT_PLAIN).options();
+ assertEquals(200, response.getStatus());
+ final String allowHeader = response.getHeaderString("Allow");
+ _checkAllowContent(allowHeader);
+ assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType());
+ final String responseBody = response.readEntity(String.class);
+ _checkAllowContent(responseBody);
+ }
+
+ private void _checkAllowContent(final String content) {
+ assertTrue(content.contains("GET"));
+ assertTrue(content.contains("HEAD"));
+ assertTrue(content.contains("OPTIONS"));
+ }
+
+ @Test
+ public void testMissingResourceNotFound() {
+ Response response;
+
+ response = target().path(ROOT_PATH + "arbitrary").request().get();
+ assertEquals(404, response.getStatus());
+ response.close();
+
+ response = target().path(ROOT_PATH).path("arbitrary").request().get();
+ assertEquals(404, response.getStatus());
+ response.close();
+ }
+
+ @Test
+ public void testLoggingFilterClientClass() {
+ Client client = client();
+ client.register(CustomLoggingFilter.class).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target().path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ client.close();
+ }
+
+ @Test
+ public void testLoggingFilterClientInstance() {
+ Client client = client();
+ client.register(new CustomLoggingFilter()).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target().path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ client.close();
+ }
+
+ @Test
+ public void testLoggingFilterTargetClass() {
+ WebTarget target = target().path(ROOT_PATH);
+ target.register(CustomLoggingFilter.class).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target.request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ }
+
+ @Test
+ public void testLoggingFilterTargetInstance() {
+ WebTarget target = target().path(ROOT_PATH);
+ target.register(new CustomLoggingFilter()).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target.request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ }
+
+ @Test
+ public void testConfigurationUpdate() {
+ Client client1 = client();
+ client1.register(CustomLoggingFilter.class).property("foo", "bar");
+
+ Client client = ClientBuilder.newClient(client1.getConfiguration());
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target().path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ client.close();
+ }
+
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/Http2PresenceTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/Http2PresenceTest.java
new file mode 100644
index 0000000..df7da3f
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/Http2PresenceTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.spi.ConnectorProvider;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HeaderParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/**
+ * Tests the HTTP2 presence.
+ *
+ */
+public class Http2PresenceTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(Http2PresenceTest.class.getName());
+
+ @Path("/test")
+ public static class HttpMethodResource {
+ @POST
+ public String post(
+ @HeaderParam("Transfer-Encoding") String transferEncoding,
+ @HeaderParam("X-CLIENT") String xClient,
+ @HeaderParam("X-WRITER") String xWriter,
+ String entity) {
+ assertEquals("client", xClient);
+ return "POST";
+ }
+
+ @GET
+ public String testUserAgent(@Context HttpHeaders httpHeaders) {
+ final List<String> requestHeader = httpHeaders.getRequestHeader(HttpHeaders.USER_AGENT);
+ if (requestHeader.size() != 1) {
+ return "FAIL";
+ }
+ return requestHeader.get(0);
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ }
+
+ @Test
+ public void testPost() {
+ Response response = target().path("test").request().header("X-CLIENT", "client").post(null);
+
+ assertEquals(200, response.getStatus());
+ assertTrue(response.hasEntity());
+ }
+
+ @Test
+ public void testHttp2Presence() {
+ final ConnectorProvider provider = ((ClientConfig) target().getConfiguration()).getConnectorProvider();
+ assertTrue(provider instanceof Jetty11Http2ConnectorProvider);
+
+ final HttpClient client = ((Jetty11Http2ConnectorProvider) provider).getHttpClient(target());
+ assertTrue(client.getTransport() instanceof HttpClientTransportOverHTTP2);
+ }
+
+ /**
+ * Test, that {@code User-agent} header is as set by Jersey, not by underlying Jetty client.
+ */
+ @Test
+ public void testUserAgent() {
+ String response = target().path("test").request().get(String.class);
+ assertTrue(response.startsWith("Jersey"), "User-agent header should start with 'Jersey', but was " + response);
+ }
+}
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/HttpHeadersTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/HttpHeadersTest.java
new file mode 100644
index 0000000..9cdd81e
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/HttpHeadersTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HeaderParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HttpHeadersTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(HttpHeadersTest.class.getName());
+
+ @Path("/test")
+ public static class HttpMethodResource {
+ @POST
+ public String post(
+ @HeaderParam("Transfer-Encoding") String transferEncoding,
+ @HeaderParam("X-CLIENT") String xClient,
+ @HeaderParam("X-WRITER") String xWriter,
+ String entity) {
+ assertEquals("client", xClient);
+ return "POST";
+ }
+
+ @GET
+ public String testUserAgent(@Context HttpHeaders httpHeaders) {
+ final List<String> requestHeader = httpHeaders.getRequestHeader(HttpHeaders.USER_AGENT);
+ if (requestHeader.size() != 1) {
+ return "FAIL";
+ }
+ return requestHeader.get(0);
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ }
+
+ @Test
+ public void testPost() {
+ Response response = target().path("test").request().header("X-CLIENT", "client").post(null);
+
+ assertEquals(200, response.getStatus());
+ assertTrue(response.hasEntity());
+ }
+
+ /**
+ * Test, that {@code User-agent} header is as set by Jersey, not by underlying Jetty client.
+ */
+ @Test
+ public void testUserAgent() {
+ String response = target().path("test").request().get(String.class);
+ assertTrue(response.startsWith("Jersey"), "User-agent header should start with 'Jersey', but was " + response);
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/ManagedClientTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/ManagedClientTest.java
new file mode 100644
index 0000000..e5ba7d3
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/ManagedClientTest.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ClientBinding;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.Uri;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.DynamicFeature;
+import jakarta.ws.rs.container.ResourceInfo;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.FeatureContext;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ManagedClientTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(ManagedClientTest.class.getName());
+
+ /**
+ * Managed client configuration for client A.
+ */
+ @ClientBinding(configClass = MyClientAConfig.class)
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.FIELD, ElementType.PARAMETER})
+ public static @interface ClientA {
+ }
+
+ /**
+ * Managed client configuration for client B.
+ */
+ @ClientBinding(configClass = MyClientBConfig.class)
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.FIELD, ElementType.PARAMETER})
+ public @interface ClientB {
+ }
+
+ /**
+ * Dynamic feature that appends a properly configured {@link CustomHeaderFilter} instance
+ * to every method that is annotated with {@link Require @Require} internal feature
+ * annotation.
+ */
+ public static class CustomHeaderFeature implements DynamicFeature {
+
+ /**
+ * A method annotation to be placed on those resource methods to which a validating
+ * {@link CustomHeaderFilter} instance should be added.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Documented
+ @Target(ElementType.METHOD)
+ public static @interface Require {
+
+ /**
+ * Expected custom header name to be validated by the {@link CustomHeaderFilter}.
+ */
+ public String headerName();
+
+ /**
+ * Expected custom header value to be validated by the {@link CustomHeaderFilter}.
+ */
+ public String headerValue();
+ }
+
+ @Override
+ public void configure(ResourceInfo resourceInfo, FeatureContext context) {
+ final Require va = resourceInfo.getResourceMethod().getAnnotation(Require.class);
+ if (va != null) {
+ context.register(new CustomHeaderFilter(va.headerName(), va.headerValue()));
+ }
+ }
+ }
+
+ /**
+ * A filter for appending and validating custom headers.
+ * <p>
+ * On the client side, appends a new custom request header with a configured name and value to each outgoing request.
+ * </p>
+ * <p>
+ * On the server side, validates that each request has a custom header with a configured name and value.
+ * If the validation fails a HTTP 403 response is returned.
+ * </p>
+ */
+ public static class CustomHeaderFilter implements ContainerRequestFilter, ClientRequestFilter {
+
+ private final String headerName;
+ private final String headerValue;
+
+ public CustomHeaderFilter(String headerName, String headerValue) {
+ if (headerName == null || headerValue == null) {
+ throw new IllegalArgumentException("Header name and value must not be null.");
+ }
+ this.headerName = headerName;
+ this.headerValue = headerValue;
+ }
+
+ @Override
+ public void filter(ContainerRequestContext ctx) throws IOException { // validate
+ if (!headerValue.equals(ctx.getHeaderString(headerName))) {
+ ctx.abortWith(Response.status(Response.Status.FORBIDDEN)
+ .type(MediaType.TEXT_PLAIN)
+ .entity(String
+ .format("Expected header '%s' not present or value not equal to '%s'", headerName, headerValue))
+ .build());
+ }
+ }
+
+ @Override
+ public void filter(ClientRequestContext ctx) throws IOException { // append
+ ctx.getHeaders().putSingle(headerName, headerValue);
+ }
+ }
+
+ /**
+ * Internal resource accessed from the managed client resource.
+ */
+ @Path("internal")
+ public static class InternalResource {
+
+ @GET
+ @Path("a")
+ @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "a")
+ public String getA() {
+ return "a";
+ }
+
+ @GET
+ @Path("b")
+ @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "b")
+ public String getB() {
+ return "b";
+ }
+ }
+
+ /**
+ * A resource that uses managed clients to retrieve values of internal
+ * resources 'A' and 'B', which are protected by a {@link CustomHeaderFilter}
+ * and require a specific custom header in a request to be set to a specific value.
+ * <p>
+ * Properly configured managed clients have a {@code CustomHeaderFilter} instance
+ * configured to insert the {@link CustomHeaderFeature.Require required} custom header
+ * with a proper value into the outgoing client requests.
+ * </p>
+ */
+ @Path("public")
+ public static class PublicResource {
+
+ @Uri("a")
+ @ClientA // resolves to <base>/internal/a
+ private WebTarget targetA;
+
+ @GET
+ @Produces("text/plain")
+ @Path("a")
+ public String getTargetA() {
+ return targetA.request(MediaType.TEXT_PLAIN).get(String.class);
+ }
+
+ @GET
+ @Produces("text/plain")
+ @Path("b")
+ public Response getTargetB(@Uri("internal/b") @ClientB WebTarget targetB) {
+ return targetB.request(MediaType.TEXT_PLAIN).get();
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(PublicResource.class, InternalResource.class, CustomHeaderFeature.class)
+ .property(ClientA.class.getName() + ".baseUri", this.getBaseUri().toString() + "internal");
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ public static class MyClientAConfig extends ClientConfig {
+
+ public MyClientAConfig() {
+ this.register(new CustomHeaderFilter("custom-header", "a"));
+ }
+ }
+
+ public static class MyClientBConfig extends ClientConfig {
+
+ public MyClientBConfig() {
+ this.register(new CustomHeaderFilter("custom-header", "b"));
+ }
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ }
+
+ /**
+ * Test that a connection via managed clients works properly.
+ *
+ * @throws Exception in case of test failure.
+ */
+ @Test
+ public void testManagedClient() throws Exception {
+ final WebTarget resource = target().path("public").path("{name}");
+ Response response;
+
+ response = resource.resolveTemplate("name", "a").request(MediaType.TEXT_PLAIN).get();
+ assertEquals(200, response.getStatus());
+ assertEquals("a", response.readEntity(String.class));
+
+ response = resource.resolveTemplate("name", "b").request(MediaType.TEXT_PLAIN).get();
+ assertEquals(200, response.getStatus());
+ assertEquals("b", response.readEntity(String.class));
+ }
+
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/MethodTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/MethodTest.java
new file mode 100644
index 0000000..2dabed5
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/MethodTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PATCH;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class MethodTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(MethodTest.class.getName());
+
+ private static final String PATH = "test";
+
+ @Path("/test")
+ public static class HttpMethodResource {
+ @GET
+ public String get() {
+ return "GET";
+ }
+
+ @POST
+ public String post(String entity) {
+ return entity;
+ }
+
+ @PUT
+ public String put(String entity) {
+ return entity;
+ }
+
+ @PATCH
+ public String patch(String entity) {
+ return entity;
+ }
+
+ @DELETE
+ public String delete() {
+ return "DELETE";
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ }
+
+ @Test
+ public void testGet() {
+ Response response = target(PATH).request().get();
+ assertEquals("GET", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testGetAsync() throws ExecutionException, InterruptedException {
+ Response response = target(PATH).request().async().get().get();
+ assertEquals("GET", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testPost() {
+ Response response = target(PATH).request().post(Entity.entity("POST", MediaType.TEXT_PLAIN));
+ assertEquals("POST", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testPostAsync() throws ExecutionException, InterruptedException {
+ Response response = target(PATH).request().async().post(Entity.entity("POST", MediaType.TEXT_PLAIN)).get();
+ assertEquals("POST", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testPut() {
+ Response response = target(PATH).request().put(Entity.entity("PUT", MediaType.TEXT_PLAIN));
+ assertEquals("PUT", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testPutAsync() throws ExecutionException, InterruptedException {
+ Response response = target(PATH).request().async().put(Entity.entity("PUT", MediaType.TEXT_PLAIN)).get();
+ assertEquals("PUT", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testDelete() {
+ Response response = target(PATH).request().delete();
+ assertEquals("DELETE", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testDeleteAsync() throws ExecutionException, InterruptedException {
+ Response response = target(PATH).request().async().delete().get();
+ assertEquals("DELETE", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testPatch() {
+ Response response = target(PATH).request().method("PATCH", Entity.entity("PATCH", MediaType.TEXT_PLAIN));
+ assertEquals("PATCH", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testOptionsWithEntity() {
+ Response response = target(PATH).request().build("OPTIONS", Entity.text("OPTIONS")).invoke();
+ assertEquals(200, response.getStatus());
+ response.close();
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/NoEntityTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/NoEntityTest.java
new file mode 100644
index 0000000..3414f2c
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/NoEntityTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Logger;
+
+public class NoEntityTest extends JerseyTest {
+ private static final Logger LOGGER = Logger.getLogger(NoEntityTest.class.getName());
+
+ @Path("/test")
+ public static class HttpMethodResource {
+ @GET
+ public Response get() {
+ return Response.status(Response.Status.CONFLICT).build();
+ }
+
+ @POST
+ public void post(String entity) {
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ }
+
+ @Test
+ public void testGet() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().get();
+ cr.close();
+ }
+ }
+
+ @Test
+ public void testGetWithClose() {
+ WebTarget r = target("test");
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().get();
+ cr.close();
+ }
+ }
+
+ @Test
+ public void testPost() {
+ WebTarget r = target("test");
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().post(null);
+ }
+ }
+
+ @Test
+ public void testPostWithClose() {
+ WebTarget r = target("test");
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().post(null);
+ cr.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/SyncResponseSizeTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/SyncResponseSizeTest.java
new file mode 100644
index 0000000..159738e
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/SyncResponseSizeTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.jetty11.connector.Jetty11ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.net.URI;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class SyncResponseSizeTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(SyncResponseSizeTest.class.getName());
+
+ private static final int maxBufferSize = 4 * 1024 * 1024; //4 MiB
+
+ @Path("/test")
+ public static class TimeoutResource {
+
+ private static final byte[] data = new byte[maxBufferSize];
+
+ static {
+ Byte b = "a".getBytes()[0];
+ for (int i = 0; i < maxBufferSize; i++) data[i] = b.byteValue();
+ }
+
+ @GET
+ @Path("/small")
+ public String getSmall() {
+ return "GET";
+ }
+
+ @GET
+ @Path("/big")
+ public String getBig() {
+ return new String(data);
+ }
+
+ @GET
+ @Path("/verybig")
+ public String getVeryBig() {
+ return new String(data) + "a";
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(TimeoutResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ }
+
+ @Test
+ public void testDefaultSmall() {
+ Response r = target("test/small").request().get();
+ assertEquals(200, r.getStatus());
+ assertEquals("GET", r.readEntity(String.class));
+ }
+
+ @Test
+ public void testDefaultTooBig() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ try {
+ t.path("test/big").request().get();
+ fail("Exception expected.");
+ } catch (ProcessingException e) {
+ // Buffering capacity ... exceeded.
+ assertTrue(ExecutionException.class.isInstance(e.getCause()));
+ assertTrue(IllegalArgumentException.class.isInstance(e.getCause().getCause()));
+ } finally {
+ c.close();
+ }
+ }
+
+ @Test
+ public void testCustomBig() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ config.property(Jetty11ClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE, maxBufferSize);
+
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ try {
+ Response r = t.path("test/big").request().get();
+ String p = r.readEntity(String.class);
+ assertEquals(p.length(), maxBufferSize);
+ } catch (ProcessingException e) {
+ assertThat("Unexpected processing exception cause",
+ e.getCause(), instanceOf(TimeoutException.class));
+ } finally {
+ c.close();
+ }
+ }
+
+ @Test
+ public void testCustomTooBig() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ config.property(Jetty11ClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE, maxBufferSize);
+
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ try {
+ t.path("test/verybig").request().get();
+ fail("Exception expected.");
+ } catch (ProcessingException e) {
+ // Buffering capacity ... exceeded.
+ assertTrue(ExecutionException.class.isInstance(e.getCause()));
+ assertTrue(IllegalArgumentException.class.isInstance(e.getCause().getCause()));
+ } finally {
+ c.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/TimeoutTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/TimeoutTest.java
new file mode 100644
index 0000000..e3745ef
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/TimeoutTest.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.glassfish.jersey.CommonProperties;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.jetty11.connector.Jetty11ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.StreamingOutput;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class TimeoutTest extends JerseyTest {
+ private static final Logger LOGGER = Logger.getLogger(TimeoutTest.class.getName());
+
+ @Path("/test")
+ public static class TimeoutResource {
+ @GET
+ public String get() {
+ return "GET";
+ }
+
+ @GET
+ @Path("timeout")
+ public String getTimeout() {
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ return "GET";
+ }
+
+ /**
+ * Long-running streaming request
+ *
+ * @param count number of packets send
+ * @param pauseMillis pause between each packets
+ */
+ @GET
+ @Path("stream")
+ public Response streamsWithDelay(@QueryParam("start") @DefaultValue("0") int startMillis, @QueryParam("count") int count,
+ @QueryParam("pauseMillis") int pauseMillis) {
+ StreamingOutput streamingOutput = streamSlowly(startMillis, count, pauseMillis);
+
+ return Response.ok(streamingOutput)
+ .build();
+ }
+ }
+
+ private static StreamingOutput streamSlowly(int startMillis, int count, int pauseMillis) {
+
+ return output -> {
+ try {
+ TimeUnit.MILLISECONDS.sleep(startMillis);
+ }
+ catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ output.write("begin\n".getBytes(StandardCharsets.UTF_8));
+ output.flush();
+ for (int i = 0; i < count; i++) {
+ try {
+ TimeUnit.MILLISECONDS.sleep(pauseMillis);
+ }
+ catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ output.write(("message " + i + "\n").getBytes(StandardCharsets.UTF_8));
+ output.flush();
+ }
+ output.write("end".getBytes(StandardCharsets.UTF_8));
+ };
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(TimeoutResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ }
+
+ @Test
+ public void testFast() {
+ Response r = target("test").request().get();
+ assertEquals(200, r.getStatus());
+ assertEquals("GET", r.readEntity(String.class));
+ }
+
+ @Test
+ public void testSlow() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ try {
+ t.path("test/timeout").request().get();
+ fail("Timeout expected.");
+ } catch (ProcessingException e) {
+ assertThat("Unexpected processing exception cause",
+ e.getCause(), instanceOf(TimeoutException.class));
+ } finally {
+ c.close();
+ }
+ }
+
+ @Test
+ public void testTimeoutInRequest() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig();
+ config.connectorProvider(new Jetty11Http2ConnectorProvider());
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ try {
+ t.path("test/timeout").request().property(ClientProperties.READ_TIMEOUT, 1_000).get();
+ fail("Timeout expected.");
+ } catch (ProcessingException e) {
+ assertThat("Unexpected processing exception cause",
+ e.getCause(), instanceOf(TimeoutException.class));
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Test accessing an operation that is streaming slowly
+ *
+ * @throws ProcessingException in case of a test error.
+ */
+ @Test
+ public void testSlowlyStreamedContentDoesNotReadTimeout() throws Exception {
+
+ int count = 5;
+ int pauseMillis = 50;
+
+ final Response response = target("test")
+ .property(ClientProperties.READ_TIMEOUT, 100L)
+ .property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1")
+ .path("stream")
+ .queryParam("count", count)
+ .queryParam("pauseMillis", pauseMillis)
+ .request().get();
+
+ assertTrue(response.readEntity(String.class).contains("end"));
+ }
+
+ @Test
+ public void testSlowlyStreamedContentDoesTotalTimeout() throws Exception {
+
+ int count = 5;
+ int pauseMillis = 50;
+
+ try {
+ target("test")
+ .property(Jetty11ClientProperties.TOTAL_TIMEOUT, 100L)
+ .property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1")
+ .path("stream")
+ .queryParam("count", count)
+ .queryParam("pauseMillis", pauseMillis)
+ .request().get();
+
+ fail("This operation should trigger total timeout");
+ } catch (ProcessingException e) {
+ assertEquals(TimeoutException.class, e.getCause().getClass());
+ }
+ }
+
+ /**
+ * Test accessing an operation that is streaming slowly
+ *
+ * @throws ProcessingException in case of a test error.
+ */
+ @Test
+ public void testSlowToStartStreamedContentDoesReadTimeout() throws Exception {
+
+ int start = 150;
+ int count = 5;
+ int pauseMillis = 50;
+
+ try {
+ target("test")
+ .property(ClientProperties.READ_TIMEOUT, 100L)
+ .property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1")
+ .path("stream")
+ .queryParam("start", start)
+ .queryParam("count", count)
+ .queryParam("pauseMillis", pauseMillis)
+ .request().get();
+ fail("This operation should trigger idle timeout");
+ } catch (ProcessingException e) {
+ assertEquals(TimeoutException.class, e.getCause().getClass());
+ }
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/TraceSupportTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/TraceSupportTest.java
new file mode 100644
index 0000000..815167e
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/TraceSupportTest.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.process.Inflector;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.model.Resource;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Request;
+import jakarta.ws.rs.core.Response;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class TraceSupportTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(TraceSupportTest.class.getName());
+
+ /**
+ * Programmatic tracing root resource path.
+ */
+ public static final String ROOT_PATH_PROGRAMMATIC = "tracing/programmatic";
+
+ /**
+ * Annotated class-based tracing root resource path.
+ */
+ public static final String ROOT_PATH_ANNOTATED = "tracing/annotated";
+
+ @HttpMethod(TRACE.NAME)
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface TRACE {
+ public static final String NAME = "TRACE";
+ }
+
+ @Path(ROOT_PATH_ANNOTATED)
+ public static class TracingResource {
+
+ @TRACE
+ @Produces("text/plain")
+ public String trace(Request request) {
+ return stringify((ContainerRequest) request);
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(TracingResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ final Resource.Builder resourceBuilder = Resource.builder(ROOT_PATH_PROGRAMMATIC);
+ resourceBuilder.addMethod(TRACE.NAME).handledBy(new Inflector<ContainerRequestContext, Response>() {
+
+ @Override
+ public Response apply(ContainerRequestContext request) {
+ if (request == null) {
+ return Response.noContent().build();
+ } else {
+ return Response.ok(stringify((ContainerRequest) request), MediaType.TEXT_PLAIN).build();
+ }
+ }
+ });
+
+ return config.registerResources(resourceBuilder.build());
+
+ }
+
+ private String[] expectedFragmentsProgrammatic = new String[]{
+ "TRACE http://localhost:" + this.getPort() + "/tracing/programmatic"
+ };
+ private String[] expectedFragmentsAnnotated = new String[]{
+ "TRACE http://localhost:" + this.getPort() + "/tracing/annotated"
+ };
+
+ private WebTarget prepareTarget(String path) {
+ final WebTarget target = target();
+ target.register(LoggingFeature.class);
+ return target.path(path);
+ }
+
+ @Test
+ public void testProgrammaticApp() throws Exception {
+ Response response = prepareTarget(ROOT_PATH_PROGRAMMATIC).request("text/plain").method(TRACE.NAME);
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode());
+
+ String responseEntity = response.readEntity(String.class);
+ for (String expectedFragment : expectedFragmentsProgrammatic) {
+ assertTrue(// toLowerCase - http header field names are case insensitive
+ responseEntity.contains(expectedFragment),
+ "Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity);
+ }
+ }
+
+ @Test
+ public void testAnnotatedApp() throws Exception {
+ Response response = prepareTarget(ROOT_PATH_ANNOTATED).request("text/plain").method(TRACE.NAME);
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode());
+
+ String responseEntity = response.readEntity(String.class);
+ for (String expectedFragment : expectedFragmentsAnnotated) {
+ assertTrue(// toLowerCase - http header field names are case insensitive
+ responseEntity.contains(expectedFragment),
+ "Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity);
+ }
+ }
+
+ @Test
+ public void testTraceWithEntity() throws Exception {
+ _testTraceWithEntity(false, false);
+ }
+
+ @Test
+ public void testAsyncTraceWithEntity() throws Exception {
+ _testTraceWithEntity(true, false);
+ }
+
+ @Test
+ public void testTraceWithEntityJettyConnector() throws Exception {
+ _testTraceWithEntity(false, true);
+ }
+
+ @Test
+ public void testAsyncTraceWithEntityJettyConnector() throws Exception {
+ _testTraceWithEntity(true, true);
+ }
+
+ private void _testTraceWithEntity(final boolean isAsync, final boolean useJettyConnection) throws Exception {
+ try {
+ WebTarget target = useJettyConnection ? getJettyClient().target(target().getUri()) : target();
+ target = target.path(ROOT_PATH_ANNOTATED);
+
+ final Entity<String> entity = Entity.entity("trace", MediaType.WILDCARD_TYPE);
+
+ Response response;
+ if (!isAsync) {
+ response = target.request().method(TRACE.NAME, entity);
+ } else {
+ response = target.request().async().method(TRACE.NAME, entity).get();
+ }
+
+ fail("A TRACE request MUST NOT include an entity. (response=" + response + ")");
+ } catch (Exception e) {
+ // OK
+ }
+ }
+
+ private Client getJettyClient() {
+ return ClientBuilder.newClient(new ClientConfig().connectorProvider(new Jetty11Http2ConnectorProvider()));
+ }
+
+
+ public static String stringify(ContainerRequest request) {
+ StringBuilder buffer = new StringBuilder();
+
+ printRequestLine(buffer, request);
+ printPrefixedHeaders(buffer, request.getHeaders());
+
+ if (request.hasEntity()) {
+ buffer.append(request.readEntity(String.class)).append("\n");
+ }
+
+ return buffer.toString();
+ }
+
+ private static void printRequestLine(StringBuilder buffer, ContainerRequest request) {
+ buffer.append(request.getMethod()).append(" ").append(request.getUriInfo().getRequestUri().toASCIIString()).append("\n");
+ }
+
+ private static void printPrefixedHeaders(StringBuilder buffer, Map<String, List<String>> headers) {
+ for (Map.Entry<String, List<String>> e : headers.entrySet()) {
+ List<String> val = e.getValue();
+ String header = e.getKey();
+
+ if (val.size() == 1) {
+ buffer.append(header).append(": ").append(val.get(0)).append("\n");
+ } else {
+ StringBuilder sb = new StringBuilder();
+ boolean add = false;
+ for (String s : val) {
+ if (add) {
+ sb.append(',');
+ }
+ add = true;
+ sb.append(s);
+ }
+ buffer.append(header).append(": ").append(sb.toString()).append("\n");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/UnderlyingHttpClientAccessTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/UnderlyingHttpClientAccessTest.java
new file mode 100644
index 0000000..b56b536
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/UnderlyingHttpClientAccessTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.jetty11.http2.connector;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.glassfish.jersey.client.ClientConfig;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+public class UnderlyingHttpClientAccessTest {
+
+ /**
+ * Verifier of JERSEY-2424 fix.
+ */
+ @Test
+ public void testHttpClientInstanceAccess() {
+ final Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new Jetty11Http2ConnectorProvider()));
+ final HttpClient hcOnClient = Jetty11Http2ConnectorProvider.getHttpClient(client);
+ // important: the web target instance in this test must be only created AFTER the client has been pre-initialized
+ // (see org.glassfish.jersey.client.Initializable.preInitialize method). This is here achieved by calling the
+ // connector provider's static getHttpClient method above.
+ final WebTarget target = client.target("http://localhost/");
+ final HttpClient hcOnTarget = Jetty11Http2ConnectorProvider.getHttpClient(target);
+
+ assertNotNull(hcOnClient, "HTTP client instance set on JerseyClient should not be null.");
+ assertNotNull(hcOnTarget, "HTTP client instance set on JerseyWebTarget should not be null.");
+ assertSame(hcOnClient, hcOnTarget, "HTTP client instance set on JerseyClient should be the same instance as the one "
+ + "set on JerseyWebTarget (provided the target instance has not been further configured).");
+ }
+
+ @Test
+ public void testGetProvidedClientInstance() {
+ final HttpClient httpClient = new HttpClient();
+ final ClientConfig clientConfig = new ClientConfig()
+ .connectorProvider(new Jetty11Http2ConnectorProvider())
+ .register(new Jetty11Http2ClientSupplier(httpClient));
+ final Client client = ClientBuilder.newClient(clientConfig);
+ final WebTarget target = client.target("http://localhost/");
+ final HttpClient hcOnTarget = Jetty11Http2ConnectorProvider.getHttpClient(target);
+
+ assertThat("Instance provided to a ClientConfig differs from instance provided by JettyProvider",
+ httpClient, is(hcOnTarget));
+ }
+}
\ No newline at end of file
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java
new file mode 100644
index 0000000..471321f
--- /dev/null
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.netty.connector;
+
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpVersion;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.RequestEntityProcessing;
+import org.glassfish.jersey.client.internal.ConnectorExtension;
+
+import java.io.IOException;
+import java.net.ProtocolException;
+
+class Expect100ContinueConnectorExtension
+ implements ConnectorExtension<HttpRequest, IOException> {
+ private static final String EXCEPTION_MESSAGE = "Server rejected operation";
+ @Override
+ public void invoke(ClientRequest request, HttpRequest extensionParam) {
+
+ final long length = request.getLengthLong();
+ final RequestEntityProcessing entityProcessing = request.resolveProperty(
+ ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.class);
+
+ final Boolean expectContinueActivated = request.resolveProperty(
+ ClientProperties.EXPECT_100_CONTINUE, Boolean.class);
+ final Long expectContinueSizeThreshold = request.resolveProperty(
+ ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE,
+ ClientProperties.DEFAULT_EXPECT_100_CONTINUE_THRESHOLD_SIZE);
+
+ final boolean allowStreaming = length > expectContinueSizeThreshold
+ || entityProcessing == RequestEntityProcessing.CHUNKED;
+
+ if (extensionParam.protocolVersion().equals(HttpVersion.HTTP_1_0)
+ || !Boolean.TRUE.equals(expectContinueActivated)
+ || !request.hasEntity()
+ || !allowStreaming) {
+ return;
+ }
+ extensionParam.headers().add(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE);
+
+ }
+
+ @Override
+ public void postConnectionProcessing(HttpRequest extensionParam) {
+ }
+
+ @Override
+ public boolean handleException(ClientRequest request, HttpRequest extensionParam, IOException ex) {
+ final Boolean expectContinueActivated = request.resolveProperty(
+ ClientProperties.EXPECT_100_CONTINUE, Boolean.FALSE);
+
+ return expectContinueActivated
+ && (ex instanceof ProtocolException && ex.getMessage().equals(EXCEPTION_MESSAGE));
+ }
+}
\ No newline at end of file
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java
index d460867..4a9836d 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java
@@ -20,17 +20,24 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
-import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.ClientRequest;
import org.glassfish.jersey.client.ClientResponse;
+import org.glassfish.jersey.http.HttpHeaders;
+import org.glassfish.jersey.http.ResponseStatus;
import org.glassfish.jersey.netty.connector.internal.NettyInputStream;
import org.glassfish.jersey.netty.connector.internal.RedirectException;
@@ -38,13 +45,12 @@
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.HttpContent;
-import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpResponse;
-import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.timeout.IdleStateEvent;
+import org.glassfish.jersey.uri.internal.JerseyUriBuilder;
/**
* Jersey implementation of Netty channel handler.
@@ -103,17 +109,27 @@
jerseyResponse = null;
int responseStatus = cr.getStatus();
if (followRedirects
- && (responseStatus == HttpResponseStatus.MOVED_PERMANENTLY.code()
- || responseStatus == HttpResponseStatus.FOUND.code()
- || responseStatus == HttpResponseStatus.SEE_OTHER.code()
- || responseStatus == HttpResponseStatus.TEMPORARY_REDIRECT.code()
- || responseStatus == HttpResponseStatus.PERMANENT_REDIRECT.code())) {
+ && (responseStatus == ResponseStatus.Redirect3xx.MOVED_PERMANENTLY_301.getStatusCode()
+ || responseStatus == ResponseStatus.Redirect3xx.FOUND_302.getStatusCode()
+ || responseStatus == ResponseStatus.Redirect3xx.SEE_OTHER_303.getStatusCode()
+ || responseStatus == ResponseStatus.Redirect3xx.TEMPORARY_REDIRECT_307.getStatusCode()
+ || responseStatus == ResponseStatus.Redirect3xx.PERMANENT_REDIRECT_308.getStatusCode())) {
String location = cr.getHeaderString(HttpHeaders.LOCATION);
if (location == null || location.isEmpty()) {
responseAvailable.completeExceptionally(new RedirectException(LocalizationMessages.REDIRECT_NO_LOCATION()));
} else {
try {
URI newUri = URI.create(location);
+ if (!newUri.isAbsolute()) {
+ final URI originalUri = jerseyRequest.getUri();
+ newUri = new JerseyUriBuilder()
+ .scheme(originalUri.getScheme())
+ .userInfo(originalUri.getUserInfo())
+ .host(originalUri.getHost())
+ .port(originalUri.getPort())
+ .uri(location)
+ .build();
+ }
boolean alreadyRequested = !redirectUriHistory.add(newUri);
if (alreadyRequested) {
// infinite loop detection
@@ -126,6 +142,7 @@
} else {
ClientRequest newReq = new ClientRequest(jerseyRequest);
newReq.setUri(newUri);
+ restrictRedirectRequest(newReq, cr);
connector.execute(newReq, redirectUriHistory, responseAvailable);
}
} catch (IllegalArgumentException e) {
@@ -165,7 +182,7 @@
}
// request entity handling.
- if ((response.headers().contains(HttpHeaderNames.CONTENT_LENGTH) && HttpUtil.getContentLength(response) > 0)
+ if ((response.headers().contains(HttpHeaders.CONTENT_LENGTH) && HttpUtil.getContentLength(response) > 0)
|| HttpUtil.isTransferEncodingChunked(response)) {
nis = new NettyInputStream();
@@ -218,4 +235,63 @@
super.userEventTriggered(ctx, evt);
}
}
+
+ /*
+ * RFC 9110 Section 15.4
+ * https://httpwg.org/specs/rfc9110.html#rfc.section.15.4
+ */
+ private void restrictRedirectRequest(ClientRequest newRequest, ClientResponse response) {
+ final MultivaluedMap<String, Object> headers = newRequest.getHeaders();
+ final Boolean keepMethod = newRequest.resolveProperty(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, Boolean.TRUE);
+
+ if (Boolean.FALSE.equals(keepMethod) && newRequest.getMethod().equals(HttpMethod.POST)) {
+ switch (response.getStatus()) {
+ case 301 /* MOVED PERMANENTLY */:
+ case 302 /* FOUND */:
+ removeContentHeaders(headers);
+ newRequest.setMethod(HttpMethod.GET);
+ newRequest.setEntity(null);
+ break;
+ }
+ }
+
+ for (final Iterator<Map.Entry<String, List<Object>>> it = headers.entrySet().iterator(); it.hasNext(); ) {
+ final Map.Entry<String, List<Object>> entry = it.next();
+ if (ProxyHeaders.INSTANCE.test(entry.getKey())) {
+ it.remove();
+ }
+ }
+
+ headers.remove(HttpHeaders.IF_MATCH);
+ headers.remove(HttpHeaders.IF_NONE_MATCH);
+ headers.remove(HttpHeaders.IF_MODIFIED_SINCE);
+ headers.remove(HttpHeaders.IF_UNMODIFIED_SINCE);
+ headers.remove(HttpHeaders.AUTHORIZATION);
+ headers.remove(HttpHeaders.REFERER);
+ headers.remove(HttpHeaders.COOKIE);
+ }
+
+ private void removeContentHeaders(MultivaluedMap<String, Object> headers) {
+ for (final Iterator<Map.Entry<String, List<Object>>> it = headers.entrySet().iterator(); it.hasNext(); ) {
+ final Map.Entry<String, List<Object>> entry = it.next();
+ final String lowName = entry.getKey().toLowerCase(Locale.ROOT);
+ if (lowName.startsWith("content-")) {
+ it.remove();
+ }
+ }
+ headers.remove(HttpHeaders.LAST_MODIFIED);
+ headers.remove(HttpHeaders.TRANSFER_ENCODING);
+ }
+
+ /* package */ static class ProxyHeaders implements Predicate<String> {
+ static final ProxyHeaders INSTANCE = new ProxyHeaders();
+ private static final String HOST = HttpHeaders.HOST.toLowerCase(Locale.ROOT);
+ private static final String FORWARDED = HttpHeaders.FORWARDED.toLowerCase(Locale.ROOT);
+
+ @Override
+ public boolean test(String headerName) {
+ String lowName = headerName.toLowerCase(Locale.ROOT);
+ return lowName.startsWith("proxy-") || lowName.equals(HOST) || lowName.equals(FORWARDED);
+ }
+ }
}
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java
new file mode 100644
index 0000000..8bceac6
--- /dev/null
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.netty.connector;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.http.DefaultFullHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpResponse;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpUtil;
+import org.glassfish.jersey.client.ClientRequest;
+
+import jakarta.ws.rs.ProcessingException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class JerseyExpectContinueHandler extends ChannelInboundHandlerAdapter {
+
+ private boolean isExpected;
+
+ private static final List<HttpResponseStatus> statusesToBeConsidered = Arrays.asList(HttpResponseStatus.CONTINUE,
+ HttpResponseStatus.UNAUTHORIZED, HttpResponseStatus.EXPECTATION_FAILED,
+ HttpResponseStatus.METHOD_NOT_ALLOWED, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE);
+
+ private CompletableFuture<HttpResponseStatus> expectedFuture = new CompletableFuture<>();
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ if (isExpected && msg instanceof HttpResponse) {
+ final HttpResponse response = (HttpResponse) msg;
+ if (statusesToBeConsidered.contains(response.status())) {
+ expectedFuture.complete(response.status());
+ }
+ if (!HttpResponseStatus.CONTINUE.equals(response.status())) {
+ ctx.fireChannelRead(msg); //bypass the message to the next handler in line
+ } else {
+ ctx.pipeline().remove(JerseyExpectContinueHandler.class);
+ }
+ } else {
+ if (!isExpected) {
+ ctx.pipeline().remove(JerseyExpectContinueHandler.class);
+ }
+ ctx.fireChannelRead(msg); //bypass the message to the next handler in line
+ }
+ }
+
+ CompletableFuture<HttpResponseStatus> processExpect100ContinueRequest(HttpRequest nettyRequest,
+ ClientRequest jerseyRequest,
+ Channel ch,
+ Integer timeout)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ //check for 100-Continue presence/availability
+ final Expect100ContinueConnectorExtension expect100ContinueExtension
+ = new Expect100ContinueConnectorExtension();
+
+ final DefaultFullHttpRequest nettyRequestHeaders =
+ new DefaultFullHttpRequest(nettyRequest.protocolVersion(), nettyRequest.method(), nettyRequest.uri());
+ nettyRequestHeaders.headers().setAll(nettyRequest.headers());
+
+ if (!nettyRequestHeaders.headers().contains(HttpHeaderNames.HOST)) {
+ nettyRequestHeaders.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost());
+ }
+
+ //If Expect:100-continue feature is enabled and client supports it, the nettyRequestHeaders will be
+ //enriched with the 'Expect:100-continue' header.
+ expect100ContinueExtension.invoke(jerseyRequest, nettyRequestHeaders);
+
+ final ChannelFuture expect100ContinueFuture = (HttpUtil.is100ContinueExpected(nettyRequestHeaders))
+ // Send only head of the HTTP request enriched with Expect:100-continue header.
+ ? ch.writeAndFlush(nettyRequestHeaders)
+ // Expect:100-Continue either is not supported or is turned off
+ : null;
+ isExpected = expect100ContinueFuture != null;
+ if (!isExpected) {
+ ch.pipeline().remove(JerseyExpectContinueHandler.class);
+ } else {
+ final HttpResponseStatus status = expectedFuture
+ .get(timeout, TimeUnit.MILLISECONDS);
+
+ processExpectationStatus(status);
+ }
+ return expectedFuture;
+ }
+
+ private void processExpectationStatus(HttpResponseStatus status)
+ throws TimeoutException {
+ if (!statusesToBeConsidered.contains(status)) {
+ throw new ProcessingException(LocalizationMessages
+ .UNEXPECTED_VALUE_FOR_EXPECT_100_CONTINUE_STATUSES(status.code()), null);
+ }
+ if (!expectedFuture.isDone() || HttpResponseStatus.EXPECTATION_FAILED.equals(status)) {
+ isExpected = false;
+ throw new TimeoutException(); // continue without expectations
+ }
+ if (!HttpResponseStatus.CONTINUE.equals(status)) {
+ throw new ProcessingException(LocalizationMessages
+ .UNEXPECTED_VALUE_FOR_EXPECT_100_CONTINUE_STATUSES(status.code()), null);
+ }
+ }
+
+ boolean isExpected() {
+ return isExpected;
+ }
+}
\ No newline at end of file
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java
index 671b08f..9c79d12 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -28,11 +28,32 @@
/**
* <p>
- * This property determines the maximum number of idle connections that will be simultaneously kept alive
- * in total, rather than per destination. The default is 60. Specify 0 to disable.
+ * Sets the endpoint identification algorithm to HTTPS.
* </p>
+ * <p>
+ * The default value is {@code true} (for HTTPS uri scheme).
+ * </p>
+ * <p>
+ * The name of the configuration property is <tt>{@value}</tt>.
+ * </p>
+ * @since 2.35
+ * @see javax.net.ssl.SSLParameters#setEndpointIdentificationAlgorithm(String)
*/
- public static final String MAX_CONNECTIONS_TOTAL = "jersey.config.client.maxTotalConnections";
+ public static final String ENABLE_SSL_HOSTNAME_VERIFICATION = "jersey.config.client.tls.enableHostnameVerification";
+
+ /**
+ * <p>
+ * Filter the HTTP headers for requests (CONNECT) towards the proxy except for PROXY-prefixed and HOST headers when {@code true}.
+ * </p>
+ * <p>
+ * The default value is {@code true} and the headers are filtered out.
+ * </p>
+ * <p>
+ * The name of the configuration property is <tt>{@value}</tt>.
+ * </p>
+ * @since 2.41
+ */
+ public static final String FILTER_HEADERS_FOR_PROXY = "jersey.config.client.filter.headers.proxy";
/**
* <p>
@@ -56,23 +77,16 @@
/**
* <p>
- * Sets the endpoint identification algorithm to HTTPS.
+ * This property determines the maximum number of idle connections that will be simultaneously kept alive
+ * in total, rather than per destination. The default is 60. Specify 0 to disable.
* </p>
- * <p>
- * The default value is {@code true} (for HTTPS uri scheme).
- * </p>
- * <p>
- * The name of the configuration property is <tt>{@value}</tt>.
- * </p>
- * @since 2.35
- * @see javax.net.ssl.SSLParameters#setEndpointIdentificationAlgorithm(String)
*/
- public static final String ENABLE_SSL_HOSTNAME_VERIFICATION = "jersey.config.client.tls.enableHostnameVerification";
+ public static final String MAX_CONNECTIONS_TOTAL = "jersey.config.client.maxTotalConnections";
/**
* The maximal number of redirects during single request.
* <p/>
- * Value is expected to be positive {@link Integer}. Default value is {@value #DEFAULT_MAX_REDIRECTS}.
+ * Value is expected to be positive {@link Integer}. Default value is 5.
* <p/>
* HTTP redirection must be enabled by property {@link org.glassfish.jersey.client.ClientProperties#FOLLOW_REDIRECTS},
* otherwise {@code MAX_REDIRECTS} is not applied.
@@ -82,4 +96,37 @@
* @see org.glassfish.jersey.netty.connector.internal.RedirectException
*/
public static final String MAX_REDIRECTS = "jersey.config.client.NettyConnectorProvider.maxRedirects";
+
+ /**
+ * <p>
+ * Sets the HTTP POST method to be preserved on HTTP status 301 (MOVED PERMANENTLY) or status 302 (FOUND) when {@code true}
+ * or redirected as GET when {@code false}.
+ * </p>
+ * <p>
+ * The default value is {@code true} and the HTTP POST request is not redirected as GET.
+ * </p>
+ * <p>
+ * The name of the configuration property is <tt>{@value}</tt>.
+ * </p>
+ * @since 2.41
+ */
+ public static final String PRESERVE_METHOD_ON_REDIRECT = "jersey.config.client.redirect.preserve.method";
+
+
+ /**
+ * This timeout is used for waiting for 100-Continue response when 100-Continue is sent by the client.
+ *
+ * @since 2.41
+ */
+ public static final String
+ EXPECT_100_CONTINUE_TIMEOUT = "jersey.config.client.request.expect.100.continue.timeout";
+
+ /**
+ * The default value of EXPECT_100_CONTINUE_TIMEOUT.
+ *
+ * @since 2.41
+ */
+ public static final Integer
+ DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE = 500;
+
}
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
index 4b4f16f..ede8c36 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
@@ -31,13 +31,16 @@
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLContext;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.Client;
@@ -123,6 +126,7 @@
private static final String PRUNE_INACTIVE_POOL = "prune_inactive_pool";
private static final String READ_TIMEOUT_HANDLER = "read_timeout_handler";
private static final String REQUEST_HANDLER = "request_handler";
+ private static final String EXPECT_100_CONTINUE_HANDLER = "expect_100_continue_handler";
NettyConnector(Client client) {
@@ -189,6 +193,9 @@
protected void execute(final ClientRequest jerseyRequest, final Set<URI> redirectUriHistory,
final CompletableFuture<ClientResponse> responseAvailable) {
Integer timeout = jerseyRequest.resolveProperty(ClientProperties.READ_TIMEOUT, 0);
+ final Integer expect100ContinueTimeout = jerseyRequest.resolveProperty(
+ NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT,
+ NettyClientProperties.DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE);
if (timeout == null || timeout < 0) {
throw new ProcessingException(LocalizationMessages.WRONG_READ_TIMEOUT(timeout));
}
@@ -200,8 +207,10 @@
int port = requestUri.getPort() != -1 ? requestUri.getPort() : "https".equals(requestUri.getScheme()) ? 443 : 80;
try {
+ final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder()
+ .request(jerseyRequest).setSNIAlways(true).build();
- String key = requestUri.getScheme() + "://" + host + ":" + port;
+ String key = requestUri.getScheme() + "://" + sslConfig.getSNIHostName() + ":" + port;
ArrayList<Channel> conns;
synchronized (connections) {
conns = connections.get(key);
@@ -231,9 +240,8 @@
}
}
- Integer connectTimeout = jerseyRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, 0);
-
if (chan == null) {
+ Integer connectTimeout = jerseyRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, 0);
Bootstrap b = new Bootstrap();
// http proxy
@@ -270,7 +278,7 @@
if ("https".equals(requestUri.getScheme())) {
// making client authentication optional for now; it could be extracted to configurable property
JdkSslContext jdkSslContext = new JdkSslContext(
- client.getSslContext(),
+ getSslContext(client, jerseyRequest),
true,
(Iterable) null,
IdentityCipherSuiteFilter.INSTANCE,
@@ -281,8 +289,7 @@
);
final int port = requestUri.getPort();
- final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder()
- .request(jerseyRequest).setSNIAlways(true).build();
+
final SslHandler sslHandler = jdkSslContext.newHandler(
ch.alloc(), sslConfig.getSNIHostName(), port <= 0 ? 443 : port, executorService
);
@@ -320,9 +327,11 @@
final Channel ch = chan;
JerseyClientHandler clientHandler =
new JerseyClientHandler(jerseyRequest, responseAvailable, responseDone, redirectUriHistory, this);
+ final JerseyExpectContinueHandler expect100ContinueHandler = new JerseyExpectContinueHandler();
// read timeout makes sense really as an inactivity timeout
ch.pipeline().addLast(READ_TIMEOUT_HANDLER,
new IdleStateHandler(0, 0, timeout, TimeUnit.MILLISECONDS));
+ ch.pipeline().addLast(EXPECT_100_CONTINUE_HANDLER, expect100ContinueHandler);
ch.pipeline().addLast(REQUEST_HANDLER, clientHandler);
responseDone.whenComplete((_r, th) -> {
@@ -375,11 +384,13 @@
}
// headers
- setHeaders(jerseyRequest, nettyRequest.headers());
+ if (!jerseyRequest.hasEntity()) {
+ setHeaders(jerseyRequest, nettyRequest.headers(), false);
- // host header - http 1.1
- if (!nettyRequest.headers().contains(HttpHeaderNames.HOST)) {
- nettyRequest.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost());
+ // host header - http 1.1
+ if (!nettyRequest.headers().contains(HttpHeaderNames.HOST)) {
+ nettyRequest.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost());
+ }
}
if (jerseyRequest.hasEntity()) {
@@ -407,23 +418,35 @@
// // Set later after the entity is "written"
// break;
}
+ try {
+ expect100ContinueHandler.processExpect100ContinueRequest(nettyRequest, jerseyRequest,
+ ch, expect100ContinueTimeout);
+ } catch (ExecutionException e) {
+ responseDone.completeExceptionally(e);
+ } catch (TimeoutException e) {
+ //Expect:100-continue allows timeouts by the spec
+ //just removing the pipeline from processing
+ if (ch.pipeline().context(JerseyExpectContinueHandler.class) != null) {
+ ch.pipeline().remove(EXPECT_100_CONTINUE_HANDLER);
+ }
+ }
- // Send the HTTP request.
- entityWriter.writeAndFlush(nettyRequest);
+ final CountDownLatch headersSet = new CountDownLatch(1);
+ final CountDownLatch contentLengthSet = new CountDownLatch(1);
jerseyRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {
@Override
public OutputStream getOutputStream(int contentLength) throws IOException {
+ replaceHeaders(jerseyRequest, nettyRequest.headers()); // WriterInterceptor changes
+ if (!nettyRequest.headers().contains(HttpHeaderNames.HOST)) {
+ nettyRequest.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost());
+ }
+ headersSet.countDown();
+
return entityWriter.getOutputStream();
}
});
- if (HttpUtil.isTransferEncodingChunked(nettyRequest)) {
- entityWriter.write(new HttpChunkedInput(entityWriter.getChunkedInput()));
- } else {
- entityWriter.write(entityWriter.getChunkedInput());
- }
-
executorService.execute(new Runnable() {
@Override
public void run() {
@@ -434,9 +457,8 @@
jerseyRequest.writeEntity();
if (entityWriter.getType() == NettyEntityWriter.Type.DELAYED) {
- replaceHeaders(jerseyRequest, nettyRequest.headers()); // WriterInterceptor changes
nettyRequest.headers().set(HttpHeaderNames.CONTENT_LENGTH, entityWriter.getLength());
- entityWriter.flush();
+ contentLengthSet.countDown();
}
} catch (IOException e) {
@@ -445,9 +467,23 @@
}
});
- if (entityWriter.getType() != NettyEntityWriter.Type.DELAYED) {
- entityWriter.flush();
+ headersSet.await();
+ if (!expect100ContinueHandler.isExpected()) {
+ // Send the HTTP request. Expect:100-continue processing is not applicable
+ // in this case.
+ entityWriter.writeAndFlush(nettyRequest);
}
+
+ if (HttpUtil.isTransferEncodingChunked(nettyRequest)) {
+ entityWriter.write(new HttpChunkedInput(entityWriter.getChunkedInput()));
+ } else {
+ entityWriter.write(entityWriter.getChunkedInput());
+ }
+
+ if (entityWriter.getType() == NettyEntityWriter.Type.DELAYED) {
+ contentLengthSet.await();
+ }
+ entityWriter.flush();
} else {
// Send the HTTP request.
ch.writeAndFlush(nettyRequest);
@@ -458,6 +494,11 @@
}
}
+ private SSLContext getSslContext(Client client, ClientRequest request) {
+ Supplier<SSLContext> supplier = request.resolveProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, Supplier.class);
+ return supplier == null ? client.getSslContext() : supplier.get();
+ }
+
private String buildPathWithQueryParameters(URI requestUri) {
if (requestUri.getRawQuery() != null) {
return String.format("%s?%s", requestUri.getRawPath(), requestUri.getRawQuery());
@@ -510,7 +551,8 @@
private static ProxyHandler createProxyHandler(ClientRequest jerseyRequest, SocketAddress proxyAddr,
String userName, String password, long connectTimeout) {
- HttpHeaders httpHeaders = setHeaders(jerseyRequest, new DefaultHttpHeaders());
+ final Boolean filter = jerseyRequest.resolveProperty(NettyClientProperties.FILTER_HEADERS_FOR_PROXY, Boolean.TRUE);
+ HttpHeaders httpHeaders = setHeaders(jerseyRequest, new DefaultHttpHeaders(), Boolean.TRUE.equals(filter));
ProxyHandler proxy = userName == null ? new HttpProxyHandler(proxyAddr, httpHeaders)
: new HttpProxyHandler(proxyAddr, userName, password, httpHeaders);
@@ -521,9 +563,12 @@
return proxy;
}
- private static HttpHeaders setHeaders(ClientRequest jerseyRequest, HttpHeaders headers) {
+ private static HttpHeaders setHeaders(ClientRequest jerseyRequest, HttpHeaders headers, boolean proxyOnly) {
for (final Map.Entry<String, List<String>> e : jerseyRequest.getStringHeaders().entrySet()) {
- headers.add(e.getKey(), e.getValue());
+ final String key = e.getKey();
+ if (!proxyOnly || JerseyClientHandler.ProxyHeaders.INSTANCE.test(key) || additionalProxyHeadersToKeep(key)) {
+ headers.add(key, e.getValue());
+ }
}
return headers;
}
@@ -534,4 +579,11 @@
}
return headers;
}
+
+ /*
+ * Keep all X- headers (X-Forwarded-For,...) for proxy
+ */
+ private static boolean additionalProxyHeadersToKeep(String key) {
+ return key.length() > 2 && (key.charAt(0) == 'x' || key.charAt(0) == 'X') && (key.charAt(1) == '-');
+ }
}
diff --git a/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties b/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties
index ba91c4f..7d6f9fc 100644
--- a/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties
+++ b/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2016, 2022 Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2016, 2023 Oracle and/or its affiliates. All rights reserved.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License v. 2.0, which is available at
@@ -22,3 +22,4 @@
redirect.error.determining.location="Error determining redirect location: ({0})."
redirect.infinite.loop="Infinite loop in chained redirects detected."
redirect.limit.reached="Max chained redirect limit ({0}) exceeded."
+unexpected.value.for.expect.100.continue.statuses=Unexpected value: ("{0}").
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/BufferedTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/BufferedTest.java
index a01c04d..963dd86 100644
--- a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/BufferedTest.java
+++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/BufferedTest.java
@@ -51,12 +51,10 @@
public static class BufferedTestResource {
@POST
public String post(@Context HttpHeaders headers, String entity) {
- System.out.println("Remote");
String ret = headers.getHeaderString(HEADER_1)
+ headers.getHeaderString(HEADER_2)
+ headers.getHeaderString(HEADER_3)
+ entity;
- System.out.println(ret);
return ret;
}
}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HttpHeadersTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HttpHeadersTest.java
index 2eb3658..ee752a5 100644
--- a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HttpHeadersTest.java
+++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HttpHeadersTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2023 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,6 +16,7 @@
package org.glassfish.jersey.netty.connector;
+import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
@@ -46,6 +47,15 @@
assertEquals("client", xClient);
return "POST";
}
+
+ @GET
+ public String get(
+ @HeaderParam("Transfer-Encoding") String transferEncoding,
+ @HeaderParam("X-CLIENT") String xClient,
+ @HeaderParam("X-WRITER") String xWriter) {
+ assertEquals("client", xClient);
+ return "GET";
+ }
}
@Override
@@ -66,4 +76,13 @@
assertTrue(response.hasEntity());
response.close();
}
+
+ @Test
+ public void testGet() {
+ Response response = target("test").request().header("X-CLIENT", "client").get();
+
+ assertEquals(200, response.getStatus());
+ assertTrue(response.hasEntity());
+ response.close();
+ }
}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/RedirectHeadersTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/RedirectHeadersTest.java
new file mode 100644
index 0000000..dd56e74
--- /dev/null
+++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/RedirectHeadersTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.netty.connector;
+
+import io.netty.handler.codec.http.HttpHeaderNames;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.ServerProperties;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.Response;
+import java.net.URI;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Logger;
+
+public class RedirectHeadersTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(RedirectHeadersTest.class.getName());
+ private static final String TEST_URL = "http://localhost:%d/test";
+ private static final AtomicReference<String> TEST_URL_REF = new AtomicReference<>();
+ private static final String ENTITY = "entity";
+
+ @BeforeEach
+ public void before() {
+ final String url = String.format(TEST_URL, getPort());
+ TEST_URL_REF.set(url);
+ }
+
+ @Path("/test")
+ public static class RedirectResource {
+ @GET
+ public String get(@QueryParam("value") String value) {
+ return "GET" + value;
+ }
+
+ @POST
+ public String echo(@QueryParam("value") String value, String entity) {
+ return entity + value;
+ }
+
+ @GET
+ @Path("headers2")
+ public String headers(@Context HttpHeaders headers) {
+ String encoding = headers.getHeaderString(HttpHeaders.CONTENT_ENCODING);
+ String auth = headers.getHeaderString(HttpHeaderNames.PROXY_AUTHORIZATION.toString());
+ return encoding + ":" + auth;
+ }
+
+ @POST
+ @Path("301")
+ public Response redirect301(String entity) {
+ return Response.status(Response.Status.MOVED_PERMANENTLY)
+ .header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "?value=301"))
+ .build();
+ }
+
+ @POST
+ @Path("302")
+ public Response redirect302(String entity) {
+ return Response.status(Response.Status.FOUND)
+ .header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "?value=302"))
+ .build();
+ }
+
+ @POST
+ @Path("307")
+ public Response redirect307(String entity) {
+ return Response.status(Response.Status.TEMPORARY_REDIRECT)
+ .header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "?value=307"))
+ .build();
+ }
+
+ @POST
+ @Path("308")
+ public Response redirectHeaders(String entity) {
+ return Response.status(308)
+ .header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "?value=308"))
+ .build();
+ }
+
+
+ @POST
+ @Path("headers1")
+ public Response redirect308(String whatever) {
+ return Response.status(301).header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "/headers2")).build();
+ }
+
+ @GET
+ @Path("relative")
+ public Response relative() {
+ return Response.status(301).header(HttpHeaders.LOCATION, URI.create("/test/headers2").toASCIIString()).build();
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(RedirectResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ config.property(ServerProperties.LOCATION_HEADER_RELATIVE_URI_RESOLUTION_DISABLED, Boolean.TRUE);
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.property(ClientProperties.FOLLOW_REDIRECTS, true);
+ config.connectorProvider(new NettyConnectorProvider());
+ }
+
+ @Test
+ void testPost() {
+ testPost("301");
+ testPost("302");
+ testPost("307");
+ testPost("308");
+ }
+
+ @Test
+ void testGet() {
+ Assertions.assertEquals("GET301", testGet("301"));
+ Assertions.assertEquals("GET302", testGet("302"));
+ Assertions.assertEquals(ENTITY + "307", testGet("307"));
+ Assertions.assertEquals(ENTITY + "308", testGet("308"));
+ }
+
+ @Test
+ void testHeaders() {
+ MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
+ headers.add(HttpHeaders.CONTENT_ENCODING, "gzip");
+ headers.add(HttpHeaderNames.PROXY_AUTHORIZATION.toString(), "basic aGVsbG86d29ybGQ=");
+ try (Response response = target("test")
+ .property(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, false)
+ .path("headers1").request().headers(headers).post(Entity.entity(ENTITY, MediaType.TEXT_PLAIN_TYPE))) {
+ Assertions.assertEquals(200, response.getStatus());
+ Assertions.assertEquals("null:null", response.readEntity(String.class));
+ }
+ }
+
+ @Test
+ void testRelative() {
+ MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
+ try (Response response = target("test")
+ .path("relative").request().headers(headers).get()) {
+ Assertions.assertEquals(200, response.getStatus());
+ Assertions.assertEquals("null:null", response.readEntity(String.class));
+ }
+ }
+
+ void testPost(String status) {
+ try (Response response = target("test").path(status).request().post(Entity.entity(ENTITY, MediaType.TEXT_PLAIN_TYPE))) {
+ Assertions.assertEquals(200, response.getStatus());
+ Assertions.assertEquals(ENTITY + status, response.readEntity(String.class));
+ }
+ }
+
+ String testGet(String status) {
+ try (Response response = target("test")
+ .property(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, false)
+ .path(status).request().post(Entity.entity(ENTITY, MediaType.TEXT_PLAIN_TYPE))) {
+ Assertions.assertEquals(200, response.getStatus());
+ return response.readEntity(String.class);
+ }
+ }
+}
diff --git a/connectors/pom.xml b/connectors/pom.xml
index 3ac6040..ad4afa3 100644
--- a/connectors/pom.xml
+++ b/connectors/pom.xml
@@ -40,6 +40,7 @@
<module>helidon-connector</module>
<module>jdk-connector</module>
<module>jetty-connector</module>
+ <module>jetty11-http2-connector</module>
<module>jetty11-connector</module>
<module>jnh-connector</module>
<module>netty-connector</module>