Skip to content

Commit ffcf463

Browse files
committed
HTTPCLIENT-2390: Add RedirectMethodPolicy to control 301/302 method rewriting. Opt-in preservation of POST (repeatable entity) while keeping the browser-compatible default; classic and async updated with tests and examples.
1 parent 72a00a0 commit ffcf463

8 files changed

Lines changed: 548 additions & 7 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http.config;
28+
29+
30+
/**
31+
* Policy controlling method/body rewriting on 301/302 redirects.
32+
*
33+
* @since 5.6
34+
*/
35+
public enum RedirectMethodPolicy {
36+
/**
37+
* Browser compatibility: POST→GET for 301/302; 303→GET; 307/308 preserve.
38+
*/
39+
BROWSER_COMPAT,
40+
41+
/**
42+
* Preserve original method (& body if repeatable) for 301/302.
43+
*/
44+
PRESERVE_METHOD,
45+
46+
/**
47+
* Preserve original method (& body if repeatable) for 301/302
48+
* only when the redirect stays on the same authority (scheme+host+port).
49+
*/
50+
PRESERVE_SAME_AUTH
51+
}

httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,15 @@ public class RequestConfig implements Cloneable {
6969

7070
private final ExpectContinueTrigger expectContinueTrigger;
7171

72+
private final RedirectMethodPolicy redirectMethodPolicy;
73+
7274
/**
7375
* Intended for CDI compatibility
7476
*/
7577
protected RequestConfig() {
7678
this(false, null, null, false, false, 0, false, null, null,
7779
DEFAULT_CONNECTION_REQUEST_TIMEOUT, null, null, DEFAULT_CONN_KEEP_ALIVE, false, false, false, null,
78-
ExpectContinueTrigger.ALWAYS);
80+
ExpectContinueTrigger.ALWAYS, null);
7981
}
8082

8183
RequestConfig(
@@ -96,7 +98,8 @@ protected RequestConfig() {
9698
final boolean hardCancellationEnabled,
9799
final boolean protocolUpgradeEnabled,
98100
final Path unixDomainSocket,
99-
final ExpectContinueTrigger expectContinueTrigger) {
101+
final ExpectContinueTrigger expectContinueTrigger,
102+
final RedirectMethodPolicy redirectMethodPolicy) {
100103
super();
101104
this.expectContinueEnabled = expectContinueEnabled;
102105
this.proxy = proxy;
@@ -116,6 +119,7 @@ protected RequestConfig() {
116119
this.protocolUpgradeEnabled = protocolUpgradeEnabled;
117120
this.unixDomainSocket = unixDomainSocket;
118121
this.expectContinueTrigger = expectContinueTrigger;
122+
this.redirectMethodPolicy = redirectMethodPolicy;
119123
}
120124

121125
/**
@@ -248,6 +252,13 @@ public ExpectContinueTrigger getExpectContinueTrigger() {
248252
return expectContinueTrigger;
249253
}
250254

255+
/**
256+
* @since 5.6
257+
*/
258+
public RedirectMethodPolicy getRedirectMethodPolicy() {
259+
return redirectMethodPolicy;
260+
}
261+
251262
@Override
252263
protected RequestConfig clone() throws CloneNotSupportedException {
253264
return (RequestConfig) super.clone();
@@ -274,6 +285,7 @@ public String toString() {
274285
builder.append(", hardCancellationEnabled=").append(hardCancellationEnabled);
275286
builder.append(", protocolUpgradeEnabled=").append(protocolUpgradeEnabled);
276287
builder.append(", unixDomainSocket=").append(unixDomainSocket);
288+
builder.append(", redirectMethodPolicy=").append(redirectMethodPolicy);
277289
builder.append("]");
278290
return builder.toString();
279291
}
@@ -323,6 +335,7 @@ public static class Builder {
323335
private boolean protocolUpgradeEnabled;
324336
private Path unixDomainSocket;
325337
private ExpectContinueTrigger expectContinueTrigger;
338+
private RedirectMethodPolicy redirectMethodPolicy;
326339

327340
Builder() {
328341
super();
@@ -334,6 +347,7 @@ public static class Builder {
334347
this.hardCancellationEnabled = true;
335348
this.protocolUpgradeEnabled = true;
336349
this.expectContinueTrigger = ExpectContinueTrigger.ALWAYS;
350+
this.redirectMethodPolicy = RedirectMethodPolicy.BROWSER_COMPAT;
337351
}
338352

339353
/**
@@ -693,6 +707,17 @@ public Builder setExpectContinueTrigger(final ExpectContinueTrigger trigger) {
693707
this.expectContinueTrigger = Args.notNull(trigger, "ExpectContinueTrigger");
694708
return this;
695709
}
710+
/**
711+
* Control method/body rewriting for 301/302.
712+
* Default is {@link RedirectMethodPolicy#BROWSER_COMPAT}.
713+
*
714+
* @since 5.6
715+
*/
716+
public Builder setRedirectMethodPolicy(final RedirectMethodPolicy policy) {
717+
this.redirectMethodPolicy = Args.notNull(policy, "policy");
718+
return this;
719+
}
720+
696721

697722
public RequestConfig build() {
698723
return new RequestConfig(
@@ -713,7 +738,8 @@ public RequestConfig build() {
713738
hardCancellationEnabled,
714739
protocolUpgradeEnabled,
715740
unixDomainSocket,
716-
expectContinueTrigger);
741+
expectContinueTrigger,
742+
redirectMethodPolicy);
717743
}
718744

719745
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncRedirectExec.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import org.apache.hc.core5.http.support.BasicRequestBuilder;
6161
import org.slf4j.Logger;
6262
import org.slf4j.LoggerFactory;
63+
import org.apache.hc.client5.http.config.RedirectMethodPolicy; // <<< ADDED
6364

6465
/**
6566
* Request execution handler in the asynchronous request execution chain
@@ -147,8 +148,14 @@ public AsyncDataConsumer handleResponse(
147148
case HttpStatus.SC_MOVED_PERMANENTLY:
148149
case HttpStatus.SC_MOVED_TEMPORARILY:
149150
if (Method.POST.isSame(request.getMethod())) {
150-
redirectBuilder = BasicRequestBuilder.get();
151-
state.currentEntityProducer = null;
151+
final RedirectMethodPolicy methodPolicy = config.getRedirectMethodPolicy();
152+
final boolean sameAuth = Objects.equals(currentScope.route.getTargetHost(), newTarget);
153+
if (methodPolicy == RedirectMethodPolicy.PRESERVE_METHOD || methodPolicy == RedirectMethodPolicy.PRESERVE_SAME_AUTH && sameAuth) {
154+
redirectBuilder = BasicRequestBuilder.copy(currentScope.originalRequest);
155+
} else {
156+
redirectBuilder = BasicRequestBuilder.get();
157+
state.currentEntityProducer = null;
158+
}
152159
} else {
153160
redirectBuilder = BasicRequestBuilder.copy(currentScope.originalRequest);
154161
}
@@ -293,4 +300,4 @@ public void execute(
293300
internalExecute(state, chain, asyncExecCallback);
294301
}
295302

296-
}
303+
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/RedirectExec.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import org.apache.hc.core5.util.Args;
6060
import org.slf4j.Logger;
6161
import org.slf4j.LoggerFactory;
62+
import org.apache.hc.client5.http.config.RedirectMethodPolicy; // <<< ADDED
6263

6364
/**
6465
* Request execution handler in the classic request execution chain
@@ -146,7 +147,13 @@ public ClassicHttpResponse execute(
146147
case HttpStatus.SC_MOVED_PERMANENTLY:
147148
case HttpStatus.SC_MOVED_TEMPORARILY:
148149
if (Method.POST.isSame(request.getMethod())) {
149-
redirectBuilder = ClassicRequestBuilder.get();
150+
final RedirectMethodPolicy methodPolicy = config.getRedirectMethodPolicy();
151+
final boolean sameAuth = Objects.equals(currentScope.route.getTargetHost(), newTarget);
152+
if (methodPolicy == RedirectMethodPolicy.PRESERVE_METHOD || methodPolicy == RedirectMethodPolicy.PRESERVE_SAME_AUTH && sameAuth) {
153+
redirectBuilder = ClassicRequestBuilder.copy(currentScope.originalRequest);
154+
} else {
155+
redirectBuilder = ClassicRequestBuilder.get();
156+
}
150157
} else {
151158
redirectBuilder = ClassicRequestBuilder.copy(currentScope.originalRequest);
152159
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http.examples;
28+
29+
import java.util.concurrent.Future;
30+
31+
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
32+
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
33+
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
34+
import org.apache.hc.client5.http.async.methods.SimpleRequestProducer;
35+
import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer;
36+
import org.apache.hc.client5.http.config.RedirectMethodPolicy;
37+
import org.apache.hc.client5.http.config.RequestConfig;
38+
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
39+
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
40+
import org.apache.hc.core5.concurrent.FutureCallback;
41+
import org.apache.hc.core5.http.ContentType;
42+
import org.apache.hc.core5.http.message.StatusLine;
43+
import org.apache.hc.core5.io.CloseMode;
44+
45+
/**
46+
* Demonstrates how to control 301/302 redirect method rewriting in the
47+
* <b>async</b> client using {@link RedirectMethodPolicy}.
48+
* <p>
49+
* The example executes the same JSON POST twice against a 301 redirecting URL:
50+
* once with the default browser-compatible policy (resulting in a GET without body),
51+
* and once with {@link RedirectMethodPolicy#PRESERVE_METHOD} (resulting in a POST with body).
52+
* </p>
53+
*
54+
* <h3>Notes</h3>
55+
* <ul>
56+
* <li>When preserving the method, the {@code AsyncEntityProducer} must be repeatable.</li>
57+
* <li>303 is always followed with GET; 307/308 always preserve method/body.</li>
58+
* <li>Redirect safety rules (e.g., stripping {@code Authorization} across authorities) still apply.</li>
59+
* </ul>
60+
*
61+
* <h3>How to run</h3>
62+
* <pre>{@code
63+
* $ mvn -q -DskipTests exec:java -Dexec.mainClass=org.apache.hc.client5.http.examples.AsyncClientRedirectPreserveMethod
64+
* }</pre>
65+
*
66+
* @see RequestConfig#setRedirectMethodPolicy(RedirectMethodPolicy)
67+
* @see RedirectMethodPolicy
68+
* @since 5.6
69+
*/
70+
public class AsyncClientRedirectPreserveMethod {
71+
72+
private static String redirectUrl() {
73+
// httpbin: redirect to /anything with status 301
74+
return "https://httpbin.org/redirect-to?url=/anything&status_code=301";
75+
}
76+
77+
private static void runOnce(
78+
final CloseableHttpAsyncClient client,
79+
final String label) throws Exception {
80+
81+
final SimpleHttpRequest req = SimpleRequestBuilder.post(redirectUrl())
82+
.setBody("{\"hello\":\"world\"}", ContentType.APPLICATION_JSON)
83+
.build();
84+
85+
System.out.println("\n[" + label + "] Executing " + req);
86+
final Future<SimpleHttpResponse> f = client.execute(
87+
SimpleRequestProducer.create(req),
88+
SimpleResponseConsumer.create(),
89+
new FutureCallback<SimpleHttpResponse>() {
90+
@Override
91+
public void completed(final SimpleHttpResponse response) {
92+
System.out.println("[" + label + "] " + new StatusLine(response));
93+
final String body = response.getBodyText();
94+
System.out.println(body != null ? body : "");
95+
}
96+
97+
@Override
98+
public void failed(final Exception ex) {
99+
System.out.println("[" + label + "] failed: " + ex);
100+
}
101+
102+
@Override
103+
public void cancelled() {
104+
System.out.println("[" + label + "] cancelled");
105+
}
106+
});
107+
f.get();
108+
}
109+
110+
public static void main(final String[] args) throws Exception {
111+
final RequestConfig browserCompat = RequestConfig.custom()
112+
.setRedirectsEnabled(true)
113+
.setRedirectMethodPolicy(RedirectMethodPolicy.BROWSER_COMPAT)
114+
.build();
115+
116+
final RequestConfig preserveMethod = RequestConfig.custom()
117+
.setRedirectsEnabled(true)
118+
.setRedirectMethodPolicy(RedirectMethodPolicy.PRESERVE_METHOD)
119+
.build();
120+
121+
try (CloseableHttpAsyncClient clientDefault = HttpAsyncClients.custom()
122+
.setDefaultRequestConfig(browserCompat)
123+
.build();
124+
CloseableHttpAsyncClient clientPreserve = HttpAsyncClients.custom()
125+
.setDefaultRequestConfig(preserveMethod)
126+
.build()) {
127+
128+
System.out.println("== Async client redirect demo ==");
129+
System.out.println("URL: " + redirectUrl());
130+
System.out.println("Sending POST with JSON body...\n");
131+
132+
clientDefault.start();
133+
clientPreserve.start();
134+
135+
runOnce(clientDefault, "Default (BROWSER_COMPAT: POST→GET)");
136+
runOnce(clientPreserve, "Opt-in (PRESERVE_METHOD: keep POST)");
137+
138+
System.out.println("\nShutting down");
139+
clientDefault.close(CloseMode.GRACEFUL);
140+
clientPreserve.close(CloseMode.GRACEFUL);
141+
}
142+
}
143+
}

0 commit comments

Comments
 (0)