Skip to content

Commit b7e4bf2

Browse files
committed
Implementation of Netty HTTP redirect logic being custom replaceble.
Signed-off-by: jansupol <[email protected]>
1 parent 382f69e commit b7e4bf2

File tree

6 files changed

+244
-73
lines changed

6 files changed

+244
-73
lines changed

connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java

Lines changed: 24 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2016, 2024 Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2016, 2025 Oracle and/or its affiliates. All rights reserved.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -18,8 +18,6 @@
1818

1919
import java.io.IOException;
2020
import java.net.URI;
21-
import java.util.Iterator;
22-
import java.util.List;
2321
import java.util.Locale;
2422
import java.util.Map;
2523
import java.util.Set;
@@ -28,8 +26,6 @@
2826
import java.util.concurrent.TimeoutException;
2927
import java.util.function.Predicate;
3028

31-
import javax.ws.rs.HttpMethod;
32-
import javax.ws.rs.core.MultivaluedMap;
3329
import javax.ws.rs.core.Response;
3430

3531
import org.glassfish.jersey.client.ClientProperties;
@@ -67,6 +63,7 @@ class JerseyClientHandler extends SimpleChannelInboundHandler<HttpObject> {
6763
private final boolean followRedirects;
6864
private final int maxRedirects;
6965
private final NettyConnector connector;
66+
private final NettyHttpRedirectController redirectController;
7067

7168
private NettyInputStream nis;
7269
private ClientResponse jerseyResponse;
@@ -83,6 +80,10 @@ class JerseyClientHandler extends SimpleChannelInboundHandler<HttpObject> {
8380
this.followRedirects = jerseyRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, true);
8481
this.maxRedirects = jerseyRequest.resolveProperty(NettyClientProperties.MAX_REDIRECTS, DEFAULT_MAX_REDIRECTS);
8582
this.connector = connector;
83+
84+
final NettyHttpRedirectController customRedirectController = jerseyRequest
85+
.resolveProperty(NettyClientProperties.HTTP_REDIRECT_CONTROLLER, NettyHttpRedirectController.class);
86+
this.redirectController = customRedirectController == null ? new NettyHttpRedirectController() : customRedirectController;
8687
}
8788

8889
@Override
@@ -142,22 +143,24 @@ protected void notifyResponse() {
142143
} else {
143144
ClientRequest newReq = new ClientRequest(jerseyRequest);
144145
newReq.setUri(newUri);
145-
restrictRedirectRequest(newReq, cr);
146-
147-
final NettyConnector newConnector = new NettyConnector(newReq.getClient());
148-
newConnector.execute(newReq, redirectUriHistory, new CompletableFuture<ClientResponse>() {
149-
@Override
150-
public boolean complete(ClientResponse value) {
151-
newConnector.close();
152-
return responseAvailable.complete(value);
153-
}
154-
155-
@Override
156-
public boolean completeExceptionally(Throwable ex) {
157-
newConnector.close();
158-
return responseAvailable.completeExceptionally(ex);
159-
}
160-
});
146+
if (redirectController.prepareRedirect(newReq, cr)) {
147+
final NettyConnector newConnector = new NettyConnector(newReq.getClient());
148+
newConnector.execute(newReq, redirectUriHistory, new CompletableFuture<ClientResponse>() {
149+
@Override
150+
public boolean complete(ClientResponse value) {
151+
newConnector.close();
152+
return responseAvailable.complete(value);
153+
}
154+
155+
@Override
156+
public boolean completeExceptionally(Throwable ex) {
157+
newConnector.close();
158+
return responseAvailable.completeExceptionally(ex);
159+
}
160+
});
161+
} else {
162+
responseAvailable.complete(cr);
163+
}
161164
}
162165
} catch (IllegalArgumentException e) {
163166
responseAvailable.completeExceptionally(
@@ -226,8 +229,6 @@ public String getReasonPhrase() {
226229
}
227230
}
228231

229-
230-
231232
@Override
232233
public void exceptionCaught(ChannelHandlerContext ctx, final Throwable cause) {
233234
responseDone.completeExceptionally(cause);
@@ -243,53 +244,6 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc
243244
}
244245
}
245246

246-
/*
247-
* RFC 9110 Section 15.4
248-
* https://httpwg.org/specs/rfc9110.html#rfc.section.15.4
249-
*/
250-
private void restrictRedirectRequest(ClientRequest newRequest, ClientResponse response) {
251-
final MultivaluedMap<String, Object> headers = newRequest.getHeaders();
252-
final Boolean keepMethod = newRequest.resolveProperty(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, Boolean.TRUE);
253-
254-
if (Boolean.FALSE.equals(keepMethod) && newRequest.getMethod().equals(HttpMethod.POST)) {
255-
switch (response.getStatus()) {
256-
case 301 /* MOVED PERMANENTLY */:
257-
case 302 /* FOUND */:
258-
removeContentHeaders(headers);
259-
newRequest.setMethod(HttpMethod.GET);
260-
newRequest.setEntity(null);
261-
break;
262-
}
263-
}
264-
265-
for (final Iterator<Map.Entry<String, List<Object>>> it = headers.entrySet().iterator(); it.hasNext(); ) {
266-
final Map.Entry<String, List<Object>> entry = it.next();
267-
if (ProxyHeaders.INSTANCE.test(entry.getKey())) {
268-
it.remove();
269-
}
270-
}
271-
272-
headers.remove(HttpHeaders.IF_MATCH);
273-
headers.remove(HttpHeaders.IF_NONE_MATCH);
274-
headers.remove(HttpHeaders.IF_MODIFIED_SINCE);
275-
headers.remove(HttpHeaders.IF_UNMODIFIED_SINCE);
276-
headers.remove(HttpHeaders.AUTHORIZATION);
277-
headers.remove(HttpHeaders.REFERER);
278-
headers.remove(HttpHeaders.COOKIE);
279-
}
280-
281-
private void removeContentHeaders(MultivaluedMap<String, Object> headers) {
282-
for (final Iterator<Map.Entry<String, List<Object>>> it = headers.entrySet().iterator(); it.hasNext(); ) {
283-
final Map.Entry<String, List<Object>> entry = it.next();
284-
final String lowName = entry.getKey().toLowerCase(Locale.ROOT);
285-
if (lowName.startsWith("content-")) {
286-
it.remove();
287-
}
288-
}
289-
headers.remove(HttpHeaders.LAST_MODIFIED);
290-
headers.remove(HttpHeaders.TRANSFER_ENCODING);
291-
}
292-
293247
/* package */ static class ProxyHeaders implements Predicate<String> {
294248
static final ProxyHeaders INSTANCE = new ProxyHeaders();
295249
private static final String HOST = HttpHeaders.HOST.toLowerCase(Locale.ROOT);

connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020, 2024 Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2020, 2025 Oracle and/or its affiliates. All rights reserved.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -55,6 +55,15 @@ public class NettyClientProperties {
5555
*/
5656
public static final String FILTER_HEADERS_FOR_PROXY = "jersey.config.client.filter.headers.proxy";
5757

58+
/**
59+
* <p>
60+
* The implementation of custom {@link NettyHttpRedirectController} redirect logic.
61+
* </p>
62+
*
63+
* @since 2.47
64+
*/
65+
public static final String HTTP_REDIRECT_CONTROLLER = "jersey.config.client.netty.http.redirect.controller";
66+
5867
/**
5968
* <p>
6069
* This property determines the number of seconds the idle connections are kept in the pool before pruned.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0, which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the
10+
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11+
* version 2 with the GNU Classpath Exception, which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
*/
16+
17+
package org.glassfish.jersey.netty.connector;
18+
19+
import org.glassfish.jersey.client.ClientRequest;
20+
import org.glassfish.jersey.client.ClientResponse;
21+
import org.glassfish.jersey.http.HttpHeaders;
22+
23+
import javax.ws.rs.HttpMethod;
24+
import javax.ws.rs.core.MultivaluedMap;
25+
import java.util.Iterator;
26+
import java.util.List;
27+
import java.util.Locale;
28+
import java.util.Map;
29+
30+
/**
31+
* The HTTP Redirect logic implementation for Netty Connector.
32+
*
33+
* @since 2.47
34+
*/
35+
public class NettyHttpRedirectController {
36+
37+
/**
38+
* Configure the HTTP request after HTTP Redirect response has been received.
39+
* By default, the HTTP POST request is transformed into HTTP GET for status 301 & 302.
40+
* Also, HTTP Headers described by RFC 9110 Section 15.4 are removed from the new HTTP Request.
41+
*
42+
* @param request The new {@link ClientRequest} to be sent to the redirected URI.
43+
* @param response The original HTTP redirect {@link ClientResponse} received.
44+
* @return {@code true} when the new request should be sent.
45+
*/
46+
public boolean prepareRedirect(ClientRequest request, ClientResponse response) {
47+
final Boolean keepMethod = request.resolveProperty(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, Boolean.TRUE);
48+
49+
if (Boolean.FALSE.equals(keepMethod) && request.getMethod().equals(HttpMethod.POST)) {
50+
switch (response.getStatus()) {
51+
case 301 /* MOVED PERMANENTLY */:
52+
case 302 /* FOUND */:
53+
removeContentHeaders(request.getHeaders());
54+
request.setMethod(HttpMethod.GET);
55+
request.setEntity(null);
56+
break;
57+
}
58+
}
59+
60+
restrictRequestHeaders(request, response);
61+
return true;
62+
}
63+
64+
/**
65+
* RFC 9110 Section 15.4 defines the HTTP headers that should be removed from the redirected request.
66+
* https://httpwg.org/specs/rfc9110.html#rfc.section.15.4.
67+
*
68+
* @param request the new request to a new URI location.
69+
* @param response the HTTP redirect response.
70+
*/
71+
protected void restrictRequestHeaders(ClientRequest request, ClientResponse response) {
72+
final MultivaluedMap<String, Object> headers = request.getHeaders();
73+
74+
for (final Iterator<Map.Entry<String, List<Object>>> it = headers.entrySet().iterator(); it.hasNext(); ) {
75+
final Map.Entry<String, List<Object>> entry = it.next();
76+
if (JerseyClientHandler.ProxyHeaders.INSTANCE.test(entry.getKey())) {
77+
it.remove();
78+
}
79+
}
80+
81+
headers.remove(HttpHeaders.IF_MATCH);
82+
headers.remove(HttpHeaders.IF_NONE_MATCH);
83+
headers.remove(HttpHeaders.IF_MODIFIED_SINCE);
84+
headers.remove(HttpHeaders.IF_UNMODIFIED_SINCE);
85+
headers.remove(HttpHeaders.AUTHORIZATION);
86+
headers.remove(HttpHeaders.REFERER);
87+
headers.remove(HttpHeaders.COOKIE);
88+
}
89+
90+
private void removeContentHeaders(MultivaluedMap<String, Object> headers) {
91+
for (final Iterator<Map.Entry<String, List<Object>>> it = headers.entrySet().iterator(); it.hasNext(); ) {
92+
final Map.Entry<String, List<Object>> entry = it.next();
93+
final String lowName = entry.getKey().toLowerCase(Locale.ROOT);
94+
if (lowName.startsWith("content-")) {
95+
it.remove();
96+
}
97+
}
98+
headers.remove(HttpHeaders.LAST_MODIFIED);
99+
headers.remove(HttpHeaders.TRANSFER_ENCODING);
100+
}
101+
102+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0, which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the
10+
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11+
* version 2 with the GNU Classpath Exception, which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
*/
16+
17+
package org.glassfish.jersey.netty.connector;
18+
19+
import org.glassfish.jersey.client.ClientConfig;
20+
import org.glassfish.jersey.client.ClientRequest;
21+
import org.glassfish.jersey.client.ClientResponse;
22+
import org.glassfish.jersey.http.HttpHeaders;
23+
import org.glassfish.jersey.server.ResourceConfig;
24+
import org.glassfish.jersey.test.JerseyTest;
25+
import org.hamcrest.MatcherAssert;
26+
import org.hamcrest.Matchers;
27+
import org.junit.jupiter.api.Test;
28+
29+
import javax.ws.rs.GET;
30+
import javax.ws.rs.POST;
31+
import javax.ws.rs.Path;
32+
import javax.ws.rs.client.Entity;
33+
import javax.ws.rs.core.Application;
34+
import javax.ws.rs.core.Context;
35+
import javax.ws.rs.core.MediaType;
36+
import javax.ws.rs.core.Response;
37+
import javax.ws.rs.core.UriInfo;
38+
39+
public class CustomRedirectControllerTest extends JerseyTest {
40+
private static final String REDIRECTED = "redirected";
41+
42+
@Path("/")
43+
public static class CustomRedirectControllerTestResource {
44+
@Context
45+
UriInfo uriInfo;
46+
47+
@GET
48+
@Path(REDIRECTED)
49+
public String redirected() {
50+
return REDIRECTED;
51+
}
52+
53+
@POST
54+
@Path("doRedirect")
55+
public Response doRedirect(int status) {
56+
return Response.status(status)
57+
.header(HttpHeaders.LOCATION, uriInfo.getBaseUri().toString() + "redirected")
58+
.build();
59+
}
60+
}
61+
62+
@Override
63+
protected Application configure() {
64+
return new ResourceConfig(CustomRedirectControllerTestResource.class);
65+
}
66+
67+
@Override
68+
protected void configureClient(ClientConfig config) {
69+
config.connectorProvider(new NettyConnectorProvider());
70+
}
71+
72+
@Test
73+
public void testRedirectToGET() {
74+
try (Response r = target("doRedirect")
75+
.property(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, false)
76+
.request().post(Entity.entity(301, MediaType.TEXT_PLAIN_TYPE))) {
77+
MatcherAssert.assertThat(r.getStatus(), Matchers.is(200));
78+
MatcherAssert.assertThat(r.readEntity(String.class), Matchers.is(REDIRECTED));
79+
}
80+
}
81+
82+
@Test
83+
public void testNotRedirected() {
84+
try (Response response = target("doRedirect")
85+
.property(NettyClientProperties.HTTP_REDIRECT_CONTROLLER, new NettyHttpRedirectController() {
86+
@Override
87+
public boolean prepareRedirect(ClientRequest request, ClientResponse response) {
88+
return false;
89+
}
90+
}).request().post(Entity.entity(301, MediaType.TEXT_PLAIN_TYPE))) {
91+
MatcherAssert.assertThat(response.getStatus(), Matchers.is(301));
92+
}
93+
}
94+
}

docs/src/main/docbook/appendix-properties.xml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0"?>
22
<!--
33
4-
Copyright (c) 2013, 2024 Oracle and/or its affiliates. All rights reserved.
4+
Copyright (c) 2013, 2025 Oracle and/or its affiliates. All rights reserved.
55
66
This program and the accompanying materials are made available under the
77
terms of the Eclipse Public License v. 2.0, which is available at
@@ -2172,6 +2172,16 @@
21722172
</row>
21732173
</thead>
21742174
<tbody>
2175+
<row>
2176+
<entry>&jersey.netty.NettyClientProperties.HTTP_REDIRECT_CONTROLLER;</entry>
2177+
<entry><literal>jersey.config.client.netty.http.redirect.controller</literal></entry>
2178+
<entry>
2179+
<para>
2180+
The implementation of custom &jersey.netty.NettyHttpRedirectController; redirect logic.
2181+
<literal>Since 2.47</literal>
2182+
</para>
2183+
</entry>
2184+
</row>
21752185
<row>
21762186
<entry>&jersey.netty.NettyClientProperties.FILTER_HEADERS_FOR_PROXY;</entry>
21772187
<entry><literal>jersey.config.client.filter.headers.proxy</literal></entry>

0 commit comments

Comments
 (0)