Merge remote-tracking branch 3.0 into 3.1
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 4294bae..43b5bed 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 @@ -2298,6 +2298,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 5fe6f8a..614c765 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 @@ -592,6 +592,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>" > @@ -603,6 +604,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 b19a305..35804fd 100644 --- a/media/jaxb/pom.xml +++ b/media/jaxb/pom.xml
@@ -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 0b95efd..493f8b7 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, 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 @@ -295,7 +295,6 @@ } finally { jaxbContextsLock.unlock(); } - } /** @@ -330,23 +329,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