Merge pull request #5890 from jansupol/merge2x250401
Merge 2.x into 3.0
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 e9d55c1..949003f 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
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2025 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
@@ -19,8 +19,6 @@
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
-import java.util.Iterator;
-import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -68,6 +66,7 @@
private final boolean followRedirects;
private final int maxRedirects;
private final NettyConnector connector;
+ private final NettyHttpRedirectController redirectController;
private NettyInputStream nis;
private ClientResponse jerseyResponse;
@@ -84,6 +83,10 @@
this.followRedirects = jerseyRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, true);
this.maxRedirects = jerseyRequest.resolveProperty(NettyClientProperties.MAX_REDIRECTS, DEFAULT_MAX_REDIRECTS);
this.connector = connector;
+
+ final NettyHttpRedirectController customRedirectController = jerseyRequest
+ .resolveProperty(NettyClientProperties.HTTP_REDIRECT_CONTROLLER, NettyHttpRedirectController.class);
+ this.redirectController = customRedirectController == null ? new NettyHttpRedirectController() : customRedirectController;
}
@Override
@@ -143,22 +146,24 @@
} else {
ClientRequest newReq = new ClientRequest(jerseyRequest);
newReq.setUri(newUri);
- restrictRedirectRequest(newReq, cr);
+ if (redirectController.prepareRedirect(newReq, cr)) {
+ final NettyConnector newConnector = new NettyConnector(newReq.getClient());
+ newConnector.execute(newReq, redirectUriHistory, new CompletableFuture<ClientResponse>() {
+ @Override
+ public boolean complete(ClientResponse value) {
+ newConnector.close();
+ return responseAvailable.complete(value);
+ }
- final NettyConnector newConnector = new NettyConnector(newReq.getClient());
- newConnector.execute(newReq, redirectUriHistory, new CompletableFuture<ClientResponse>() {
- @Override
- public boolean complete(ClientResponse value) {
- newConnector.close();
- return responseAvailable.complete(value);
- }
-
- @Override
- public boolean completeExceptionally(Throwable ex) {
- newConnector.close();
- return responseAvailable.completeExceptionally(ex);
- }
- });
+ @Override
+ public boolean completeExceptionally(Throwable ex) {
+ newConnector.close();
+ return responseAvailable.completeExceptionally(ex);
+ }
+ });
+ } else {
+ responseAvailable.complete(cr);
+ }
}
} catch (IllegalArgumentException e) {
responseAvailable.completeExceptionally(
@@ -227,8 +232,6 @@
}
}
-
-
@Override
public void exceptionCaught(ChannelHandlerContext ctx, final Throwable cause) {
responseDone.completeExceptionally(cause);
@@ -244,53 +247,6 @@
}
}
- /*
- * 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);
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 562edad..4b2e4c9 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, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2025 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
@@ -57,6 +57,15 @@
/**
* <p>
+ * The implementation of custom {@link NettyHttpRedirectController} redirect logic.
+ * </p>
+ *
+ * @since 2.47
+ */
+ public static final String HTTP_REDIRECT_CONTROLLER = "jersey.config.client.netty.http.redirect.controller";
+
+ /**
+ * <p>
* This property determines the number of seconds the idle connections are kept in the pool before pruned.
* The default is 60. Specify 0 to disable.
* </p>
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 ebdfda4..a4622b8 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
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2025 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
@@ -17,7 +17,9 @@
package org.glassfish.jersey.netty.connector;
import java.io.IOException;
+import java.io.InterruptedIOException;
import java.io.OutputStream;
+import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
@@ -463,10 +465,15 @@
jerseyRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {
@Override
public OutputStream getOutputStream(int contentLength) throws IOException {
- replaceHeaders(jerseyRequest, nettyRequest.headers()); // WriterInterceptor changes
- setHostHeader(jerseyRequest, nettyRequest);
- headersSet.countDown();
-
+ try {
+ replaceHeaders(jerseyRequest, nettyRequest.headers()); // WriterInterceptor changes
+ setHostHeader(jerseyRequest, nettyRequest);
+ } catch (Exception e) {
+ responseDone.completeExceptionally(e);
+ throw new IOException(e);
+ } finally {
+ headersSet.countDown();
+ }
return entityWriter.getOutputStream();
}
});
@@ -485,7 +492,14 @@
contentLengthSet.countDown();
}
- } catch (IOException e) {
+ } catch (Exception e) {
+ if (entityWriter.getChunkedInput() != null) {
+ try {
+ entityWriter.getChunkedInput().close();
+ } catch (Exception ex) {
+ // Ignore ex in favor of e
+ }
+ }
responseDone.completeExceptionally(e);
}
}
@@ -620,7 +634,7 @@
if (!nettyRequest.headers().contains(HttpHeaderNames.HOST)) {
int requestPort = jerseyRequest.getUri().getPort();
final String hostHeader;
- if (requestPort != 80 && requestPort != 443) {
+ if (requestPort != -1 && requestPort != 80 && requestPort != 443) {
hostHeader = jerseyRequest.getUri().getHost() + ":" + requestPort;
} else {
hostHeader = jerseyRequest.getUri().getHost();
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttpRedirectController.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttpRedirectController.java
new file mode 100644
index 0000000..670be3e
--- /dev/null
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttpRedirectController.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2025 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 org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.ClientResponse;
+import org.glassfish.jersey.http.HttpHeaders;
+
+import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.core.MultivaluedMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * The HTTP Redirect logic implementation for Netty Connector.
+ *
+ * @since 2.47
+ */
+public class NettyHttpRedirectController {
+
+ /**
+ * Configure the HTTP request after HTTP Redirect response has been received.
+ * By default, the HTTP POST request is transformed into HTTP GET for status 301 & 302.
+ * Also, HTTP Headers described by RFC 9110 Section 15.4 are removed from the new HTTP Request.
+ *
+ * @param request The new {@link ClientRequest} to be sent to the redirected URI.
+ * @param response The original HTTP redirect {@link ClientResponse} received.
+ * @return {@code true} when the new request should be sent.
+ */
+ public boolean prepareRedirect(ClientRequest request, ClientResponse response) {
+ final Boolean keepMethod = request.resolveProperty(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, Boolean.TRUE);
+
+ if (Boolean.FALSE.equals(keepMethod) && request.getMethod().equals(HttpMethod.POST)) {
+ switch (response.getStatus()) {
+ case 301 /* MOVED PERMANENTLY */:
+ case 302 /* FOUND */:
+ removeContentHeaders(request.getHeaders());
+ request.setMethod(HttpMethod.GET);
+ request.setEntity(null);
+ break;
+ }
+ }
+
+ restrictRequestHeaders(request, response);
+ return true;
+ }
+
+ /**
+ * RFC 9110 Section 15.4 defines the HTTP headers that should be removed from the redirected request.
+ * https://httpwg.org/specs/rfc9110.html#rfc.section.15.4.
+ *
+ * @param request the new request to a new URI location.
+ * @param response the HTTP redirect response.
+ */
+ protected void restrictRequestHeaders(ClientRequest request, ClientResponse response) {
+ final MultivaluedMap<String, Object> headers = request.getHeaders();
+
+ for (final Iterator<Map.Entry<String, List<Object>>> it = headers.entrySet().iterator(); it.hasNext(); ) {
+ final Map.Entry<String, List<Object>> entry = it.next();
+ if (JerseyClientHandler.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);
+ }
+
+}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputClosedOnErrorTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputClosedOnErrorTest.java
new file mode 100644
index 0000000..1d0135d
--- /dev/null
+++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputClosedOnErrorTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (c) 2025 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.handler.stream.ChunkedInput;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.client.spi.ConnectorProvider;
+import org.glassfish.jersey.netty.connector.internal.NettyEntityWriter;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.ProcessingException;
+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.Configuration;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.MessageBodyWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Bug 5837 reproducer
+ */
+public class ChunkedInputClosedOnErrorTest extends JerseyTest {
+
+ private static Client initClient(ConnectorProvider provider) {
+ ClientConfig defaultConfig = new ClientConfig();
+ defaultConfig.property(ClientProperties.CONNECT_TIMEOUT, 10 * 1000);
+ defaultConfig.property(ClientProperties.READ_TIMEOUT, 10 * 1000);
+ defaultConfig.connectorProvider(provider);
+ Client client = ClientBuilder.newBuilder()
+ .withConfig(defaultConfig)
+ .build();
+ return client;
+ }
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig();
+ }
+
+ @Test
+ public void testChunkedInputNotStuckedTimes() throws InterruptedException {
+ for (int i = 0; i != 10; i++) {
+ boolean ret = testChunkedInputNotStucked();
+ Assertions.assertTrue(ret, "JerseyChunkedInput was not closed on error");
+ }
+ }
+
+ public boolean testChunkedInputNotStucked() throws InterruptedException {
+ final AtomicReference<NettyEntityWriter> writer = new AtomicReference<>();
+ final CountDownLatch writerSetLatch = new CountDownLatch(1);
+ final CountDownLatch flushLatch = new CountDownLatch(1);
+ ConnectorProvider provider = new ConnectorProvider() {
+ @Override
+ public Connector getConnector(Client client, Configuration runtimeConfig) {
+ return new NettyConnector(client) {
+ @Override
+ NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel) {
+ writer.set(super.nettyEntityWriter(clientRequest, channel));
+ writerSetLatch.countDown();
+ return new NettyEntityWriter() {
+ private boolean slept = false;
+
+ @Override
+ public void write(Object object) {
+ writer.get().write(object);
+ }
+
+ @Override
+ public void writeAndFlush(Object object) {
+ writer.get().writeAndFlush(object);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ writer.get().flush();
+ flushLatch.countDown();
+ }
+
+ @Override
+ public ChunkedInput getChunkedInput() {
+ for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
+ // caught from catch block in executorService.execute(new Runnable() {
+ // "sleep" to simulate race condition
+ if (element.getClassName().contains("NettyConnector")
+ && element.getMethodName().equals("run")) {
+ try {
+ flushLatch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ return writer.get().getChunkedInput();
+ }
+
+ @Override
+ public OutputStream getOutputStream() {
+ return writer.get().getOutputStream();
+ }
+
+ @Override
+ public long getLength() {
+ return writer.get().getLength();
+ }
+
+ @Override
+ public Type getType() {
+ return writer.get().getType();
+ }
+ };
+ }
+ };
+ }
+ };
+
+ Client client = initClient(provider);
+ try {
+ Response r = client
+ .register(new MultipartWriter())
+ .target(target().getUri()).request()
+ .post(Entity.entity(new MultipartWriter(), MediaType.MULTIPART_FORM_DATA_TYPE));
+ } catch (ProcessingException expected) {
+
+ }
+ writerSetLatch.await();
+ try {
+ return writer.get().getChunkedInput().isEndOfInput();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static class MultipartWriter implements MessageBodyWriter<Object> {
+
+ @Override
+ public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+ return mediaType.equals(MediaType.MULTIPART_FORM_DATA_TYPE);
+ }
+
+ @Override
+ public void writeTo(Object object, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
+ MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException,
+ WebApplicationException {
+ throw new IllegalArgumentException("TestException");
+ }
+ }
+
+}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/CustomRedirectControllerTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/CustomRedirectControllerTest.java
new file mode 100644
index 0000000..d284379
--- /dev/null
+++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/CustomRedirectControllerTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2025 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 org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.ClientResponse;
+import org.glassfish.jersey.http.HttpHeaders;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.hamcrest.MatcherAssert;
+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.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.UriInfo;
+
+public class CustomRedirectControllerTest extends JerseyTest {
+ private static final String REDIRECTED = "redirected";
+
+ @Path("/")
+ public static class CustomRedirectControllerTestResource {
+ @Context
+ UriInfo uriInfo;
+
+ @GET
+ @Path(REDIRECTED)
+ public String redirected() {
+ return REDIRECTED;
+ }
+
+ @POST
+ @Path("doRedirect")
+ public Response doRedirect(int status) {
+ return Response.status(status)
+ .header(HttpHeaders.LOCATION, uriInfo.getBaseUri().toString() + "redirected")
+ .build();
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(CustomRedirectControllerTestResource.class);
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new NettyConnectorProvider());
+ }
+
+ @Test
+ public void testRedirectToGET() {
+ try (Response r = target("doRedirect")
+ .property(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, false)
+ .request().post(Entity.entity(301, MediaType.TEXT_PLAIN_TYPE))) {
+ MatcherAssert.assertThat(r.getStatus(), Matchers.is(200));
+ MatcherAssert.assertThat(r.readEntity(String.class), Matchers.is(REDIRECTED));
+ }
+ }
+
+ @Test
+ public void testNotRedirected() {
+ try (Response response = target("doRedirect")
+ .property(NettyClientProperties.HTTP_REDIRECT_CONTROLLER, new NettyHttpRedirectController() {
+ @Override
+ public boolean prepareRedirect(ClientRequest request, ClientResponse response) {
+ return false;
+ }
+ }).request().post(Entity.entity(301, MediaType.TEXT_PLAIN_TYPE))) {
+ MatcherAssert.assertThat(response.getStatus(), Matchers.is(301));
+ }
+ }
+}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/EmptyHeaderTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/EmptyHeaderTest.java
new file mode 100644
index 0000000..f47ab20
--- /dev/null
+++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/EmptyHeaderTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2025 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 org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Assertions;
+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.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+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.util.Collections;
+import java.util.concurrent.ExecutionException;
+
+/* Bug 5836 reproducer */
+public class EmptyHeaderTest extends JerseyTest {
+
+ public static void main(String[] args) throws ExecutionException, InterruptedException {
+ new EmptyHeaderTest().testEmptyHeaders();
+ }
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig().register(new EmptyHeaderTestResource());
+ }
+
+ @Test
+ public void testEmptyHeaders() throws ExecutionException, InterruptedException {
+ MultivaluedMap<String, Object> jersey2Headers = new MultivaluedHashMap();
+ jersey2Headers.put("", Collections.singletonList("sss"));
+
+ Entity mData = Entity.entity("{\"dd\":\"ddd\"}", MediaType.APPLICATION_JSON_TYPE);
+
+ ClientConfig config = new ClientConfig();
+ config.connectorProvider(new NettyConnectorProvider());
+ try {
+ Response r = ClientBuilder.newBuilder()
+ .withConfig(config)
+ .build()
+ .target(target().getUri())
+ .request()
+ .headers(jersey2Headers)
+ .post(mData);
+ Assertions.fail("Processing Exception not thrown for empty header name");
+ } catch (ProcessingException processingException) {
+ System.out.println(processingException.getMessage());
+ }
+ }
+
+ @Path("")
+ private static class EmptyHeaderTestResource {
+ @GET
+ public Response ok() {
+ return Response.ok().build();
+ }
+ }
+}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientMessageBodyFactory.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientMessageBodyFactory.java
index bbd0abc..1addb24 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/ClientMessageBodyFactory.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientMessageBodyFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2025 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.client;
+import org.glassfish.jersey.innate.io.SafelyClosable;
import org.glassfish.jersey.internal.BootstrapBag;
import org.glassfish.jersey.internal.BootstrapConfigurator;
import org.glassfish.jersey.internal.inject.Bindings;
@@ -29,7 +30,7 @@
import jakarta.ws.rs.core.Configuration;
-class ClientMessageBodyFactory extends MessageBodyFactory {
+class ClientMessageBodyFactory extends MessageBodyFactory implements SafelyClosable {
/**
* Keep reference to {@link ClientRuntime} so that {@code finalize} on it is not called
@@ -39,7 +40,7 @@
* but if the finalizer is invoked before that, the HK2 injection manager gets closed.
* </p>
*/
- private final LazyValue<ClientRuntime> clientRuntime;
+ private LazyValue<ClientRuntime> clientRuntime;
/**
* Create a new message body factory.
@@ -52,6 +53,11 @@
clientRuntime = Values.lazy(clientRuntimeValue);
}
+ @Override
+ public void close() {
+ clientRuntime = null;
+ }
+
/**
* Configurator which initializes and register {@link MessageBodyWorkers} instance into {@link InjectionManager} and
* {@link BootstrapBag}.
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/io/SafelyClosable.java b/core-common/src/main/java/org/glassfish/jersey/innate/io/SafelyClosable.java
new file mode 100644
index 0000000..33983b6
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/io/SafelyClosable.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2025 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.innate.io;
+
+/**
+ * A SafelyClosable is a resource that can be closed.
+ * The close method is invoked to release resources that the object is holding.
+ * Closing the resource is safe in a sense that no Exception is being thrown.
+ */
+public interface SafelyClosable {
+
+ /**
+ * Close the resource, no checked exception thrown.
+ */
+ void close();
+}
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/MessageBodyWorkers.java b/core-common/src/main/java/org/glassfish/jersey/message/MessageBodyWorkers.java
index 07907f2..4c4fc5c 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/MessageBodyWorkers.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/MessageBodyWorkers.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2025 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
@@ -32,6 +32,7 @@
import jakarta.ws.rs.ext.ReaderInterceptor;
import jakarta.ws.rs.ext.WriterInterceptor;
+import org.glassfish.jersey.innate.io.SafelyClosable;
import org.glassfish.jersey.internal.PropertiesDelegate;
/**
@@ -43,7 +44,7 @@
* @see MessageBodyReader
* @see MessageBodyWriter
*/
-public interface MessageBodyWorkers {
+public interface MessageBodyWorkers extends SafelyClosable {
/**
* Get the map of media type to list of message body writers that are compatible with
* a media type.
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
index 8f31f74..29cb6e0 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 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
@@ -45,6 +45,7 @@
import javax.xml.transform.Source;
+import org.glassfish.jersey.innate.io.SafelyClosable;
import org.glassfish.jersey.internal.LocalizationMessages;
import org.glassfish.jersey.internal.PropertiesDelegate;
import org.glassfish.jersey.internal.util.collection.GuardianStringKeyMultivaluedMap;
@@ -58,7 +59,7 @@
*
* @author Marek Potociar
*/
-public abstract class InboundMessageContext extends MessageHeaderMethods {
+public abstract class InboundMessageContext extends MessageHeaderMethods implements SafelyClosable {
private static final InputStream EMPTY = new InputStream() {
@@ -720,6 +721,9 @@
*/
public void close() {
entityContent.close(true);
+ if (workers != null) {
+ workers.close();
+ }
setWorkers(null);
}
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java
index ab95eaf..a154d48 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2025 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
@@ -1189,4 +1189,11 @@
}
return false;
}
+
+
+ @Override
+ public void close() {
+ // NOOP
+ }
+
}
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
index ceb1540..efa8899 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 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
@@ -39,6 +39,7 @@
import jakarta.ws.rs.core.MultivaluedMap;
import org.glassfish.jersey.CommonProperties;
+import org.glassfish.jersey.innate.io.SafelyClosable;
import org.glassfish.jersey.internal.RuntimeDelegateDecorator;
import org.glassfish.jersey.internal.util.ReflectionHelper;
import org.glassfish.jersey.internal.util.collection.GuardianStringKeyMultivaluedMap;
@@ -52,7 +53,7 @@
*
* @author Marek Potociar
*/
-public class OutboundMessageContext extends MessageHeaderMethods {
+public class OutboundMessageContext extends MessageHeaderMethods implements SafelyClosable {
private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0];
private static final List<MediaType> WILDCARD_ACCEPTABLE_TYPE_SINGLETON_LIST =
Collections.<MediaType>singletonList(MediaTypes.WILDCARD_ACCEPTABLE_TYPE);
diff --git a/core-common/src/main/java/org/glassfish/jersey/model/internal/ComponentBag.java b/core-common/src/main/java/org/glassfish/jersey/model/internal/ComponentBag.java
index 08f321c..abd0953 100644
--- a/core-common/src/main/java/org/glassfish/jersey/model/internal/ComponentBag.java
+++ b/core-common/src/main/java/org/glassfish/jersey/model/internal/ComponentBag.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 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
@@ -605,7 +605,7 @@
return instancesView.stream()
.filter(input -> {
final ContractProvider model = getModel(input.getClass());
- return filter.test(model);
+ return model == null ? false : filter.test(model);
})
.collect(Collectors.toCollection(LinkedHashSet::new));
}
diff --git a/docs/src/main/docbook/appendix-properties.xml b/docs/src/main/docbook/appendix-properties.xml
index c53de04..2ab6d44 100644
--- a/docs/src/main/docbook/appendix-properties.xml
+++ b/docs/src/main/docbook/appendix-properties.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<!--
- Copyright (c) 2013, 2024 Oracle and/or its affiliates. All rights reserved.
+ Copyright (c) 2013, 2025 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
@@ -2208,6 +2208,16 @@
</thead>
<tbody>
<row>
+ <entry>&jersey.netty.NettyClientProperties.HTTP_REDIRECT_CONTROLLER;</entry>
+ <entry><literal>jersey.config.client.netty.http.redirect.controller</literal></entry>
+ <entry>
+ <para>
+ The implementation of custom &jersey.netty.NettyHttpRedirectController; redirect logic.
+ <literal>Since 2.47</literal>
+ </para>
+ </entry>
+ </row>
+ <row>
<entry>&jersey.netty.NettyClientProperties.FILTER_HEADERS_FOR_PROXY;</entry>
<entry><literal>jersey.config.client.filter.headers.proxy</literal></entry>
<entry>
diff --git a/docs/src/main/docbook/jersey.ent b/docs/src/main/docbook/jersey.ent
index cc94282..52b8a74 100644
--- a/docs/src/main/docbook/jersey.ent
+++ b/docs/src/main/docbook/jersey.ent
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="iso-8859-1" ?>
<!--
- Copyright (c) 2010, 2024 Oracle and/or its affiliates. All rights reserved.
+ Copyright (c) 2010, 2025 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
@@ -571,6 +571,7 @@
<!ENTITY jersey.message.filtering.SecurityEntityFilteringFeature "<link xlink:href='&jersey.javadoc.uri.prefix;/message/filtering/SecurityEntityFilteringFeature.html'>SecurityEntityFilteringFeature</link>">
<!ENTITY jersey.message.filtering.SelectableEntityFilteringFeature "<link xlink:href='&jersey.javadoc.uri.prefix;/message/filtering/SelectableEntityFilteringFeature.html'>SelectableEntityFilteringFeature</link>">
<!ENTITY jersey.netty.NettyClientProperties "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyClientProperties.html'>NettyClientProperties</link>" >
+<!ENTITY jersey.netty.NettyClientProperties.HTTP_REDIRECT_CONTROLLER "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyClientProperties.html#HTTP_REDIRECT_CONTROLLER'>NettyClientProperties.HTTP_REDIRECT_CONTROLLER</link>" >
<!ENTITY jersey.netty.NettyClientProperties.FILTER_HEADERS_FOR_PROXY "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyClientProperties.html#FILTER_HEADERS_FOR_PROXY'>NettyClientProperties.FILTER_HEADERS_FOR_PROXY</link>" >
<!ENTITY jersey.netty.NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyClientProperties.html#IDLE_CONNECTION_PRUNE_TIMEOUT'>NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT</link>" >
<!ENTITY jersey.netty.NettyClientProperties.MAX_CONNECTIONS "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyClientProperties.html#MAX_CONNECTIONS'>NettyClientProperties.MAX_CONNECTIONS</link>" >
@@ -582,6 +583,7 @@
<!ENTITY jersey.netty.NettyClientProperties.MAX_INITIAL_LINE_LENGTH "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyClientProperties.html#MAX_INITIAL_LINE_LENGTH'>NettyClientProperties.MAX_INITIAL_LINE_LENGTH</link>" >
<!ENTITY jersey.netty.NettyClientProperties.MAX_CHUNK_SIZE "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyClientProperties.html#MAX_CHUNK_SIZE'>NettyClientProperties.MAX_CHUNK_SIZE</link>" >
<!ENTITY jersey.netty.NettyConnectorProvider "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyConnectorProvider.html'>NettyConnectorProvider</link>">
+<!ENTITY jersey.netty.NettyHttpRedirectController "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyHttpRedirectController.html'>NettyHttpRedirectController</link>">
<!ENTITY jersey.server.ApplicationHandler "<link xlink:href='&jersey.javadoc.uri.prefix;/server/ApplicationHandler.html'>ApplicationHandler</link>">
<!ENTITY jersey.server.BackgroundScheduler "<link xlink:href='&jersey.javadoc.uri.prefix;/server/BackgroundScheduler.html'>@BackgroundScheduler</link>">
<!ENTITY jersey.server.BackgroundSchedulerLiteral "<link xlink:href='&jersey.javadoc.uri.prefix;/server/BackgroundSchedulerLiteral.html'>BackgroundSchedulerLiteral</link>">
diff --git a/media/jaxb/pom.xml b/media/jaxb/pom.xml
index 6586b38..484d0a4 100644
--- a/media/jaxb/pom.xml
+++ b/media/jaxb/pom.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
- Copyright (c) 2015, 2024 Oracle and/or its affiliates. All rights reserved.
+ Copyright (c) 2015, 2025 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
@@ -134,6 +134,12 @@
<scope>test</scope>
</dependency>
<dependency>
+ <groupId>org.glassfish.jersey.core</groupId>
+ <artifactId>jersey-client</artifactId>
+ <version>${jersey.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
diff --git a/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractJaxbProvider.java b/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractJaxbProvider.java
index 1d3c5c5..49e954f 100644
--- a/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractJaxbProvider.java
+++ b/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractJaxbProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2025 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
@@ -323,23 +323,25 @@
* @param annotations array of annotations that MAY contain a {@code XmlHeader} annotation instance.
*/
protected void setHeader(Marshaller marshaller, Annotation[] annotations) {
- for (Annotation a : annotations) {
- if (a instanceof XmlHeader) {
- try {
- // standalone jaxb ri
- marshaller.setProperty("org.glassfish.jaxb.xmlHeaders", ((XmlHeader) a).value());
- } catch (PropertyException e) {
+ if (annotations != null) {
+ for (Annotation a : annotations) {
+ if (a instanceof XmlHeader) {
try {
- // older name
- marshaller.setProperty("com.sun.xml.bind.xmlHeaders", ((XmlHeader) a).value());
- } catch (PropertyException ex) {
- // other jaxb implementation
- Logger.getLogger(AbstractJaxbProvider.class.getName()).log(
- Level.WARNING, "@XmlHeader annotation is not supported with this JAXB implementation."
- + " Please use JAXB RI if you need this feature.");
+ // standalone jaxb ri
+ marshaller.setProperty("org.glassfish.jaxb.xmlHeaders", ((XmlHeader) a).value());
+ } catch (PropertyException e) {
+ try {
+ // older name
+ marshaller.setProperty("com.sun.xml.bind.xmlHeaders", ((XmlHeader) a).value());
+ } catch (PropertyException ex) {
+ // other jaxb implementation
+ Logger.getLogger(AbstractJaxbProvider.class.getName()).log(
+ Level.WARNING, "@XmlHeader annotation is not supported with this JAXB implementation."
+ + " Please use JAXB RI if you need this feature.");
+ }
}
+ break;
}
- break;
}
}
}
diff --git a/media/jaxb/src/test/java/org/glassfish/jersey/jaxb/internal/AbortClientTest.java b/media/jaxb/src/test/java/org/glassfish/jersey/jaxb/internal/AbortClientTest.java
new file mode 100644
index 0000000..faf8d3a
--- /dev/null
+++ b/media/jaxb/src/test/java/org/glassfish/jersey/jaxb/internal/AbortClientTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2025 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.jaxb.internal;
+
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.core.Response;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+public class AbortClientTest {
+ public static final String MESSAGE = "hello";
+ @Test
+ void testAbortWithJaxbEntity() {
+ Client client = ClientBuilder.newBuilder()
+ .register(AbortRequestFilter.class)
+ .build();
+
+ try {
+ JaxbEntity entity = client.target("http://localhost:8080")
+ .request()
+ .get()
+ .readEntity(JaxbEntity.class);
+ MatcherAssert.assertThat(entity.getStr(), Matchers.is(MESSAGE));
+ } finally {
+ client.close();
+ }
+ }
+
+ public static class AbortRequestFilter implements ClientRequestFilter {
+
+ @Override
+ public void filter(ClientRequestContext requestContext) {
+ requestContext.abortWith(Response.ok(new JaxbEntity(MESSAGE)).build());
+ }
+
+ }
+
+ @XmlRootElement
+ public static class JaxbEntity {
+
+ @XmlElement
+ private String str;
+
+ public JaxbEntity() {}
+
+ public JaxbEntity(String str) {
+ this.str = str;
+ }
+
+ public String getStr() {
+ return str;
+ }
+ }
+}
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/JerseyClientRuntimeTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/JerseyClientRuntimeTest.java
new file mode 100644
index 0000000..32ecfce
--- /dev/null
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/JerseyClientRuntimeTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2025 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.tests.e2e.client;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import java.io.IOException;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+public class JerseyClientRuntimeTest {
+
+ private static int COUNT = 10;
+ private List<WeakReference<Object>> list = new ArrayList<>();
+ private ReferenceQueue queue = new ReferenceQueue();
+
+ @Test
+ public void testClientRuntimeInstancesAreGCed() throws InterruptedException {
+ Client c = ClientBuilder.newClient();
+ c.register(new ClientRequestFilter() {
+ @Override
+ public void filter(ClientRequestContext requestContext) throws IOException {
+ requestContext.abortWith(Response
+ .ok("<myDTO xmlns=\"http://org.example.dtos\"/>")
+ .type(MediaType.APPLICATION_XML_TYPE)
+ .build());
+ }
+ });
+
+ WebTarget target = c.target("http://localhost/nowhere");
+ for (int i = 0; i != COUNT; i++) {
+ target = target.property("SOME", "PROPERTY");
+ ClientConfig config = (ClientConfig) target.getConfiguration();
+ Object clientRuntime = getClientRuntime(config);
+ addToList(clientRuntime);
+ try (Response response = target.request().get()) {
+ MatcherAssert.assertThat(response.getStatus(), Matchers.is(200));
+ MyDTO dto = response.readEntity(MyDTO.class);
+ MatcherAssert.assertThat(dto, Matchers.notNullValue());
+ }
+ }
+
+ System.gc();
+ do {
+ Thread.sleep(100L);
+ } while (queueIsEmpty(queue));
+
+ c.close();
+
+ }
+
+ private static Object getClientRuntime(ClientConfig config) {
+ try {
+ Method m = ClientConfig.class.getDeclaredMethod("getRuntime");
+ m.setAccessible(true);
+ return m.invoke(config);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private static boolean queueIsEmpty(ReferenceQueue queue) {
+ return queue.poll() == null;
+ }
+
+ private void addToList(Object object) {
+ list.add(new WeakReference<>(object, queue));
+ }
+
+ @XmlRootElement(name = "myDTO", namespace = "http://org.example.dtos")
+ public static class MyDTO {
+
+ }
+}
diff --git a/tests/integration/jersey-5796/pom.xml b/tests/integration/jersey-5796/pom.xml
new file mode 100644
index 0000000..2710d8e
--- /dev/null
+++ b/tests/integration/jersey-5796/pom.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Copyright (c) 2025 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">
+ <parent>
+ <artifactId>project</artifactId>
+ <groupId>org.glassfish.jersey.tests.integration</groupId>
+ <version>2.47-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>jersey-5796</artifactId>
+ <name>jersey-tests-integration-jersey-5796</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+ <artifactId>jersey-test-framework-provider-bundle</artifactId>
+ <type>pom</type>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <argLine>-XX:+UseG1GC</argLine>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
\ No newline at end of file
diff --git a/tests/integration/jersey-5796/src/test/java/org/glassfish/jersey/tests/integration/jersey5796/Jersey5796Test.java b/tests/integration/jersey-5796/src/test/java/org/glassfish/jersey/tests/integration/jersey5796/Jersey5796Test.java
new file mode 100644
index 0000000..00705b1
--- /dev/null
+++ b/tests/integration/jersey-5796/src/test/java/org/glassfish/jersey/tests/integration/jersey5796/Jersey5796Test.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2025 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.tests.integration.jersey5796;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.GenericType;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ChunkedInput;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientLifecycleListener;
+import org.glassfish.jersey.client.JerseyClient;
+import org.glassfish.jersey.server.ChunkedOutput;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+
+public class Jersey5796Test extends JerseyTest {
+
+ private static final int COUNT = 50;
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(Resource.class);
+ }
+
+ @Test
+ public void testMemoryLeak() throws Exception {
+ ClientRuntimeCloseVerifier.closedClientRuntime = new AtomicInteger(0);
+ Client client = ClientBuilder.newClient(new ClientConfig(ClientRuntimeCloseVerifier.class));
+ assertEquals(0, ClientRuntimeCloseVerifier.closedClientRuntime.get());
+ for (int i = 0; i < COUNT; i++) {
+ Response response = client.target(getBaseUri()).property("test", "test").path("/get1").request().get();
+ assertEquals("GET", response.readEntity(String.class));
+ response.close();
+ }
+ System.gc();
+ do {
+ Thread.sleep(100L);
+ } while (ClientRuntimeCloseVerifier.closedClientRuntime.get() != 50);
+ assertEquals(COUNT, ClientRuntimeCloseVerifier.closedClientRuntime.get());
+ client.close();
+
+ }
+
+ /* Reproduces issue 4507
+ MultiException stack 1 of 1
+ java.lang.IllegalStateException: ServiceLocatorImpl(__HK2_Generated_0,0,427183206) has been shut down
+ at org.jvnet.hk2.internal.ServiceLocatorImpl.checkState(ServiceLocatorImpl.java:2399)
+ at org.jvnet.hk2.internal.ServiceLocatorImpl.getServiceHandleImpl(ServiceLocatorImpl.java:627)
+ at org.jvnet.hk2.internal.ServiceLocatorImpl.getServiceHandle(ServiceLocatorImpl.java:620)
+ at org.jvnet.hk2.internal.ServiceLocatorImpl.getServiceHandle(ServiceLocatorImpl.java:638)
+ at org.jvnet.hk2.internal.FactoryCreator.getFactoryHandle(FactoryCreator.java:79)
+ at org.jvnet.hk2.internal.FactoryCreator.dispose(FactoryCreator.java:149)
+ at org.jvnet.hk2.internal.SystemDescriptor.dispose(SystemDescriptor.java:521)
+ at org.glassfish.jersey.inject.hk2.RequestContext.lambda$findOrCreate$0(RequestContext.java:60)
+ at org.glassfish.jersey.internal.inject.ForeignDescriptorImpl.dispose(ForeignDescriptorImpl.java:63)
+ at org.glassfish.jersey.inject.hk2.Hk2RequestScope$Instance.remove(Hk2RequestScope.java:126)
+ at java.base/java.lang.Iterable.forEach(Iterable.java:75)
+ at org.glassfish.jersey.inject.hk2.Hk2RequestScope$Instance.release(Hk2RequestScope.java:143)
+ at org.glassfish.jersey.server.ChunkedOutput.flushQueue(ChunkedOutput.java:405)
+ at org.glassfish.jersey.server.ChunkedOutput.write(ChunkedOutput.java:264)
+ at org.glassfish.jersey.tests.integration.jersey5796.Jersey5796Test$Resource.lambda$get2$0(Jersey5796Test.java:116)
+ at java.base/java.lang.Thread.run(Thread.java:1583)
+
+ */
+ @Test
+ public void testChunkedInput() throws Exception {
+ ClientRuntimeCloseVerifier.closedClientRuntime = new AtomicInteger(0);
+ Client client = ClientBuilder.newClient(new ClientConfig(ClientRuntimeCloseVerifier.class));
+ assertEquals(0, ClientRuntimeCloseVerifier.closedClientRuntime.get());
+ for (int i = 0; i < COUNT; i++) {
+ ChunkedInput<String> chunkedInput = client.target(getBaseUri()).property("test", "test")
+ .path("/get2").request().get(new GenericType<ChunkedInput<String>>() {});
+ chunkedInput.setParser(ChunkedInput.createParser("\n"));
+ int j = 0;
+ String chunk;
+ while ((chunk = chunkedInput.read()) != null) {
+ assertEquals("Chunk " + j, chunk);
+ j++;
+ }
+ chunkedInput.close();
+ }
+ System.gc();
+ do {
+ Thread.sleep(100L);
+ } while (ClientRuntimeCloseVerifier.closedClientRuntime.get() != 50);
+ assertEquals(COUNT, ClientRuntimeCloseVerifier.closedClientRuntime.get());
+ client.close();
+ }
+
+ @Path("/")
+ public static class Resource {
+
+ @GET
+ @Path("/get1")
+ public String get1() {
+ return "GET";
+ }
+
+ @GET
+ @Path("/get2")
+ public ChunkedOutput<String> get2() {
+ ChunkedOutput<String> output = new ChunkedOutput<>(String.class);
+ new Thread(() -> {
+ try {
+ for (int i = 0; i < 3; i++) {
+ output.write("Chunk " + i + "\n");
+ }
+ } catch (Exception e1) {
+ e1.printStackTrace();
+ } finally {
+ try {
+ output.close();
+ } catch (Exception e2) {
+ e2.printStackTrace();
+ }
+ }
+ }).start();
+ return output;
+ }
+ }
+
+ public static class ClientRuntimeCloseVerifier implements ClientLifecycleListener {
+
+ private static AtomicInteger closedClientRuntime;
+
+ @Override
+ public void onInit() {
+ }
+
+ @Override
+ public void onClose() {
+ closedClientRuntime.incrementAndGet();
+ }
+ }
+}
\ No newline at end of file