Skip to content
Closed
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
@@ -0,0 +1,51 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.config;


/**
* Policy controlling method/body rewriting on 301/302 redirects.
*
* @since 5.6
*/
public enum RedirectMethodPolicy {
/**
* Browser compatibility: POST→GET for 301/302; 303→GET; 307/308 preserve.
*/
BROWSER_COMPAT,

/**
* Preserve original method (& body if repeatable) for 301/302.
*/
PRESERVE_METHOD,

/**
* Preserve original method (& body if repeatable) for 301/302
* only when the redirect stays on the same authority (scheme+host+port).
*/
PRESERVE_SAME_AUTH
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ public class RequestConfig implements Cloneable {

private final ExpectContinueTrigger expectContinueTrigger;

private final RedirectMethodPolicy redirectMethodPolicy;

/**
* Intended for CDI compatibility
*/
protected RequestConfig() {
this(false, null, null, false, false, 0, false, null, null,
DEFAULT_CONNECTION_REQUEST_TIMEOUT, null, null, DEFAULT_CONN_KEEP_ALIVE, false, false, false, null,
ExpectContinueTrigger.ALWAYS);
ExpectContinueTrigger.ALWAYS, null);
}

RequestConfig(
Expand All @@ -96,7 +98,8 @@ protected RequestConfig() {
final boolean hardCancellationEnabled,
final boolean protocolUpgradeEnabled,
final Path unixDomainSocket,
final ExpectContinueTrigger expectContinueTrigger) {
final ExpectContinueTrigger expectContinueTrigger,
final RedirectMethodPolicy redirectMethodPolicy) {
super();
this.expectContinueEnabled = expectContinueEnabled;
this.proxy = proxy;
Expand All @@ -116,6 +119,7 @@ protected RequestConfig() {
this.protocolUpgradeEnabled = protocolUpgradeEnabled;
this.unixDomainSocket = unixDomainSocket;
this.expectContinueTrigger = expectContinueTrigger;
this.redirectMethodPolicy = redirectMethodPolicy;
}

/**
Expand Down Expand Up @@ -248,6 +252,13 @@ public ExpectContinueTrigger getExpectContinueTrigger() {
return expectContinueTrigger;
}

/**
* @since 5.6
*/
public RedirectMethodPolicy getRedirectMethodPolicy() {
return redirectMethodPolicy;
}

@Override
protected RequestConfig clone() throws CloneNotSupportedException {
return (RequestConfig) super.clone();
Expand All @@ -274,6 +285,7 @@ public String toString() {
builder.append(", hardCancellationEnabled=").append(hardCancellationEnabled);
builder.append(", protocolUpgradeEnabled=").append(protocolUpgradeEnabled);
builder.append(", unixDomainSocket=").append(unixDomainSocket);
builder.append(", redirectMethodPolicy=").append(redirectMethodPolicy);
builder.append("]");
return builder.toString();
}
Expand Down Expand Up @@ -323,6 +335,7 @@ public static class Builder {
private boolean protocolUpgradeEnabled;
private Path unixDomainSocket;
private ExpectContinueTrigger expectContinueTrigger;
private RedirectMethodPolicy redirectMethodPolicy;

Builder() {
super();
Expand All @@ -334,6 +347,7 @@ public static class Builder {
this.hardCancellationEnabled = true;
this.protocolUpgradeEnabled = true;
this.expectContinueTrigger = ExpectContinueTrigger.ALWAYS;
this.redirectMethodPolicy = RedirectMethodPolicy.BROWSER_COMPAT;
}

/**
Expand Down Expand Up @@ -693,6 +707,17 @@ public Builder setExpectContinueTrigger(final ExpectContinueTrigger trigger) {
this.expectContinueTrigger = Args.notNull(trigger, "ExpectContinueTrigger");
return this;
}
/**
* Control method/body rewriting for 301/302.
* Default is {@link RedirectMethodPolicy#BROWSER_COMPAT}.
*
* @since 5.6
*/
public Builder setRedirectMethodPolicy(final RedirectMethodPolicy policy) {
this.redirectMethodPolicy = Args.notNull(policy, "policy");
return this;
}


public RequestConfig build() {
return new RequestConfig(
Expand All @@ -713,7 +738,8 @@ public RequestConfig build() {
hardCancellationEnabled,
protocolUpgradeEnabled,
unixDomainSocket,
expectContinueTrigger);
expectContinueTrigger,
redirectMethodPolicy);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import org.apache.hc.core5.http.support.BasicRequestBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.hc.client5.http.config.RedirectMethodPolicy; // <<< ADDED

/**
* Request execution handler in the asynchronous request execution chain
Expand Down Expand Up @@ -147,8 +148,14 @@ public AsyncDataConsumer handleResponse(
case HttpStatus.SC_MOVED_PERMANENTLY:
case HttpStatus.SC_MOVED_TEMPORARILY:
if (Method.POST.isSame(request.getMethod())) {
redirectBuilder = BasicRequestBuilder.get();
state.currentEntityProducer = null;
final RedirectMethodPolicy methodPolicy = config.getRedirectMethodPolicy();
final boolean sameAuth = Objects.equals(currentScope.route.getTargetHost(), newTarget);
if (methodPolicy == RedirectMethodPolicy.PRESERVE_METHOD || methodPolicy == RedirectMethodPolicy.PRESERVE_SAME_AUTH && sameAuth) {
redirectBuilder = BasicRequestBuilder.copy(currentScope.originalRequest);
} else {
redirectBuilder = BasicRequestBuilder.get();
state.currentEntityProducer = null;
}
} else {
redirectBuilder = BasicRequestBuilder.copy(currentScope.originalRequest);
}
Expand Down Expand Up @@ -293,4 +300,4 @@ public void execute(
internalExecute(state, chain, asyncExecCallback);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import org.apache.hc.core5.util.Args;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.hc.client5.http.config.RedirectMethodPolicy; // <<< ADDED

/**
* Request execution handler in the classic request execution chain
Expand Down Expand Up @@ -146,7 +147,13 @@ public ClassicHttpResponse execute(
case HttpStatus.SC_MOVED_PERMANENTLY:
case HttpStatus.SC_MOVED_TEMPORARILY:
if (Method.POST.isSame(request.getMethod())) {
redirectBuilder = ClassicRequestBuilder.get();
final RedirectMethodPolicy methodPolicy = config.getRedirectMethodPolicy();
final boolean sameAuth = Objects.equals(currentScope.route.getTargetHost(), newTarget);
if (methodPolicy == RedirectMethodPolicy.PRESERVE_METHOD || methodPolicy == RedirectMethodPolicy.PRESERVE_SAME_AUTH && sameAuth) {
redirectBuilder = ClassicRequestBuilder.copy(currentScope.originalRequest);
} else {
redirectBuilder = ClassicRequestBuilder.get();
}
} else {
redirectBuilder = ClassicRequestBuilder.copy(currentScope.originalRequest);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.examples;

import java.util.concurrent.Future;

import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
import org.apache.hc.client5.http.async.methods.SimpleRequestProducer;
import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer;
import org.apache.hc.client5.http.config.RedirectMethodPolicy;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.message.StatusLine;
import org.apache.hc.core5.io.CloseMode;

/**
* Demonstrates how to control 301/302 redirect method rewriting in the
* <b>async</b> client using {@link RedirectMethodPolicy}.
* <p>
* The example executes the same JSON POST twice against a 301 redirecting URL:
* once with the default browser-compatible policy (resulting in a GET without body),
* and once with {@link RedirectMethodPolicy#PRESERVE_METHOD} (resulting in a POST with body).
* </p>
*
* <h3>Notes</h3>
* <ul>
* <li>When preserving the method, the {@code AsyncEntityProducer} must be repeatable.</li>
* <li>303 is always followed with GET; 307/308 always preserve method/body.</li>
* <li>Redirect safety rules (e.g., stripping {@code Authorization} across authorities) still apply.</li>
* </ul>
*
* <h3>How to run</h3>
* <pre>{@code
* $ mvn -q -DskipTests exec:java -Dexec.mainClass=org.apache.hc.client5.http.examples.AsyncClientRedirectPreserveMethod
* }</pre>
*
* @see RequestConfig#setRedirectMethodPolicy(RedirectMethodPolicy)
* @see RedirectMethodPolicy
* @since 5.6
*/
public class AsyncClientRedirectPreserveMethod {

private static String redirectUrl() {
// httpbin: redirect to /anything with status 301
return "https://httpbin.org/redirect-to?url=/anything&status_code=301";
}

private static void runOnce(
final CloseableHttpAsyncClient client,
final String label) throws Exception {

final SimpleHttpRequest req = SimpleRequestBuilder.post(redirectUrl())
.setBody("{\"hello\":\"world\"}", ContentType.APPLICATION_JSON)
.build();

System.out.println("\n[" + label + "] Executing " + req);
final Future<SimpleHttpResponse> f = client.execute(
SimpleRequestProducer.create(req),
SimpleResponseConsumer.create(),
new FutureCallback<SimpleHttpResponse>() {
@Override
public void completed(final SimpleHttpResponse response) {
System.out.println("[" + label + "] " + new StatusLine(response));
final String body = response.getBodyText();
System.out.println(body != null ? body : "");
}

@Override
public void failed(final Exception ex) {
System.out.println("[" + label + "] failed: " + ex);
}

@Override
public void cancelled() {
System.out.println("[" + label + "] cancelled");
}
});
f.get();
}

public static void main(final String[] args) throws Exception {
final RequestConfig browserCompat = RequestConfig.custom()
.setRedirectsEnabled(true)
.setRedirectMethodPolicy(RedirectMethodPolicy.BROWSER_COMPAT)
.build();

final RequestConfig preserveMethod = RequestConfig.custom()
.setRedirectsEnabled(true)
.setRedirectMethodPolicy(RedirectMethodPolicy.PRESERVE_METHOD)
.build();

try (CloseableHttpAsyncClient clientDefault = HttpAsyncClients.custom()
.setDefaultRequestConfig(browserCompat)
.build();
CloseableHttpAsyncClient clientPreserve = HttpAsyncClients.custom()
.setDefaultRequestConfig(preserveMethod)
.build()) {

System.out.println("== Async client redirect demo ==");
System.out.println("URL: " + redirectUrl());
System.out.println("Sending POST with JSON body...\n");

clientDefault.start();
clientPreserve.start();

runOnce(clientDefault, "Default (BROWSER_COMPAT: POST→GET)");
runOnce(clientPreserve, "Opt-in (PRESERVE_METHOD: keep POST)");

System.out.println("\nShutting down");
clientDefault.close(CloseMode.GRACEFUL);
clientPreserve.close(CloseMode.GRACEFUL);
}
}
}
Loading
Loading