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