diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java index 471321ff06..6072a52c1d 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 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 @@ -30,20 +30,21 @@ class Expect100ContinueConnectorExtension implements ConnectorExtension { + + private final NettyConnectorProvider.Config.RW requestConfiguration; + + Expect100ContinueConnectorExtension(NettyConnectorProvider.Config.RW requestConfiguration) { + this.requestConfiguration = requestConfiguration; + } + private static final String EXCEPTION_MESSAGE = "Server rejected operation"; @Override public void invoke(ClientRequest request, HttpRequest extensionParam) { final long length = request.getLengthLong(); - final RequestEntityProcessing entityProcessing = request.resolveProperty( - ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.class); - - final Boolean expectContinueActivated = request.resolveProperty( - ClientProperties.EXPECT_100_CONTINUE, Boolean.class); - final Long expectContinueSizeThreshold = request.resolveProperty( - ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, - ClientProperties.DEFAULT_EXPECT_100_CONTINUE_THRESHOLD_SIZE); - + final RequestEntityProcessing entityProcessing = requestConfiguration.requestEntityProcessing(request); + final Boolean expectContinueActivated = requestConfiguration.expect100Continue(request); + final long expectContinueSizeThreshold = requestConfiguration.expect100ContinueThreshold(request); final boolean allowStreaming = length > expectContinueSizeThreshold || entityProcessing == RequestEntityProcessing.CHUNKED; 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 45a36972d3..01671eecfe 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 @@ -28,7 +28,6 @@ import javax.ws.rs.core.Response; -import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.ClientResponse; import org.glassfish.jersey.http.HttpHeaders; @@ -53,17 +52,14 @@ */ class JerseyClientHandler extends SimpleChannelInboundHandler { - private static final int DEFAULT_MAX_REDIRECTS = 5; - // Modified only by the same thread. No need to synchronize it. private final Set redirectUriHistory; private final ClientRequest jerseyRequest; private final CompletableFuture responseAvailable; private final CompletableFuture responseDone; - private final boolean followRedirects; - private final int maxRedirects; private final NettyConnector connector; private final NettyHttpRedirectController redirectController; + private final NettyConnectorProvider.Config.RW requestConfiguration; private NettyInputStream nis; private ClientResponse jerseyResponse; @@ -71,19 +67,20 @@ class JerseyClientHandler extends SimpleChannelInboundHandler { private boolean readTimedOut; JerseyClientHandler(ClientRequest request, CompletableFuture responseAvailable, - CompletableFuture responseDone, Set redirectUriHistory, NettyConnector connector) { + CompletableFuture responseDone, Set redirectUriHistory, NettyConnector connector, + NettyConnectorProvider.Config.RW requestConfiguration) { this.redirectUriHistory = redirectUriHistory; this.jerseyRequest = request; this.responseAvailable = responseAvailable; this.responseDone = responseDone; - // Follow redirects by default - this.followRedirects = jerseyRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, true); - this.maxRedirects = jerseyRequest.resolveProperty(NettyClientProperties.MAX_REDIRECTS, DEFAULT_MAX_REDIRECTS); + this.requestConfiguration = requestConfiguration; this.connector = connector; + // Follow redirects by default + requestConfiguration.followRedirects(jerseyRequest); + requestConfiguration.maxRedirects(jerseyRequest); - final NettyHttpRedirectController customRedirectController = jerseyRequest - .resolveProperty(NettyClientProperties.HTTP_REDIRECT_CONTROLLER, NettyHttpRedirectController.class); - this.redirectController = customRedirectController == null ? new NettyHttpRedirectController() : customRedirectController; + this.redirectController = requestConfiguration.redirectController(jerseyRequest); + this.redirectController.init(requestConfiguration); } @Override @@ -109,7 +106,7 @@ protected void notifyResponse(ChannelHandlerContext ctx) { ClientResponse cr = jerseyResponse; jerseyResponse = null; int responseStatus = cr.getStatus(); - if (followRedirects + if (Boolean.TRUE.equals(requestConfiguration.followRedirects()) && (responseStatus == ResponseStatus.Redirect3xx.MOVED_PERMANENTLY_301.getStatusCode() || responseStatus == ResponseStatus.Redirect3xx.FOUND_302.getStatusCode() || responseStatus == ResponseStatus.Redirect3xx.SEE_OTHER_303.getStatusCode() @@ -136,16 +133,17 @@ protected void notifyResponse(ChannelHandlerContext ctx) { // infinite loop detection responseAvailable.completeExceptionally( new RedirectException(LocalizationMessages.REDIRECT_INFINITE_LOOP())); - } else if (redirectUriHistory.size() > maxRedirects) { + } else if (redirectUriHistory.size() > requestConfiguration.maxRedirects.get()) { // maximal number of redirection - responseAvailable.completeExceptionally( - new RedirectException(LocalizationMessages.REDIRECT_LIMIT_REACHED(maxRedirects))); + responseAvailable.completeExceptionally(new RedirectException( + LocalizationMessages.REDIRECT_LIMIT_REACHED(requestConfiguration.maxRedirects.get()))); } else { ClientRequest newReq = new ClientRequest(jerseyRequest); newReq.setUri(newUri); ctx.close(); if (redirectController.prepareRedirect(newReq, cr)) { - final NettyConnector newConnector = new NettyConnector(newReq.getClient()); + final NettyConnector newConnector = + new NettyConnector(newReq.getClient(), connector.connectorConfiguration); newConnector.execute(newReq, redirectUriHistory, new CompletableFuture() { @Override public boolean complete(ClientResponse value) { 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 4b2e4c97a5..98f7a4c9f4 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 @@ -157,7 +157,8 @@ public class NettyClientProperties { DEFAULT_HEADER_SIZE = 8192; /** - * Parameter which allows extending of the initial line length for the Netty connector + * Parameter which allows extending of the first line length of the HTTP header for the Netty connector. + * Taken from {@link io.netty.handler.codec.http.HttpClientCodec#HttpClientCodec(int, int, int)}. * * @since 2.44 */ @@ -166,12 +167,12 @@ public class NettyClientProperties { /** * Default initial line length for Netty Connector. - * Taken from {@link io.netty.handler.codec.http.HttpClientCodec#HttpClientCodec(int, int, int)} + * Typically, set this to the same value as {@link #MAX_HEADER_SIZE}. * * @since 2.44 */ public static final Integer - DEFAULT_INITIAL_LINE_LENGTH = 4096; + DEFAULT_INITIAL_LINE_LENGTH = 8192; /** * Parameter which allows extending of the chunk size for the Netty connector diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectionController.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectionController.java new file mode 100644 index 0000000000..5d8725c42a --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectionController.java @@ -0,0 +1,39 @@ +/* + * 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 java.net.URI; + +/** + * Adjustable connection pooling controller. + */ +public class NettyConnectionController { + /** + * Get the group of connections to be pooled, purged idle, and reused together. + * + * @param clientRequest the HTTP client request. + * @param uri the uri for the HTTP client request. + * @param hostName the hostname for the request. Can differ from the hostname in the uri based on other request attributes. + * @param port the real port for the request. Can differ from the port in the uri based on other request attributes. + * @return the group of connections identifier. + */ + public String getConnectionGroup(ClientRequest clientRequest, URI uri, String hostName, int port) { + return uri.getScheme() + "://" + hostName + ":" + port; + } +} 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 06c1efd8c4..42368ea490 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 @@ -18,8 +18,6 @@ import java.io.IOException; import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.SocketAddress; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; @@ -36,9 +34,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.function.Supplier; -import javax.net.ssl.SSLContext; import javax.ws.rs.ProcessingException; import javax.ws.rs.client.Client; import javax.ws.rs.core.Configuration; @@ -55,10 +51,8 @@ import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.DefaultFullHttpRequest; -import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.HttpChunkedInput; -import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; @@ -67,8 +61,6 @@ import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; -import io.netty.handler.proxy.HttpProxyHandler; -import io.netty.handler.proxy.ProxyHandler; import io.netty.handler.ssl.ApplicationProtocolConfig; import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.IdentityCipherSuiteFilter; @@ -80,7 +72,6 @@ import io.netty.handler.timeout.IdleStateHandler; import io.netty.resolver.NoopAddressResolverGroup; import io.netty.util.concurrent.GenericFutureListener; -import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.ClientResponse; import org.glassfish.jersey.client.innate.ClientProxy; @@ -105,6 +96,7 @@ class NettyConnector implements Connector { final EventLoopGroup group; final Client client; final HashMap> connections = new HashMap<>(); + final NettyConnectorProvider.Config.RW connectorConfiguration; private static final LazyValue NETTY_VERSION = Values.lazy( (Value) () -> { @@ -117,63 +109,29 @@ class NettyConnector implements Connector { return "Netty " + nettyVersion; }); - // If HTTP keepalive is enabled the value of "http.maxConnections" determines the maximum number - // of idle connections that will be simultaneously kept alive, per destination. - private static final String HTTP_KEEPALIVE_STRING = System.getProperty("http.keepAlive"); - // http.keepalive (default: true) - private static final Boolean HTTP_KEEPALIVE = - HTTP_KEEPALIVE_STRING == null ? Boolean.TRUE : Boolean.parseBoolean(HTTP_KEEPALIVE_STRING); - - // http.maxConnections (default: 5) - private static final int DEFAULT_MAX_POOL_SIZE = 5; - private static final int MAX_POOL_SIZE = Integer.getInteger("http.maxConnections", DEFAULT_MAX_POOL_SIZE); - private static final int DEFAULT_MAX_POOL_IDLE = 60; // seconds - private static final int DEFAULT_MAX_POOL_SIZE_TOTAL = 60; // connections - - - private final Integer maxPoolSize; // either from system property, or from Jersey config, or default - private final Integer maxPoolSizeTotal; //either from Jersey config, or default - private final Integer maxPoolIdle; // either from Jersey config, or default - static final String INACTIVE_POOLED_CONNECTION_HANDLER = "inactive_pooled_connection_handler"; private static final String PRUNE_INACTIVE_POOL = "prune_inactive_pool"; private static final String READ_TIMEOUT_HANDLER = "read_timeout_handler"; private static final String REQUEST_HANDLER = "request_handler"; private static final String EXPECT_100_CONTINUE_HANDLER = "expect_100_continue_handler"; - NettyConnector(Client client) { + NettyConnector(Client client) { // TODO drop + this(client, NettyConnectorProvider.config().rw()); + } - final Configuration configuration = client.getConfiguration(); - final Map properties = configuration.getProperties(); - final Object threadPoolSize = properties.get(ClientProperties.ASYNC_THREADPOOL_SIZE); + NettyConnector(Client client, NettyConnectorProvider.Config.RW connectorConfiguration) { + this.client = client; + this.connectorConfiguration = connectorConfiguration.fromClient(client); - if (threadPoolSize != null && threadPoolSize instanceof Integer && (Integer) threadPoolSize > 0) { - executorService = VirtualThreadUtil.withConfig(configuration).newFixedThreadPool((Integer) threadPoolSize); - this.group = new NioEventLoopGroup((Integer) threadPoolSize); + final Configuration configuration = client.getConfiguration(); + final Integer threadPoolSize = this.connectorConfiguration.asyncThreadPoolSize(); + if (threadPoolSize != null && threadPoolSize > 0) { + executorService = VirtualThreadUtil.withConfig(configuration).newFixedThreadPool(threadPoolSize); + this.group = new NioEventLoopGroup(threadPoolSize); } else { executorService = VirtualThreadUtil.withConfig(configuration).newCachedThreadPool(); this.group = new NioEventLoopGroup(); } - - this.client = client; - - final Object maxPoolSizeTotalProperty = properties.get(NettyClientProperties.MAX_CONNECTIONS_TOTAL); - final Object maxPoolIdleProperty = properties.get(NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT); - final Object maxPoolSizeProperty = properties.get(NettyClientProperties.MAX_CONNECTIONS); - - maxPoolSizeTotal = maxPoolSizeTotalProperty != null ? (Integer) maxPoolSizeTotalProperty : DEFAULT_MAX_POOL_SIZE_TOTAL; - maxPoolIdle = maxPoolIdleProperty != null ? (Integer) maxPoolIdleProperty : DEFAULT_MAX_POOL_IDLE; - maxPoolSize = maxPoolSizeProperty != null - ? (Integer) maxPoolSizeProperty - : (HTTP_KEEPALIVE ? MAX_POOL_SIZE : DEFAULT_MAX_POOL_SIZE); - - if (maxPoolSizeTotal < 0) { - throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_TOTAL(maxPoolSizeTotal)); - } - - if (maxPoolSize < 0) { - throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_SIZE(maxPoolSize)); - } } @Override @@ -206,25 +164,31 @@ public Future apply(final ClientRequest jerseyRequest, final AsyncConnectorCa protected void execute(final ClientRequest jerseyRequest, final Set redirectUriHistory, final CompletableFuture responseAvailable) { - Integer timeout = jerseyRequest.resolveProperty(ClientProperties.READ_TIMEOUT, 0); - final Integer expect100ContinueTimeout = jerseyRequest.resolveProperty( - NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, - NettyClientProperties.DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE); - if (timeout == null || timeout < 0) { - throw new ProcessingException(LocalizationMessages.WRONG_READ_TIMEOUT(timeout)); + final NettyConnectorProvider.Config.RW requestConfiguration = + connectorConfiguration + .copy() + .readTimeout(jerseyRequest) + .expect100ContinueTimeout(jerseyRequest); + final int readTimeout = requestConfiguration.readTimeout(); + if (readTimeout < 0) { + throw new ProcessingException(LocalizationMessages.WRONG_READ_TIMEOUT(readTimeout)); } final CompletableFuture responseDone = new CompletableFuture<>(); final URI requestUri = jerseyRequest.getUri(); - String host = requestUri.getHost(); - int port = requestUri.getPort() != -1 ? requestUri.getPort() : "https".equals(requestUri.getScheme()) ? 443 : 80; + final String host = requestUri.getHost(); + final int port = requestUri.getPort() != -1 + ? requestUri.getPort() + : "https".equalsIgnoreCase(requestUri.getScheme()) ? 443 : 80; try { final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder() .request(jerseyRequest).setSNIAlways(true).setSNIHostName(jerseyRequest).build(); - String key = requestUri.getScheme() + "://" + sslConfig.getSNIHostName() + ":" + port; + final String key = requestConfiguration + .connectionController() + .getConnectionGroup(jerseyRequest, requestUri, sslConfig.getSNIHostName(), port); ArrayList conns; synchronized (connections) { conns = connections.get(key); @@ -244,8 +208,8 @@ protected void execute(final ClientRequest jerseyRequest, final Set redirec } catch (NoSuchElementException e) { /* * Eat it. - * It could happen that the channel was closed, pipeline cleared and - * then it will fail to remove the names with this exception. + * It could happen that the channel was closed, pipeline cleared, + * and then it will fail to remove the names with this exception. */ } if (!chan.isOpen()) { @@ -257,20 +221,15 @@ protected void execute(final ClientRequest jerseyRequest, final Set redirec final JerseyExpectContinueHandler expect100ContinueHandler = new JerseyExpectContinueHandler(); if (chan == null) { - Integer connectTimeout = jerseyRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, 0); + requestConfiguration.connectTimeout(jerseyRequest); Bootstrap b = new Bootstrap(); // http proxy - Optional proxy = ClientProxy.proxyFromRequest(jerseyRequest); - if (!proxy.isPresent()) { - proxy = ClientProxy.proxyFromProperties(requestUri); - } - proxy.ifPresent(clientProxy -> { + final Optional handlerProxy = requestConfiguration.proxy(jerseyRequest, requestUri); + handlerProxy.ifPresent(clientProxy -> { b.resolver(NoopAddressResolverGroup.INSTANCE); // request hostname resolved by the HTTP proxy }); - final Optional handlerProxy = proxy; - b.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer() { @@ -282,19 +241,14 @@ protected void initChannel(SocketChannel ch) throws Exception { // http proxy handlerProxy.ifPresent(clientProxy -> { - final URI u = clientProxy.uri(); - InetSocketAddress proxyAddr = new InetSocketAddress(u.getHost(), - u.getPort() == -1 ? 8080 : u.getPort()); - ProxyHandler proxy1 = createProxyHandler(jerseyRequest, proxyAddr, - clientProxy.userName(), clientProxy.password(), connectTimeout); - p.addLast(proxy1); + p.addLast(requestConfiguration.createProxyHandler(clientProxy, jerseyRequest)); }); // Enable HTTPS if necessary. if ("https".equals(requestUri.getScheme())) { // making client authentication optional for now; it could be extracted to configurable property JdkSslContext jdkSslContext = new JdkSslContext( - getSslContext(client, jerseyRequest), + requestConfiguration.sslContext(client, jerseyRequest), true, (Iterable) null, IdentityCipherSuiteFilter.INSTANCE, @@ -309,8 +263,7 @@ protected void initChannel(SocketChannel ch) throws Exception { final SslHandler sslHandler = jdkSslContext.newHandler( ch.alloc(), sslConfig.getSNIHostName(), port <= 0 ? 443 : port, executorService ); - if (ClientProperties.getValue(config.getProperties(), - NettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION, true)) { + if (requestConfiguration.isSslHostnameVerificationEnabled(config.getProperties())) { sslConfig.setEndpointIdentificationAlgorithm(sslHandler.engine()); } @@ -319,16 +272,7 @@ protected void initChannel(SocketChannel ch) throws Exception { p.addLast(sslHandler); } - final Integer maxHeaderSize = ClientProperties.getValue(config.getProperties(), - NettyClientProperties.MAX_HEADER_SIZE, - NettyClientProperties.DEFAULT_HEADER_SIZE); - final Integer maxChunkSize = ClientProperties.getValue(config.getProperties(), - NettyClientProperties.MAX_CHUNK_SIZE, - NettyClientProperties.DEFAULT_CHUNK_SIZE); - final Integer maxInitialLineLength = ClientProperties.getValue(config.getProperties(), - NettyClientProperties.MAX_INITIAL_LINE_LENGTH, - NettyClientProperties.DEFAULT_INITIAL_LINE_LENGTH); - p.addLast(new HttpClientCodec(maxInitialLineLength, maxHeaderSize, maxChunkSize)); + p.addLast(requestConfiguration.createHttpClientCodec(config.getProperties())); p.addLast(EXPECT_100_CONTINUE_HANDLER, expect100ContinueHandler); p.addLast(new ChunkedWriteHandler()); p.addLast(new HttpContentDecompressor()); @@ -336,8 +280,8 @@ protected void initChannel(SocketChannel ch) throws Exception { }); // connect timeout - if (connectTimeout > 0) { - b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout); + if (requestConfiguration.connectTimeout() > 0) { + b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, requestConfiguration.connectTimeout()); } // Make the connection attempt. @@ -356,12 +300,12 @@ protected void initChannel(SocketChannel ch) throws Exception { // assert: it is ok to abort the entire response, if responseDone is completed exceptionally - in particular, nothing // will leak final Channel ch = chan; - JerseyClientHandler clientHandler = - new JerseyClientHandler(jerseyRequest, responseAvailable, responseDone, redirectUriHistory, this); + JerseyClientHandler clientHandler = new JerseyClientHandler( + jerseyRequest, responseAvailable, responseDone, redirectUriHistory, this, requestConfiguration); // read timeout makes sense really as an inactivity timeout ch.pipeline().addLast(READ_TIMEOUT_HANDLER, - new IdleStateHandler(0, 0, timeout, TimeUnit.MILLISECONDS)); + new IdleStateHandler(0, 0, requestConfiguration.readTimeout(), TimeUnit.MILLISECONDS)); ch.pipeline().addLast(REQUEST_HANDLER, clientHandler); responseDone.whenComplete((_r, th) -> { @@ -369,7 +313,8 @@ protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().remove(clientHandler); if (th == null) { - ch.pipeline().addLast(INACTIVE_POOLED_CONNECTION_HANDLER, new IdleStateHandler(0, 0, maxPoolIdle)); + ch.pipeline().addLast(INACTIVE_POOLED_CONNECTION_HANDLER, + new IdleStateHandler(0, 0, requestConfiguration.maxPoolIdle.get())); ch.pipeline().addLast(PRUNE_INACTIVE_POOL, new PruneIdlePool(connections, key)); boolean added = true; synchronized (connections) { @@ -380,7 +325,9 @@ protected void initChannel(SocketChannel ch) throws Exception { connections.put(key, conns1); } else { synchronized (conns1) { - if ((maxPoolSizeTotal == 0 || connections.size() < maxPoolSizeTotal) && conns1.size() < maxPoolSize) { + if ((requestConfiguration.maxPoolSizeTotal.get() == 0 + || connections.size() < requestConfiguration.maxPoolSizeTotal.get()) + && conns1.size() < requestConfiguration.maxPoolSize.get()) { conns1.add(ch); } else { // else do not add the Channel to the idle pool added = false; @@ -432,7 +379,7 @@ public void operationComplete(io.netty.util.concurrent.Future futu }; ch.closeFuture().addListener(closeListener); - final NettyEntityWriter entityWriter = nettyEntityWriter(jerseyRequest, ch); + final NettyEntityWriter entityWriter = nettyEntityWriter(jerseyRequest, ch, requestConfiguration); switch (entityWriter.getType()) { case CHUNKED: HttpUtil.setTransferEncodingChunked(nettyRequest, true); @@ -492,7 +439,7 @@ public void run() { }); headersSet.await(); - new Expect100ContinueConnectorExtension().invoke(jerseyRequest, nettyRequest); + new Expect100ContinueConnectorExtension(requestConfiguration).invoke(jerseyRequest, nettyRequest); boolean continueExpected = HttpUtil.is100ContinueExpected(nettyRequest); boolean expectationsFailed = false; @@ -502,7 +449,7 @@ public void run() { expect100ContinueHandler.attachCountDownLatch(expect100ContinueLatch); //send expect request, sync and wait till either response or timeout received entityWriter.writeAndFlush(nettyRequest); - expect100ContinueLatch.await(expect100ContinueTimeout, TimeUnit.MILLISECONDS); + expect100ContinueLatch.await(requestConfiguration.expect100ContTimeout.get(), TimeUnit.MILLISECONDS); try { expect100ContinueHandler.processExpectationStatus(); } catch (TimeoutException e) { @@ -542,13 +489,9 @@ public void run() { } } - /* package */ NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel) { - return NettyEntityWriter.getInstance(clientRequest, channel); - } - - private SSLContext getSslContext(Client client, ClientRequest request) { - Supplier supplier = request.resolveProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, Supplier.class); - return supplier == null ? client.getSslContext() : supplier.get(); + /* package */ NettyEntityWriter nettyEntityWriter( + ClientRequest clientRequest, Channel channel, NettyConnectorProvider.Config.RW requestConfiguration) { + return NettyEntityWriter.getInstance(clientRequest, channel, requestConfiguration); } private String buildPathWithQueryParameters(URI requestUri) { @@ -601,21 +544,7 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc } } - private static ProxyHandler createProxyHandler(ClientRequest jerseyRequest, SocketAddress proxyAddr, - String userName, String password, long connectTimeout) { - final Boolean filter = jerseyRequest.resolveProperty(NettyClientProperties.FILTER_HEADERS_FOR_PROXY, Boolean.TRUE); - HttpHeaders httpHeaders = setHeaders(jerseyRequest, new DefaultHttpHeaders(), Boolean.TRUE.equals(filter)); - - ProxyHandler proxy = userName == null ? new HttpProxyHandler(proxyAddr, httpHeaders) - : new HttpProxyHandler(proxyAddr, userName, password, httpHeaders); - if (connectTimeout > 0) { - proxy.setConnectTimeoutMillis(connectTimeout); - } - - return proxy; - } - - private static HttpHeaders setHeaders(ClientRequest jerseyRequest, HttpHeaders headers, boolean proxyOnly) { + /* package */ static HttpHeaders setHeaders(ClientRequest jerseyRequest, HttpHeaders headers, boolean proxyOnly) { for (final Map.Entry> e : jerseyRequest.getStringHeaders().entrySet()) { final String key = e.getKey(); if (!proxyOnly || JerseyClientHandler.ProxyHeaders.INSTANCE.test(key) || additionalProxyHeadersToKeep(key)) { diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorConfiguration.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorConfiguration.java new file mode 100644 index 0000000000..24dd3283cf --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorConfiguration.java @@ -0,0 +1,436 @@ +/* + * 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.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.proxy.HttpProxyHandler; +import io.netty.handler.proxy.ProxyHandler; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.innate.ClientProxy; +import org.glassfish.jersey.internal.util.collection.Ref; +import org.glassfish.jersey.netty.connector.internal.ConnectorConfiguration; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.Map; + +class NettyConnectorConfiguration> extends ConnectorConfiguration { + + /* package */ NullableRef connectionController = NullableRef.empty(); + /* package */ NullableRef enableHostnameVerification = NullableRef.of(Boolean.TRUE); + /* package */ Ref expect100ContTimeout = NullableRef.of( + NettyClientProperties.DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE); + /* package */ NullableRef filterHeadersForProxy = NullableRef.of(Boolean.TRUE); + /* package */ NullableRef firstHttpHeaderLineLength = NullableRef.of( + NettyClientProperties.DEFAULT_INITIAL_LINE_LENGTH); + /* package */ NullableRef idleConnections = NullableRef.empty(); + /* package */ NullableRef maxChunkSize = NullableRef.of(NettyClientProperties.DEFAULT_CHUNK_SIZE); + /* package */ NullableRef maxHeaderSize = NullableRef.of(NettyClientProperties.DEFAULT_HEADER_SIZE); + // either from Jersey config, or default + /* package */ Ref maxPoolSizeTotal = NullableRef.of(DEFAULT_MAX_POOL_SIZE_TOTAL); + // either from Jersey config, or default + /* package */ Ref maxPoolIdle = NullableRef.of(DEFAULT_MAX_POOL_IDLE); + // either from system property, or from Jersey config, or default + /* package */ Ref maxPoolSize = NullableRef.of(HTTP_KEEPALIVE ? MAX_POOL_SIZE : DEFAULT_MAX_POOL_SIZE); + /* package */ Ref maxRedirects = NullableRef.of(DEFAULT_MAX_REDIRECTS); + /* package */ NullableRef preserveMethodOnRedirect = NullableRef.of(Boolean.TRUE); + /* package */ NullableRef redirectController = NullableRef.empty(); + + // If HTTP keepalive is enabled the value of "http.maxConnections" determines the maximum number + // of idle connections that will be simultaneously kept alive, per destination. + private static final String HTTP_KEEPALIVE_STRING = System.getProperty("http.keepAlive"); + // http.keepalive (default: true) + private static final Boolean HTTP_KEEPALIVE = + HTTP_KEEPALIVE_STRING == null ? Boolean.TRUE : Boolean.parseBoolean(HTTP_KEEPALIVE_STRING); + + // http.maxConnections (default: 5) + private static final int DEFAULT_MAX_POOL_SIZE = 5; + private static final int MAX_POOL_SIZE = Integer.getInteger("http.maxConnections", DEFAULT_MAX_POOL_SIZE); + private static final int DEFAULT_MAX_POOL_IDLE = 60; // seconds + private static final int DEFAULT_MAX_POOL_SIZE_TOTAL = 60; // connections + + private static final int DEFAULT_MAX_REDIRECTS = 5; + + @Override + protected > void setNonEmpty(X otherCC) { + NettyConnectorConfiguration other = (NettyConnectorConfiguration) otherCC; + super.setNonEmpty(other); + this.connectionController.setNonEmpty(other.connectionController); + this.redirectController.setNonEmpty(other.redirectController); + this.connectionController.setNonEmpty(other.connectionController); + this.enableHostnameVerification.setNonEmpty(other.enableHostnameVerification); + ((NullableRef) this.expect100ContTimeout).setNonEmpty((NullableRef) other.expect100ContTimeout); + this.filterHeadersForProxy.setNonEmpty(other.filterHeadersForProxy); + this.firstHttpHeaderLineLength.setNonEmpty(other.firstHttpHeaderLineLength); + this.idleConnections.setNonEmpty(other.idleConnections); + this.maxChunkSize.setNonEmpty(other.maxChunkSize); + this.maxHeaderSize.setNonEmpty(other.maxHeaderSize); + ((NullableRef) this.maxPoolIdle).setNonEmpty((NullableRef) other.maxPoolIdle); + ((NullableRef) this.maxPoolSize).setNonEmpty((NullableRef) other.maxPoolSize); + ((NullableRef) this.maxPoolSizeTotal).setNonEmpty((NullableRef) other.maxPoolSizeTotal); + ((NullableRef) this.maxRedirects).setNonEmpty((NullableRef) other.maxRedirects); + this.preserveMethodOnRedirect.setNonEmpty(other.preserveMethodOnRedirect); + this.redirectController.setNonEmpty(other.redirectController); + } + + /** + * Set the connection pooling controller for the Netty Connector. + * + * @param controller the connection pooling controller. + * @return updated configuration. + */ + public N connectorController(NettyConnectionController controller) { + connectionController.set(controller); + return self(); + } + + /** + * This setting determines waiting time in milliseconds for 100-Continue response when 100-Continue is sent by the client. + * The property {@link NettyClientProperties#EXPECT_100_CONTINUE_TIMEOUT} has precedence over this setting. + * + * @param millis the timeout for 100-Continue response. + * @return updated configuration. + */ + public N expect100ContinueTimeout(int millis) { + expect100ContTimeout.set(millis); + return self(); + } + + /** + * Enable or disable the endpoint identification algorithm to HTTPS. The property + * {@link NettyClientProperties#ENABLE_SSL_HOSTNAME_VERIFICATION} has over this setting. + * + * @param enable enable or disable the hostname verification. + * @return updated configuration. + */ + public N enableSslHostnameVerification(boolean enable) { + enableHostnameVerification.set(enable); + return self(); + } + + /** + * Filter the HTTP headers for requests (CONNECT) towards the proxy except for PROXY-prefixed + * and HOST headers when {@code true}. + * The property {@link NettyClientProperties#FILTER_HEADERS_FOR_PROXY} has precedence over this setting. + * + * @param filter to filter or not. The default is {@code true}. + * @return updated configuration. + */ + public N filterHeadersForProxy(boolean filter) { + filterHeadersForProxy.set(filter); + return self(); + } + + /** + * 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. The property {@link NettyClientProperties#IDLE_CONNECTION_PRUNE_TIMEOUT} + * has precedence over this setting. + * + * @param seconds the timeout in seconds. + * @return updated configuration. + */ + public N idleConnectionPruneTimeout(int seconds) { + maxPoolIdle.set(seconds); + return self(); + } + + /** + * Set the maximum length of the first line of the HTTP header. + * The property {@link NettyClientProperties#MAX_INITIAL_LINE_LENGTH} has precedence over this setting. + * + * @param length the length of the first line of the HTTP header. + * @return updated configuration. + */ + public N initialHttpHeaderLineLength(int length) { + firstHttpHeaderLineLength.set(length); + return self(); + } + + /** + * Set the maximum chunk size for the Netty connector. The property {@link NettyClientProperties#MAX_CHUNK_SIZE} + * has precedence over this setting. + * + * @param size the new size of chunks. + * @return updated configuration. + */ + public N maxChunkSize(int size) { + maxChunkSize.set(size); + return self(); + } + + /** + * This setting determines the maximum number of idle connections that will be simultaneously kept alive, per destination. + * The default is 5. The property {@link NettyClientProperties#MAX_CONNECTIONS} takes precedence over this setting. + * + * @param maxCount maximum number of idle connections per destination. + * @return updated configuration. + */ + public N maxConnectionsPerDestination(int maxCount) { + idleConnections.set(maxCount); + return self(); + } + + /** + * Set the maximum header size in bytes for the HTTP headers processed by Netty. + * The property {@link NettyClientProperties#MAX_HEADER_SIZE} has precedence over this setting. + * + * @param size the new maximum header size. + * @return updated configuration. + */ + public N maxHeaderSize(int size) { + maxHeaderSize.set(size); + return self(); + } + + /** + * Set the maximum number of redirects to prevent infinite redirect loop. The default is 5. + * The property {@link NettyClientProperties#MAX_REDIRECTS} has precedence over this setting. + * + * @param max the maximum number of redirects. + * @return updated configuration. + */ + public N maxRedirects(int max) { + maxRedirects.set(max); + return self(); + } + + /** + * Set the maximum number of idle connections that will be simultaneously kept alive. The property + * {@link NettyClientProperties#MAX_CONNECTIONS_TOTAL} has precedence over this setting. + * + * @param max the maximum number of idle connections. + * @return updated configuration. + */ + public N maxTotalConnection(int max) { + maxPoolSizeTotal.set(max); + return self(); + } + + /** + * Set the preservation of methods during HTTP redirect. + * By default, the HTTP POST request are not transformed into HTTP GET for status 301 & 302. + * The property {@link NettyClientProperties#PRESERVE_METHOD_ON_REDIRECT} has precedence over this setting. + * + * @param preserve to preserve or not to preserve. + * @return updated configuration. + */ + public N preserveMethodOnRedirect(boolean preserve) { + preserveMethodOnRedirect.set(preserve); + return self(); + } + + /** + * Set the Netty Connector HTTP redirect controller. + * The property {@link NettyClientProperties#HTTP_REDIRECT_CONTROLLER} has precedence over this setting. + * + * @param controller the HTTP redirect controller. + * @return updated configuration. + */ + public N redirectController(NettyHttpRedirectController controller) { + redirectController.set(controller); + return self(); + } + + @SuppressWarnings("unchecked") + protected N self() { + return (N) this; + } + + abstract static class ReadWrite> + extends NettyConnectorConfiguration + implements ConnectorConfiguration.ReadWrite { + + /** + * Get the preset {@link NettyConnectionController} or create an instance of the default one, if not preset. + * @return the {@link NettyConnectionController} instance. + */ + /* package */ NettyConnectionController connectionController() { + return connectionController.isPresent() ? connectionController.get() : new NettyConnectionController(); + } + + /** + * Update {@link #expect100ContinueTimeout(int) expect 100-Continue timeout} based on current http request properties. + * + * @param clientRequest the current http client request. + * @return updated configuration. + */ + /* package */ N expect100ContinueTimeout(ClientRequest clientRequest) { + expect100ContTimeout.set(clientRequest.resolveProperty( + NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, + expect100ContTimeout.get())); + return this.self(); + } + + /** + * Return value of {@link #enableSslHostnameVerification(boolean)} setting either from the configuration of from the + * HTTP client request properties. The default is {@code true}. + * + * @param properties the HTTP client request properties. + * @return the value of SSL hostname verification setting. + */ + /* package */ boolean isSslHostnameVerificationEnabled(Map properties) { + return ClientProperties.getValue(properties, + NettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION, + enableHostnameVerification.get()); + } + + /** + * Update {@link #maxRedirects(int)} value from the HTTP Client request. + * @param request the HTTP Client request. + * @return maximum redirects value. + */ + /* package */ int maxRedirects(ClientRequest request) { + maxRedirects.set(request.resolveProperty(NettyClientProperties.MAX_REDIRECTS, maxRedirects.get())); + return maxRedirects.get(); + } + + /** + * Update the {@link #preserveMethodOnRedirect(boolean) preservation} of HTTP method during HTTP redirect + * by HTTP client request properties. + * + * @param request HTTP client request. + * @return the value of preservation. + */ + /* package */ boolean preserveMethodOnRedirect(ClientRequest request) { + preserveMethodOnRedirect.set( + request.resolveProperty(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, preserveMethodOnRedirect.get())); + return preserveMethodOnRedirect.get(); + } + + /** + * Get the instance of preset {@link NettyHttpRedirectController} either from configuration, + * or from the HTTP client request, or the default if non set. + * @param request the HTTP client request. + * @return an instance of {@link NettyHttpRedirectController}. + */ + /* package */ NettyHttpRedirectController redirectController(ClientRequest request) { + NettyHttpRedirectController customRedirectController = + request.resolveProperty(NettyClientProperties.HTTP_REDIRECT_CONTROLLER, NettyHttpRedirectController.class); + if (customRedirectController == null) { + customRedirectController = redirectController.get(); + } + if (customRedirectController == null) { + customRedirectController = new NettyHttpRedirectController(); + } + + return customRedirectController; + } + + /** + *

+ * Return a new instance of configuration updated by the merged settings from this and client properties. + * Only properties unresolved during the request are update. + *

+ * {@code This} is meant to be settings from the connector. + * The priorities should go DEFAULTS -> SYSTEM -> CONNECTOR -> CLIENT -> REQUEST + *

+ * + * @param client the REST client. + * @return a new instance of configuration. + */ + /* package */ N fromClient(Client client) { + final Map properties = client.getConfiguration().getProperties(); + final N clientConfiguration = copy(); + + final Object threadPoolSize = properties.get(ClientProperties.ASYNC_THREADPOOL_SIZE); + if (threadPoolSize instanceof Integer && (Integer) threadPoolSize > 0) { + clientConfiguration.asyncThreadPoolSize((Integer) threadPoolSize); + } + + final Object maxPoolSizeTotalProperty = properties.get(NettyClientProperties.MAX_CONNECTIONS_TOTAL); + final Object maxPoolIdleProperty = properties.get(NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT); + final Object maxPoolSizeProperty = properties.get(NettyClientProperties.MAX_CONNECTIONS); + + if (maxPoolSizeTotalProperty != null) { + clientConfiguration.maxPoolSizeTotal.set((Integer) maxPoolSizeTotalProperty); + } + + if (maxPoolIdleProperty != null) { + clientConfiguration.maxPoolIdle.set((Integer) maxPoolIdleProperty); + } + + if (maxPoolSizeProperty != null) { + clientConfiguration.maxPoolSize.set((Integer) maxPoolSizeProperty); + } + + if (clientConfiguration.maxPoolSizeTotal.get() < 0) { + throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_TOTAL(maxPoolSizeTotal.get())); + } + + if (clientConfiguration.maxPoolSize.get() < 0) { + throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_SIZE(maxPoolSize.get())); + } + + return clientConfiguration; + } + + /** + * Create an instance of {@link HttpClientCodec} based on preset settings {@link #initialHttpHeaderLineLength(int)}, + * {@link #maxHeaderSize} and {@link #maxChunkSize}. The settings can be preset in the configuration or + * on the HTTP client request. + * + * @param properties The HTTP client request properties. + * @return the {@link HttpClientCodec} instance. + */ + /* package */ HttpClientCodec createHttpClientCodec(Map properties) { + firstHttpHeaderLineLength.set(ClientProperties.getValue(properties, + NettyClientProperties.MAX_INITIAL_LINE_LENGTH, firstHttpHeaderLineLength.get())); + maxHeaderSize.set(ClientProperties.getValue(properties, NettyClientProperties.MAX_HEADER_SIZE, maxHeaderSize.get())); + maxChunkSize.set(ClientProperties.getValue(properties, NettyClientProperties.MAX_CHUNK_SIZE, maxChunkSize.get())); + + return new HttpClientCodec(firstHttpHeaderLineLength.get(), maxHeaderSize.get(), maxChunkSize.get()); + } + + /** + * Create an instance of {@link ProxyHandler} based on HTTP request URI's port and address, + * the preset proxy {@link #proxyUri(URI) uri}, {@link #proxyUserName(String) username}, + * and {@link #proxyPassword(String) password}. + * + * Can filter headers {@link #filterHeadersForProxy(boolean)}. + * + * @param clientProxy the Jersey {@link ClientProxy} instance. + * @param jerseyRequest the HTTP client request containing HTTP headers to be filtered. + * @return a Netty {@link ProxyHandler} instance. + */ + /* package */ ProxyHandler createProxyHandler(ClientProxy clientProxy, ClientRequest jerseyRequest) { + final URI u = clientProxy.uri(); + InetSocketAddress proxyAddr = new InetSocketAddress(u.getHost(), u.getPort() == -1 ? 8080 : u.getPort()); + + final Boolean filter = jerseyRequest + .resolveProperty(NettyClientProperties.FILTER_HEADERS_FOR_PROXY, filterHeadersForProxy.get()); + HttpHeaders httpHeaders = NettyConnector.setHeaders( + jerseyRequest, new DefaultHttpHeaders(), Boolean.TRUE.equals(filter)); + + ProxyHandler proxy = clientProxy.userName() == null ? new HttpProxyHandler(proxyAddr, httpHeaders) + : new HttpProxyHandler(proxyAddr, clientProxy.userName(), clientProxy.password(), httpHeaders); + if (connectTimeout.get() > 0) { + proxy.setConnectTimeoutMillis(connectTimeout.get()); + } + + return proxy; + } + + @Override + public abstract N self(); + } + +} diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorProvider.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorProvider.java index 01895cf8fa..e26f45a6c5 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorProvider.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2019 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 @@ -51,8 +51,59 @@ @Beta public class NettyConnectorProvider implements ConnectorProvider { + private final Config config; + + public NettyConnectorProvider() { + this.config = config(); + } + + private NettyConnectorProvider(Config config) { + this.config = config; + } + @Override public Connector getConnector(Client client, Configuration runtimeConfig) { - return new NettyConnector(client); + return new NettyConnector(client, config.rw()); + } + + /** + * Instantiate a builder allowing to configure the NettyConnectorProvider. + * @return a new {@link Config} instance. + */ + public static Config config() { + return new Config(); + } + + public static final class Config extends NettyConnectorConfiguration { + + private Config() { + } + + @Override + protected Config self() { + return this; + } + + /* package */ RW rw() { + RW rw = new RW(); + rw.setNonEmpty(this); + return rw; + } + + public NettyConnectorProvider build() { + return new NettyConnectorProvider(this); + } + + static class RW extends ReadWrite { + @Override + public RW instance() { + return new RW(); + } + + @Override + public RW self() { + return this; + } + } } } 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 index 1958cfdf2e..97731c4bf2 100644 --- 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 @@ -34,9 +34,15 @@ */ public class NettyHttpRedirectController { + private NettyConnectorProvider.Config.RW configuration; + + /* package */ void init(NettyConnectorProvider.Config.RW configuration) { + this.configuration = configuration; + } + /** * 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. + * By default, the HTTP POST request is not 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. @@ -44,7 +50,7 @@ public class NettyHttpRedirectController { * @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); + final boolean keepMethod = configuration.preserveMethodOnRedirect(request); if (Boolean.FALSE.equals(keepMethod) && request.getMethod().equals(HttpMethod.POST)) { switch (response.getStatus()) { diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/ConnectorConfiguration.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/ConnectorConfiguration.java new file mode 100644 index 0000000000..4663ae4936 --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/ConnectorConfiguration.java @@ -0,0 +1,604 @@ +/* + * 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.internal; + +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.client.innate.ClientProxy; + +import javax.net.ssl.SSLContext; +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Configuration; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.net.Proxy; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; + +// TODO move to client + +/** + * Configuration object to use for configuring the client connectors and HTTP request processing. + * This configuration provides settings to be handled by the connectors, mainly declared by {@link ClientProperties}. + * + * @param the connector configuration subtype. + */ +public class ConnectorConfiguration> { + protected NullableRef connectTimeout = NullableRef.of(0); + protected NullableRef expect100Continue = NullableRef.empty(); + protected NullableRef expect100continueThreshold = NullableRef.of( + ClientProperties.DEFAULT_EXPECT_100_CONTINUE_THRESHOLD_SIZE); + protected NullableRef followRedirects = NullableRef.of(Boolean.TRUE); + protected NullableRef proxyUri = NullableRef.empty(); + protected NullableRef proxyUserName = NullableRef.empty(); + protected NullableRef proxyPassword = NullableRef.empty(); + protected NullableRef readTimeout = NullableRef.of(0); + protected NullableRef requestEntityProcessing = NullableRef.empty(); + protected NullableRef> sslContextSupplier = NullableRef.empty(); + protected NullableRef threadPoolSize = NullableRef.empty(); + + /** + * Use factory methods provided by each connector supporting this configuration object and its subclass instead. + */ + protected ConnectorConfiguration() { + + } + + /** + * Set and replace the values of current configuration by values of other configuration + * if and only if the values of other configuration are set. + * + * @param other another configuration instance. + */ + protected > void setNonEmpty(X other) { + this.connectTimeout.setNonEmpty(other.connectTimeout); + this.expect100Continue.setNonEmpty(other.expect100Continue); + this.expect100continueThreshold.setNonEmpty(other.expect100continueThreshold); + this.followRedirects.setNonEmpty(other.followRedirects); + this.proxyUri.setNonEmpty(other.proxyUri); + this.proxyUserName.setNonEmpty(other.proxyUserName); + this.proxyPassword.setNonEmpty(other.proxyPassword); + this.readTimeout.setNonEmpty(other.readTimeout); + this.requestEntityProcessing.setNonEmpty(other.requestEntityProcessing); + this.sslContextSupplier.setNonEmpty(other.sslContextSupplier); + this.threadPoolSize.setNonEmpty(other.threadPoolSize); + } + + /** + * Set the asynchronous thread-pool size. The property {@link ClientProperties#ASYNC_THREADPOOL_SIZE} + * has precedence over this setting. + * + * @param threadPoolSize the size of the asynchronous thread-pool. + * @return updated configuration. + */ + public E asyncThreadPoolSize(int threadPoolSize) { + this.threadPoolSize.set(threadPoolSize); + return self(); + } + + /** + * Set connect timeout. The property {@link ClientProperties#CONNECT_TIMEOUT} + * has precedence over this setting. + * + * @param millis timeout in milliseconds. + * @return updated configuration. + */ + public E connectTimeout(int millis) { + connectTimeout.set(millis); + return self(); + } + + /** + * Allows for HTTP Expect:100-Continue. + * The property {@link ClientProperties#EXPECT_100_CONTINUE} has precedence over this setting. + * + * @param enable allows for HTTP Expect:100-Continue or not. + * @return updated configuration. + */ + public E expect100Continue(boolean enable) { + expect100Continue.set(enable); + return self(); + } + + /** + * Set the Expect:100-Continue content-length threshold size. + * The {@link ClientProperties#EXPECT_100_CONTINUE_THRESHOLD_SIZE} property has precedence over this setting. + * + * @param size the content-length threshold. + * @return updated configuration. + */ + public E expect100ContinueThreshold(long size) { + expect100ContinueThreshold(size); + return self(); + } + + /** + * Set to follow redirects. The property {@link ClientProperties#FOLLOW_REDIRECTS} has precedence over this setting. + * + * @param follow to follow or not to follow. + * @return updated configuration. + */ + public E followRedirects(boolean follow) { + followRedirects.set(follow); + return self(); + } + + /** + * Set proxy password. The property {@link ClientProperties#PROXY_PASSWORD} + * has precedence over this setting. + * + * @param proxyPassword the proxy password. + * @return updated configuration. + */ + public E proxyPassword(String proxyPassword) { + this.proxyPassword.set(proxyPassword); + return self(); + } + + /** + * Set proxy username. The property {@link ClientProperties#PROXY_USERNAME} + * has precedence over this setting. + * + * @param userName the proxy username. + * @return updated configuration. + */ + public E proxyUserName(String userName) { + proxyUserName.set(userName); + return self(); + } + + /** + * Set proxy URI. The property {@link ClientProperties#PROXY_URI} + * has precedence over this setting. + * + * @param proxyUri the proxy URI. + * @return updated configuration. + */ + public E proxyUri(String proxyUri) { + this.proxyUri.set(proxyUri); + return self(); + } + + /** + * Set proxy URI. The property {@link ClientProperties#PROXY_URI} + * has precedence over this setting. + * + * @param proxyUri the proxy URI. + * @return updated configuration. + */ + public E proxyUri(URI proxyUri) { + this.proxyUri.set(proxyUri); + return self(); + } + + /** + * Set HTTP proxy. The property {@link ClientProperties#PROXY_URI} + * has precedence over this setting. + * + * @param proxy the HTTP proxy. + * @return updated configuration. + */ + public E proxy(Proxy proxy) { + this.proxyUri.set(proxy); + return self(); + } + + /** + * Set read timeout. The property {@link ClientProperties#READ_TIMEOUT} + * has precedence over this setting. + * + * @param millis timeout in milliseconds. + * @return updated configuration. + */ + public E readTimeout(int millis) { + readTimeout.set(millis); + return self(); + } + + /** + * Set the request entity processing type. + * + * @param requestEntityProcessing the request entity processing type. + * @return the updated configuration. + */ + public E requestEntityProcessing(RequestEntityProcessing requestEntityProcessing) { + this.requestEntityProcessing.set(requestEntityProcessing); + return self(); + } + + /** + * Set the {@link SSLContext} supplier. The property {@link ClientProperties#SSL_CONTEXT_SUPPLIER} has precedence over + * this setting. + * + * @param sslContextSupplier the {@link SSLContext} supplier. + * @return the updated configuration. + */ + public E sslContextSupplier(Supplier sslContextSupplier) { + this.sslContextSupplier.set(sslContextSupplier); + return self(); + } + + /** + * Return type-cast self. + * @return self. + */ + @SuppressWarnings("unchecked") + protected E self() { + return (E) this; + } + + /** + *

+ * A reference to a value. The reference can be empty, but unlike the {@code Optional}, once a value is set, + * it never can be empty again. The {@code null} value is treated as a non-empty value of null. + *

+ * This {@code null} + * can be used to override some previous configuration value, to distinguish the intentional {@code null} override + * from an empty (non-set) configuration value. + *

+ * @param type of the value. + */ + protected static class NullableRef implements org.glassfish.jersey.internal.util.collection.Ref { + + private NullableRef() { + // use factory methods; + } + + /** + * Return a new empty reference. + * + * @return an empty reference. + * @param The type of the empty value. + */ + public static NullableRef empty() { + return new NullableRef<>(); + } + + /** + * Return a reference of a given value. + * + * @param value the value this reference refers to.* + * @return a new reference to a given value. + * @param type of the value. + */ + public static NullableRef of(T value) { + NullableRef ref = new NullableRef<>(); + ref.set(value); + return ref; + } + + private boolean empty = true; + private T ref = null; + + @Override + public void set(T value) { + empty = false; + ref = value; + } + + /** + * Set or replace the value if other value is set. + * @param other a reference to another value. + */ + public void setNonEmpty(NullableRef other) { + other.ifPresent(this::set); + } + + @Override + public T get() { + return ref; + } + + /** + * Run action if and only if the condition applies. + * + * @param predicate the condition to be met. + * @param action the action to run if condition is met. + */ + public void iff(Predicate predicate, Runnable action) { + if (predicate.test(ref)) { + action.run(); + } + } + + /** + * If it is empty, sets the {@code value} value. Keeps the original value, otherwise. + * + * @param value the value to be set if empty. + */ + public void ifEmptySet(T value) { + if (empty) { + set(value); + } + } + + /** + * If a value is present, performs the given action with the value, + * otherwise does nothing. + * + * @param action the action to be performed, if a value is present + * @throws NullPointerException if value is present and the given action is + * {@code null} + */ + public void ifPresent(Consumer action) { + if (!empty) { + action.accept(ref); + } + } + + /** + * If a value is present, performs the given action with the value, + * otherwise performs the given empty-based action. + * + * @param action the action to be performed, if a value is present + * @param emptyAction the empty-based action to be performed, if no value is + * present + * @throws NullPointerException if a value is present and the given action + * is {@code null}, or no value is present and the given empty-based + * action is {@code null}. + */ + public void ifPresentOrElse(Consumer action, Runnable emptyAction) { + if (!empty) { + action.accept(ref); + } else { + emptyAction.run(); + } + } + + /** + * If a value is not present, returns {@code true}, otherwise + * {@code false}. + * + * @return {@code true} if a value is not present, otherwise {@code false} + */ + public boolean isEmpty() { + return empty; + } + + /** + * If a value is present, returns {@code true}, otherwise {@code false}. + * + * @return {@code true} if a value is present, otherwise {@code false} + */ + public boolean isPresent() { + return !empty; + } + + + @Override + public int hashCode() { + return Objects.hash(ref, empty); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof NullableRef)) { + return false; + } + + NullableRef that = (NullableRef) o; + return Objects.equals(empty, that.empty) && Objects.equals(ref, that.ref); + } + + @Override + public String toString() { + return empty ? "" : ref == null ? "" : ref.toString(); + } + } + + protected interface ReadWrite> { + /** + * Return the thread-pool size setting. + * + * @return the thread pool size setting. + */ + public default Integer asyncThreadPoolSize() { + return self().threadPoolSize.get(); + } + + /** + * Update connect timeout value based on request properties settings. + * + * @param request the current HTTP client request. + * @return the updated configuration. + */ + public default CC connectTimeout(ClientRequest request) { + self().connectTimeout.set(request.resolveProperty(ClientProperties.CONNECT_TIMEOUT, self().connectTimeout.get())); + return self(); + } + + /** + * Get the value of connect timeout setting. + * + * @return connect timeout value. + */ + public default int connectTimeout() { + return self().connectTimeout.get(); + } + + /** + * Utility method to create a new instance of configuration to preserve the settings of previous configuration. + * + * @return a new instance of the configuration. + */ + public default CC copy() { + CC config = instance(); + config.setNonEmpty(self()); + return config; + } + + /** + * Update the {@link #expect100Continue(boolean)} from the HTTP client request. + * + * @param request the HTTP client request. + * @return the Expect: 100-Continue support value. + */ + public default Boolean expect100Continue(ClientRequest request) { + final Boolean expectContinueActivated = request.resolveProperty(ClientProperties.EXPECT_100_CONTINUE, Boolean.class); + if (expectContinueActivated != null) { + self().expect100Continue.set(expectContinueActivated); + } + return self().expect100Continue.get(); + } + + /** + * Update the {@link #expect100ContinueThreshold(long)} from the HTTP client request. + * + * @param request the HTTP client request. + * @return the content length threshold size. + */ + public default long expect100ContinueThreshold(ClientRequest request) { + self().expect100continueThreshold.set(request.resolveProperty( + ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, + self().expect100continueThreshold.get())); + return self().expect100continueThreshold.get(); + } + + /** + * Update the {@link #followRedirects(boolean)} setting from the HTTP client request. The default is {@code true}. + * + * @param request the HTTP client request. + * @return follow redirects setting. + */ + public default boolean followRedirects(ClientRequest request) { + self().followRedirects.set(request.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, self().followRedirects.get())); + return self().followRedirects.get(); + } + + /** + * Get the value of the follow redirects setting. + * + * @return whether to follow redirects or not. + */ + public default boolean followRedirects() { + return self().followRedirects.get(); + } + + /** + * Create optional client proxy information based on the proxy information set in the configuration + * or the HTTP client request. The used settings are {@link #proxy(Proxy)}, + * {@link #proxyUri(URI)}, {@link #proxyUri(String)}, {@link #proxyUserName(String)}, + * and {@link #proxyPassword(String)}. + * + * @param request the HTTP client request, + * @param requestUri the HTTP request URI. It can differ from the URI used in the request, based on other + * information set by the HTTP client request. + * @return the optional client proxy. + */ + public default Optional proxy(ClientRequest request, URI requestUri) { + Optional proxy = ClientProxy.proxyFromRequest(request); + if (!proxy.isPresent() && self().proxyUri.isPresent()) { + // TODO support in ClientProxy + Map properties = new HashMap<>(); + properties.put(ClientProperties.PROXY_URI, self().proxyUri.get()); + properties.put(ClientProperties.PROXY_USERNAME, self().proxyUserName.get()); + properties.put(ClientProperties.PROXY_PASSWORD, self().proxyPassword.get()); + Configuration configuration = (Configuration) java.lang.reflect.Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[]{Configuration.class}, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + switch (method.getName()) { + case "getProperties": + return properties; + } + return null; + } + }); + proxy = ClientProxy.proxyFromConfiguration(configuration); + } + if (!proxy.isPresent()) { + proxy = ClientProxy.proxyFromProperties(requestUri); + } + return proxy; + } + + /** + * Update {@link #readTimeout(int) read timeout} based on the HTTP request properties. + * + * @param request the current HTTP client request. + * @return updated configuration. + */ + public default CC readTimeout(ClientRequest request) { + self().readTimeout.set(request.resolveProperty(ClientProperties.READ_TIMEOUT, self().readTimeout.get())); + return self(); + } + + /** + * Get the value of preset {@link #readTimeout(int)}. + * + * @return the read timeout milliseconds. + */ + public default int readTimeout() { + return self().readTimeout.get(); + } + + /** + * Get the {@link RequestEntityProcessing} updated by the HTTP client request. + * + * @param request the HTTP client request. + * @return the RequestEntityProcessing type. + */ + public default RequestEntityProcessing requestEntityProcessing(ClientRequest request) { + RequestEntityProcessing entityProcessing = request.resolveProperty( + ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.class); + if (entityProcessing == null) { + entityProcessing = self().requestEntityProcessing.get(); + } + return entityProcessing; + } + + /** + * Get {@link SSLContext} either from the {@link ClientProperties#SSL_CONTEXT_SUPPLIER}, or from this configuration, + * or from the {@link Client#getSslContext()} in this order. + * + * @param client the client used to get the {@link SSLContext}. + * @param request the request used to get the {@link SSLContext}. + * @return the {@link SSLContext}. + */ + public default SSLContext sslContext(Client client, ClientRequest request) { + Supplier supplier = request.resolveProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, Supplier.class); + if (supplier == null) { + supplier = self().sslContextSupplier.get(); + } + return supplier == null ? client.getSslContext() : supplier.get(); + } + + /** + * Return a new instance of configuration. + * @return a new instance of configuration. + */ + public CC instance(); + + /** + * Return typed-cast self. + * @return self. + */ + public CC self(); + } +} diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/NettyEntityWriter.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/NettyEntityWriter.java index bcd3fd868c..de5799960d 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/NettyEntityWriter.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/NettyEntityWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 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,9 +16,9 @@ package org.glassfish.jersey.netty.connector.internal; +import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.handler.stream.ChunkedInput; -import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.RequestEntityProcessing; @@ -60,7 +60,7 @@ enum Type { /** * Flushes the writen objects. Can throw IOException. - * @throws IOException + * @throws IOException exception. */ void flush() throws IOException; @@ -68,7 +68,7 @@ enum Type { * Get the netty Chunked Input to be written. * @return The Chunked input instance */ - ChunkedInput getChunkedInput(); + ChunkedInput getChunkedInput(); /** * Get the {@link OutputStream} used to write an entity @@ -78,20 +78,20 @@ enum Type { /** * Get the length of the entity written to the {@link OutputStream} - * @return + * @return length of the entity. */ long getLength(); /** - * Return Type of - * @return + * Return Type of the {@link NettyEntityWriter}. + * @return type of the writer. */ Type getType(); - static NettyEntityWriter getInstance(ClientRequest clientRequest, Channel channel) { + static NettyEntityWriter getInstance( + ClientRequest clientRequest, Channel channel, ConnectorConfiguration.ReadWrite config) { final long lengthLong = clientRequest.getLengthLong(); - final RequestEntityProcessing entityProcessing = clientRequest.resolveProperty( - ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.class); + final RequestEntityProcessing entityProcessing = config.requestEntityProcessing(clientRequest); if ((entityProcessing == null && lengthLong == -1) || entityProcessing == RequestEntityProcessing.CHUNKED) { return new DirectEntityWriter(channel, Type.CHUNKED); @@ -129,7 +129,7 @@ public void flush() { } @Override - public ChunkedInput getChunkedInput() { + public ChunkedInput getChunkedInput() { return stream; } @@ -203,7 +203,7 @@ private void _flush() throws IOException { } @Override - public ChunkedInput getChunkedInput() { + public ChunkedInput getChunkedInput() { return writer.getChunkedInput(); } @@ -226,7 +226,7 @@ public Type getType() { private class DelayedOutputStream extends OutputStream { private final List actions = new ArrayList<>(); private int writeLen = 0; - private AtomicBoolean streamFlushed = new AtomicBoolean(false); + private final AtomicBoolean streamFlushed = new AtomicBoolean(false); @Override public void write(int b) throws IOException { 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 index f1624eb0e1..1d6af3abf9 100644 --- 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 @@ -85,8 +85,9 @@ public boolean testChunkedInputNotStucked() throws InterruptedException { public Connector getConnector(Client client, Configuration runtimeConfig) { return new NettyConnector(client) { @Override - NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel) { - writer.set(super.nettyEntityWriter(clientRequest, channel)); + NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel, + NettyConnectorProvider.Config.RW config) { + writer.set(super.nettyEntityWriter(clientRequest, channel, config)); writerSetLatch.countDown(); return new NettyEntityWriter() { private boolean slept = false; diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputWriteErrorSimulationTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputWriteErrorSimulationTest.java index bf335846c4..28da9aeb76 100644 --- a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputWriteErrorSimulationTest.java +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputWriteErrorSimulationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 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 @@ -224,8 +224,11 @@ private static ConnectorProvider getJerseyChunkedInputModifiedNettyConnector(Deq @Override public Connector getConnector(Client client, Configuration runtimeConfig) { return new NettyConnector(client) { - NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel) { - NettyEntityWriter wrapped = NettyEntityWriter.getInstance(clientRequest, channel); + @Override + NettyEntityWriter nettyEntityWriter( + ClientRequest clientRequest, Channel channel, NettyConnectorProvider.Config.RW config) { + NettyEntityWriter wrapped = NettyEntityWriter.getInstance( + clientRequest, channel, config); JerseyChunkedInput chunkedInput = (JerseyChunkedInput) wrapped.getChunkedInput(); try { diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ConnectorConfigTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ConnectorConfigTest.java new file mode 100644 index 0000000000..4df6dc2b08 --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ConnectorConfigTest.java @@ -0,0 +1,42 @@ +/* + * 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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; + +public class ConnectorConfigTest { + @Test + public void testPrecedence() { + + NettyConnectorProvider.Config.RW builderLower = NettyConnectorProvider.config().rw(); + builderLower.maxTotalConnection(55); + + NettyConnectorProvider.Config.RW builderUpper = builderLower.copy(); + builderUpper.maxTotalConnection(56); + Assertions.assertEquals(56, builderUpper.maxPoolSizeTotal.get()); + + Client client = ClientBuilder.newClient(); + client.property(NettyClientProperties.MAX_CONNECTIONS_TOTAL, 57); + NettyConnectorProvider.Config.RW result = builderUpper.fromClient(client); + Assertions.assertEquals(57, result.maxPoolSizeTotal.get()); + Assertions.assertEquals(60, result.maxPoolIdle.get()); + } +} diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/CustomConnectionControllerTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/CustomConnectionControllerTest.java new file mode 100644 index 0000000000..dd03a69cd3 --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/CustomConnectionControllerTest.java @@ -0,0 +1,71 @@ +/* + * 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.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.concurrent.atomic.AtomicBoolean; + +public class CustomConnectionControllerTest extends JerseyTest { + final AtomicBoolean hit = new AtomicBoolean(false); + + @Path("/") + public static class CustomConnectionControllerTestResource { + @GET + public String get() { + return "ok"; + } + } + + @Override + protected Application configure() { + return new ResourceConfig(CustomConnectionControllerTestResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + NettyConnectorProvider provider = NettyConnectorProvider.config().connectorController(new NettyConnectionController() { + @Override + public String getConnectionGroup(ClientRequest clientRequest, URI uri, String hostName, int port) { + hit.set(true); + return super.getConnectionGroup(clientRequest, uri, hostName, port); + } + }).build(); + + config.connectorProvider(provider); + } + + @Test + public void testCustomConnectionControllerIsInvoked() { + try (Response response = target().request().get()) { + Assertions.assertEquals(200, response.getStatus()); + } + client().close(); + Assertions.assertEquals(true, hit.get()); + + } +}