-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Redirect Policy to Azure core (#23617)
- Loading branch information
Showing
6 changed files
with
666 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
208 changes: 208 additions & 0 deletions
208
sdk/core/azure-core/src/main/java/com/azure/core/http/policy/DefaultRedirectStrategy.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package com.azure.core.http.policy; | ||
|
||
import com.azure.core.http.HttpHeaders; | ||
import com.azure.core.http.HttpMethod; | ||
import com.azure.core.http.HttpPipelineCallContext; | ||
import com.azure.core.http.HttpRequest; | ||
import com.azure.core.http.HttpResponse; | ||
import com.azure.core.util.CoreUtils; | ||
import com.azure.core.util.logging.ClientLogger; | ||
|
||
import java.net.HttpURLConnection; | ||
import java.util.HashSet; | ||
import java.util.Set; | ||
|
||
/** | ||
* A default implementation of {@link RedirectStrategy} that uses the provided maximum retry attempts, | ||
* header name to look up redirect url value for, http methods and a known set of | ||
* redirect status response codes (301, 302, 307, 308) to determine if request should be redirected. | ||
*/ | ||
public final class DefaultRedirectStrategy implements RedirectStrategy { | ||
private final ClientLogger logger = new ClientLogger(DefaultRedirectStrategy.class); | ||
|
||
private static final int DEFAULT_MAX_REDIRECT_ATTEMPTS = 3; | ||
private static final String DEFAULT_REDIRECT_LOCATION_HEADER_NAME = "Location"; | ||
private static final int PERMANENT_REDIRECT_STATUS_CODE = 308; | ||
private static final int TEMPORARY_REDIRECT_STATUS_CODE = 307; | ||
private static final Set<HttpMethod> DEFAULT_REDIRECT_ALLOWED_METHODS = new HashSet<HttpMethod>() { | ||
{ | ||
add(HttpMethod.GET); | ||
add(HttpMethod.HEAD); | ||
} | ||
}; | ||
|
||
private final int maxAttempts; | ||
private final String locationHeader; | ||
private final Set<HttpMethod> redirectMethods; | ||
|
||
/** | ||
* Creates an instance of {@link DefaultRedirectStrategy} with a maximum number of redirect attempts 3, | ||
* header name "Location" to locate the redirect url in the response headers and {@link HttpMethod#GET} | ||
* and {@link HttpMethod#HEAD} as allowed methods for performing the redirect. | ||
*/ | ||
public DefaultRedirectStrategy() { | ||
this(DEFAULT_MAX_REDIRECT_ATTEMPTS, DEFAULT_REDIRECT_LOCATION_HEADER_NAME, DEFAULT_REDIRECT_ALLOWED_METHODS); | ||
} | ||
|
||
/** | ||
* Creates an instance of {@link DefaultRedirectStrategy} with the provided number of redirect attempts and | ||
* default header name "Location" to locate the redirect url in the response headers and {@link HttpMethod#GET} | ||
* and {@link HttpMethod#HEAD} as allowed methods for performing the redirect. | ||
* | ||
* @param maxAttempts The max number of redirect attempts that can be made. | ||
* @throws IllegalArgumentException if {@code maxAttempts} is less than 0. | ||
*/ | ||
public DefaultRedirectStrategy(int maxAttempts) { | ||
this(maxAttempts, DEFAULT_REDIRECT_LOCATION_HEADER_NAME, DEFAULT_REDIRECT_ALLOWED_METHODS); | ||
} | ||
|
||
/** | ||
* Creates an instance of {@link DefaultRedirectStrategy}. | ||
* | ||
* @param maxAttempts The max number of redirect attempts that can be made. | ||
* @param locationHeader The header name containing the redirect URL. | ||
* @param allowedMethods The set of {@link HttpMethod} that are allowed to be redirected. | ||
* @throws IllegalArgumentException if {@code maxAttempts} is less than 0. | ||
*/ | ||
public DefaultRedirectStrategy(int maxAttempts, String locationHeader, Set<HttpMethod> allowedMethods) { | ||
if (maxAttempts < 0) { | ||
throw logger.logExceptionAsError(new IllegalArgumentException("Max attempts cannot be less than 0.")); | ||
} | ||
this.maxAttempts = maxAttempts; | ||
if (CoreUtils.isNullOrEmpty(locationHeader)) { | ||
logger.error("'locationHeader' provided as null will be defaulted to {}", | ||
DEFAULT_REDIRECT_LOCATION_HEADER_NAME); | ||
this.locationHeader = DEFAULT_REDIRECT_LOCATION_HEADER_NAME; | ||
} else { | ||
this.locationHeader = locationHeader; | ||
} | ||
if (CoreUtils.isNullOrEmpty(allowedMethods)) { | ||
logger.error("'allowedMethods' provided as null will be defaulted to {}", DEFAULT_REDIRECT_ALLOWED_METHODS); | ||
this.redirectMethods = DEFAULT_REDIRECT_ALLOWED_METHODS; | ||
} else { | ||
this.redirectMethods = allowedMethods; | ||
} | ||
} | ||
|
||
@Override | ||
public boolean shouldAttemptRedirect(HttpPipelineCallContext context, | ||
HttpResponse httpResponse, int tryCount, | ||
Set<String> attemptedRedirectUrls) { | ||
String redirectUrl = | ||
tryGetRedirectHeader(httpResponse.getHeaders(), this.getLocationHeader()); | ||
|
||
if (isValidRedirectCount(tryCount) | ||
&& redirectUrl != null | ||
&& !alreadyAttemptedRedirectUrl(redirectUrl, attemptedRedirectUrls) | ||
&& isValidRedirectStatusCode(httpResponse.getStatusCode()) | ||
&& isAllowedRedirectMethod(httpResponse.getRequest().getHttpMethod())) { | ||
logger.verbose("[Redirecting] Try count: {}, Attempted Redirect URLs: {}", tryCount, | ||
attemptedRedirectUrls.toString()); | ||
attemptedRedirectUrls.add(redirectUrl); | ||
return true; | ||
} else { | ||
return false; | ||
} | ||
} | ||
|
||
@Override | ||
public HttpRequest createRedirectRequest(HttpResponse httpResponse) { | ||
String responseLocation = | ||
tryGetRedirectHeader(httpResponse.getHeaders(), this.getLocationHeader()); | ||
return httpResponse.getRequest().setUrl(responseLocation); | ||
} | ||
|
||
@Override | ||
public int getMaxAttempts() { | ||
return maxAttempts; | ||
} | ||
|
||
@Override | ||
public String getLocationHeader() { | ||
return locationHeader; | ||
} | ||
|
||
@Override | ||
public Set<HttpMethod> getAllowedMethods() { | ||
return redirectMethods; | ||
} | ||
|
||
/** | ||
* Check if the redirect url provided in the response headers is already attempted. | ||
* | ||
* @param redirectUrl the redirect url provided in the response header. | ||
* @param attemptedRedirectUrls the set containing a list of attempted redirect locations. | ||
* @return {@code true} if the redirectUrl provided in the response header is already being attempted for redirect | ||
* , {@code false} otherwise. | ||
*/ | ||
private boolean alreadyAttemptedRedirectUrl(String redirectUrl, | ||
Set<String> attemptedRedirectUrls) { | ||
if (attemptedRedirectUrls.contains(redirectUrl)) { | ||
logger.error("Request was redirected more than once to: {}", redirectUrl); | ||
return true; | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* Check if the attempt count of the redirect is less than the {@code maxAttempts} | ||
* | ||
* @param tryCount the try count for the HTTP request associated to the HTTP response. | ||
* @return {@code true} if the {@code tryCount} is greater than the {@code maxAttempts}, {@code false} otherwise. | ||
*/ | ||
private boolean isValidRedirectCount(int tryCount) { | ||
if (tryCount >= getMaxAttempts()) { | ||
logger.error("Request has been redirected more than {} times.", getMaxAttempts()); | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
/** | ||
* Check if the request http method is a valid redirect method. | ||
* | ||
* @param httpMethod the http method of the request. | ||
* @return {@code true} if the request {@code httpMethod} is a valid http redirect method, {@code false} otherwise. | ||
*/ | ||
private boolean isAllowedRedirectMethod(HttpMethod httpMethod) { | ||
if (getAllowedMethods().contains(httpMethod)) { | ||
return true; | ||
} else { | ||
logger.error("Request was redirected from an invalid redirect allowed method: {}", httpMethod); | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Checks if the incoming request status code is a valid redirect status code. | ||
* | ||
* @param statusCode the status code of the incoming request. | ||
* @return {@code true} if the request {@code statusCode} is a valid http redirect method, {@code false} otherwise. | ||
*/ | ||
private boolean isValidRedirectStatusCode(int statusCode) { | ||
return statusCode == HttpURLConnection.HTTP_MOVED_TEMP | ||
|| statusCode == HttpURLConnection.HTTP_MOVED_PERM | ||
|| statusCode == PERMANENT_REDIRECT_STATUS_CODE | ||
|| statusCode == TEMPORARY_REDIRECT_STATUS_CODE; | ||
} | ||
|
||
/** | ||
* Gets the redirect url from the response headers. | ||
* | ||
* @param headers the http response headers. | ||
* @param headerName the header name to look up value for. | ||
* @return the header value for the provided header name, {@code null} otherwise. | ||
*/ | ||
String tryGetRedirectHeader(HttpHeaders headers, String headerName) { | ||
String headerValue = headers.getValue(headerName); | ||
if (CoreUtils.isNullOrEmpty(headerValue)) { | ||
logger.error("Redirect url was null for header name: {}, Request redirect was terminated", headerName); | ||
return null; | ||
} else { | ||
return headerValue; | ||
} | ||
} | ||
} |
73 changes: 73 additions & 0 deletions
73
sdk/core/azure-core/src/main/java/com/azure/core/http/policy/RedirectPolicy.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package com.azure.core.http.policy; | ||
|
||
import com.azure.core.http.HttpPipelineCallContext; | ||
import com.azure.core.http.HttpPipelineNextPolicy; | ||
import com.azure.core.http.HttpRequest; | ||
import com.azure.core.http.HttpResponse; | ||
import reactor.core.publisher.Mono; | ||
|
||
import java.util.HashSet; | ||
import java.util.Objects; | ||
import java.util.Set; | ||
|
||
/** | ||
* A {@link HttpPipelinePolicy} that redirects a {@link HttpRequest} when an HTTP Redirect is received as | ||
* {@link HttpResponse response}. | ||
*/ | ||
public final class RedirectPolicy implements HttpPipelinePolicy { | ||
private final RedirectStrategy redirectStrategy; | ||
|
||
/** | ||
* Creates {@link RedirectPolicy} with default {@link DefaultRedirectStrategy} as {@link RedirectStrategy} and | ||
* uses the redirect status response code (301, 302, 307, 308) to determine if this request should be redirected. | ||
*/ | ||
public RedirectPolicy() { | ||
this(new DefaultRedirectStrategy()); | ||
} | ||
|
||
/** | ||
* Creates {@link RedirectPolicy} with the provided {@code redirectStrategy} as {@link RedirectStrategy} | ||
* to determine if this request should be redirected. | ||
* | ||
* @param redirectStrategy The {@link RedirectStrategy} used for redirection. | ||
* @throws NullPointerException When {@code redirectStrategy} is {@code null}. | ||
*/ | ||
public RedirectPolicy(RedirectStrategy redirectStrategy) { | ||
this.redirectStrategy = Objects.requireNonNull(redirectStrategy, "'redirectStrategy' cannot be null."); | ||
} | ||
|
||
@Override | ||
public Mono<HttpResponse> process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { | ||
// Reset the attemptedRedirectUrls for each individual request. | ||
return attemptRedirect(context, next, context.getHttpRequest(), 1, new HashSet<>()); | ||
} | ||
|
||
/** | ||
* Function to process through the HTTP Response received in the pipeline | ||
* and redirect sending the request with new redirect url. | ||
*/ | ||
private Mono<HttpResponse> attemptRedirect(final HttpPipelineCallContext context, | ||
final HttpPipelineNextPolicy next, | ||
final HttpRequest originalHttpRequest, | ||
final int redirectAttempt, | ||
Set<String> attemptedRedirectUrls) { | ||
// make sure the context is not modified during retry, except for the URL | ||
context.setHttpRequest(originalHttpRequest.copy()); | ||
|
||
return next.clone().process() | ||
.flatMap(httpResponse -> { | ||
if (redirectStrategy.shouldAttemptRedirect(context, httpResponse, redirectAttempt, | ||
attemptedRedirectUrls)) { | ||
HttpRequest redirectRequestCopy = redirectStrategy.createRedirectRequest(httpResponse); | ||
return httpResponse.getBody() | ||
.ignoreElements() | ||
.then(attemptRedirect(context, next, redirectRequestCopy, redirectAttempt + 1, attemptedRedirectUrls)); | ||
} else { | ||
return Mono.just(httpResponse); | ||
} | ||
}); | ||
} | ||
} |
57 changes: 57 additions & 0 deletions
57
sdk/core/azure-core/src/main/java/com/azure/core/http/policy/RedirectStrategy.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package com.azure.core.http.policy; | ||
|
||
import com.azure.core.http.HttpMethod; | ||
import com.azure.core.http.HttpPipelineCallContext; | ||
import com.azure.core.http.HttpRequest; | ||
import com.azure.core.http.HttpResponse; | ||
|
||
import java.util.Set; | ||
|
||
/** | ||
* The interface for determining the {@link RedirectStrategy redirect strategy} used in {@link RedirectPolicy}. | ||
*/ | ||
public interface RedirectStrategy { | ||
/** | ||
* Max number of redirect attempts to be made. | ||
* | ||
* @return The max number of redirect attempts. | ||
*/ | ||
int getMaxAttempts(); | ||
|
||
/** | ||
* The header name to look up the value for the redirect url in response headers. | ||
* | ||
* @return the value of the header, or null if the header doesn't exist in the response. | ||
*/ | ||
String getLocationHeader(); | ||
|
||
/** | ||
* The {@link HttpMethod http methods} that are allowed to be redirected. | ||
* | ||
* @return the set of redirect allowed methods. | ||
*/ | ||
Set<HttpMethod> getAllowedMethods(); | ||
|
||
/** | ||
* Determines if the url should be redirected between each try. | ||
* | ||
* @param context the {@link HttpPipelineCallContext HTTP pipeline context}. | ||
* @param httpResponse the {@link HttpRequest} containing the redirect url present in the response headers | ||
* @param tryCount redirect attempts so far | ||
* @param attemptedRedirectUrls attempted redirect locations used so far. | ||
* @return {@code true} if the request should be redirected, {@code false} otherwise | ||
*/ | ||
boolean shouldAttemptRedirect(HttpPipelineCallContext context, HttpResponse httpResponse, int tryCount, | ||
Set<String> attemptedRedirectUrls); | ||
|
||
/** | ||
* Creates an {@link HttpRequest request} for the redirect attempt. | ||
* | ||
* @param httpResponse the {@link HttpResponse} containing the redirect url present in the response headers | ||
* @return the modified {@link HttpRequest} to redirect the incoming request. | ||
*/ | ||
HttpRequest createRedirectRequest(HttpResponse httpResponse); | ||
} |
Oops, something went wrong.