Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,8 +18,6 @@

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;
Expand All @@ -28,8 +26,6 @@
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;

import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;

import org.glassfish.jersey.client.ClientProperties;
Expand Down Expand Up @@ -67,6 +63,7 @@ class JerseyClientHandler extends SimpleChannelInboundHandler<HttpObject> {
private final boolean followRedirects;
private final int maxRedirects;
private final NettyConnector connector;
private final NettyHttpRedirectController redirectController;

private NettyInputStream nis;
private ClientResponse jerseyResponse;
Expand All @@ -83,6 +80,10 @@ class JerseyClientHandler extends SimpleChannelInboundHandler<HttpObject> {
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
Expand Down Expand Up @@ -142,22 +143,24 @@ protected void notifyResponse() {
} else {
ClientRequest newReq = new ClientRequest(jerseyRequest);
newReq.setUri(newUri);
restrictRedirectRequest(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);
}

@Override
public boolean completeExceptionally(Throwable ex) {
newConnector.close();
return responseAvailable.completeExceptionally(ex);
}
});
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);
}

@Override
public boolean completeExceptionally(Throwable ex) {
newConnector.close();
return responseAvailable.completeExceptionally(ex);
}
});
} else {
responseAvailable.complete(cr);
}
}
} catch (IllegalArgumentException e) {
responseAvailable.completeExceptionally(
Expand Down Expand Up @@ -226,8 +229,6 @@ public String getReasonPhrase() {
}
}



@Override
public void exceptionCaught(ChannelHandlerContext ctx, final Throwable cause) {
responseDone.completeExceptionally(cause);
Expand All @@ -243,53 +244,6 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc
}
}

/*
* 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -55,6 +55,15 @@ public class NettyClientProperties {
*/
public static final String FILTER_HEADERS_FOR_PROXY = "jersey.config.client.filter.headers.proxy";

/**
* <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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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 javax.ws.rs.HttpMethod;
import javax.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);
}

}
Original file line number Diff line number Diff line change
@@ -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 javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.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));
}
}
}
12 changes: 11 additions & 1 deletion docs/src/main/docbook/appendix-properties.xml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -2172,6 +2172,16 @@
</row>
</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>
Expand Down
Loading