Skip to content

Commit ce595eb

Browse files
Migrated redirect, retry handlers and retry options to okhttp
1 parent 4cb3031 commit ce595eb

File tree

4 files changed

+187
-67
lines changed

4 files changed

+187
-67
lines changed

src/main/java/com/microsoft/graph/httpcore/RedirectHandler.java

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,47 @@
99
import java.io.IOException;
1010
import java.net.ProtocolException;
1111

12+
import com.microsoft.graph.httpcore.middlewareoption.MiddlewareType;
13+
import com.microsoft.graph.httpcore.middlewareoption.RedirectOptions;
14+
1215
import okhttp3.HttpUrl;
1316
import okhttp3.Interceptor;
1417
import okhttp3.Request;
15-
import okhttp3.RequestBody;
1618
import okhttp3.Response;
17-
import okhttp3.internal.http.HttpMethod;
1819

1920
public class RedirectHandler implements Interceptor{
2021

21-
public static final RedirectHandler INSTANCE = new RedirectHandler();
22-
final int maxRedirect = 5;
22+
public final MiddlewareType MIDDLEWARE_TYPE = MiddlewareType.REDIRECT;
2323

24-
public boolean isRedirected(Request request, Response response, int redirectCount) throws IOException {
25-
26-
if(redirectCount > maxRedirect) return false;
24+
private RedirectOptions mRedirectOptions;
25+
26+
/*
27+
* Initialize using default redirect options, default IShouldRedirect and max redirect value
28+
*/
29+
public RedirectHandler() {
30+
this.mRedirectOptions = new RedirectOptions();
31+
}
32+
33+
/*
34+
* @param redirectOptions pass instance of redirect options to be used
35+
*/
36+
public RedirectHandler(RedirectOptions redirectOptions) {
37+
this.mRedirectOptions = redirectOptions;
38+
if(redirectOptions == null) {
39+
this.mRedirectOptions = new RedirectOptions();
40+
}
41+
}
42+
43+
public boolean isRedirected(Request request, Response response, int redirectCount, RedirectOptions redirectOptions) throws IOException {
44+
// Check max count of redirects reached
45+
if(redirectCount > redirectOptions.maxRedirects()) return false;
2746

47+
// Location header empty then don't redirect
2848
final String locationHeader = response.header("location");
2949
if(locationHeader == null)
3050
return false;
3151

52+
// If any of 301,302,303,307,308 then redirect
3253
final int statusCode = response.code();
3354
if(statusCode == HTTP_PERM_REDIRECT || //308
3455
statusCode == HTTP_MOVED_PERM || //301
@@ -46,12 +67,15 @@ public Request getRedirect(
4667
String location = userResponse.header("Location");
4768
if (location == null) return null;
4869

70+
// TODO: If Location header is relative reference then final URL should be relative to original target URL.
71+
4972
HttpUrl requestUrl = userResponse.request().url();
5073

5174
HttpUrl locationUrl = userResponse.request().url().resolve(location);
75+
5276
// Don't follow redirects to unsupported protocols.
5377
if (locationUrl == null) return null;
54-
78+
5579
// Most redirects don't include a request body.
5680
Request.Builder requestBuilder = userResponse.request().newBuilder();
5781

@@ -63,25 +87,37 @@ public Request getRedirect(
6387
if (!sameScheme || !sameHost) {
6488
requestBuilder.removeHeader("Authorization");
6589
}
90+
91+
// Response status code 303 See Other then POST changes to GET
92+
if(userResponse.code() == HTTP_SEE_OTHER) {
93+
requestBuilder.method("GET", null);
94+
}
6695

6796
return requestBuilder.url(locationUrl).build();
6897
}
6998

99+
// Intercept request and response made to network
70100
@Override
71101
public Response intercept(Chain chain) throws IOException {
72102
Request request = chain.request();
73103
Response response = null;
74-
int redirectCount = 1;
104+
int requestsCount = 1;
105+
106+
// Use should retry pass along with this request
107+
RedirectOptions redirectOptions = request.tag(RedirectOptions.class);
108+
redirectOptions = redirectOptions != null ? redirectOptions : this.mRedirectOptions;
109+
75110
while(true) {
76111
response = chain.proceed(request);
77-
boolean shouldRedirect = isRedirected(request, response, redirectCount);
112+
boolean shouldRedirect = redirectOptions.shouldRedirect().shouldRedirect(response)
113+
&& isRedirected(request, response, requestsCount, redirectOptions);
78114
if(!shouldRedirect) break;
79115

80116
Request followup = getRedirect(request, response);
81117
if(followup == null) break;
82118
request = followup;
83119

84-
redirectCount++;
120+
requestsCount++;
85121
}
86122
return response;
87123
}

src/main/java/com/microsoft/graph/httpcore/RetryHandler.java

Lines changed: 83 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,102 +2,137 @@
22

33
import java.io.IOException;
44

5+
import com.microsoft.graph.httpcore.middlewareoption.IShouldRetry;
6+
import com.microsoft.graph.httpcore.middlewareoption.MiddlewareType;
7+
import com.microsoft.graph.httpcore.middlewareoption.RedirectOptions;
58
import com.microsoft.graph.httpcore.middlewareoption.RetryOptions;
69

710
import okhttp3.Interceptor;
811
import okhttp3.Request;
912
import okhttp3.Response;
1013

1114
public class RetryHandler implements Interceptor{
15+
16+
public final MiddlewareType MIDDLEWARE_TYPE = MiddlewareType.RETRY;
1217

13-
/**
14-
* Maximum number of allowed retries if the server responds with a HTTP code
15-
* in our retry code list. Default value is 1.
16-
*/
17-
private final int maxRetries = 2;
18-
19-
/**
20-
* Retry interval between subsequent requests, in milliseconds. Default
21-
* value is 1 second.
18+
private RetryOptions mRetryOption;
19+
20+
/*
21+
* constant string being used
2222
*/
23-
private long retryInterval = 1000;
24-
private final int DELAY_MILLISECONDS = 1000;
23+
private final String RETRY_ATTEMPT_HEADER = "Retry-Attempt";
2524
private final String RETRY_AFTER = "Retry-After";
2625
private final String TRANSFER_ENCODING = "Transfer-Encoding";
26+
private final String TRANSFER_ENCODING_CHUNKED = "chunked";
27+
private final String APPLICATION_OCTET_STREAM = "application/octet-stream";
28+
private final String CONTENT_TYPE = "Content-Type";
29+
30+
public static final int MSClientErrorCodeTooManyRequests = 429;
31+
public static final int MSClientErrorCodeServiceUnavailable = 503;
32+
public static final int MSClientErrorCodeGatewayTimeout = 504;
2733

28-
private final int MSClientErrorCodeTooManyRequests = 429;
29-
private final int MSClientErrorCodeServiceUnavailable = 503;
30-
private final int MSClientErrorCodeGatewayTimeout = 504;
31-
private final RetryOptions mRetryOption;
34+
private final long DELAY_MILLISECONDS = 1000;
3235

33-
public RetryHandler(RetryOptions option) {
34-
super();
35-
this.mRetryOption = option;
36+
/*
37+
* @retryOption Create Retry handler using retry option
38+
*/
39+
public RetryHandler(RetryOptions retryOption) {
40+
this.mRetryOption = retryOption;
41+
if(this.mRetryOption == null) {
42+
this.mRetryOption = new RetryOptions();
43+
}
3644
}
37-
45+
/*
46+
* Initialize retry handler with default retry option
47+
*/
3848
public RetryHandler() {
3949
this(null);
4050
}
4151

42-
public boolean retryRequest(Response response, int executionCount, Request request) {
52+
private boolean retryRequest(Response response, int executionCount, Request request, RetryOptions retryOptions) {
4353

44-
RetryOptions retryOption = request.tag(RetryOptions.class);
45-
if(retryOption != null) {
46-
return retryOption.shouldRetry().shouldRetry(response, executionCount, request);
54+
// Should retry option
55+
// Use should retry common for all requests
56+
IShouldRetry shouldRetryCallback = null;
57+
if(retryOptions != null) {
58+
shouldRetryCallback = retryOptions.shouldRetry();
4759
}
48-
if(mRetryOption != null) {
49-
return mRetryOption.shouldRetry().shouldRetry(response, executionCount, request);
60+
// Call should retry callback
61+
if(shouldRetryCallback != null) {
62+
shouldRetryCallback.shouldRetry(response, executionCount, request, retryOptions.delay());
5063
}
5164

5265
boolean shouldRetry = false;
66+
// Status codes 429 503 504
5367
int statusCode = response.code();
54-
shouldRetry = (executionCount < maxRetries) && checkStatus(statusCode) && isBuffered(response, request);
68+
// Only requests with payloads that are buffered/rewindable are supported.
69+
// Payloads with forward only streams will be have the responses returned
70+
// without any retry attempt.
71+
shouldRetry = (executionCount <= retryOptions.maxRetries()) && checkStatus(statusCode) && isBuffered(response, request);
5572

5673
if(shouldRetry) {
57-
String retryAfterHeader = response.header(RETRY_AFTER);
58-
if(retryAfterHeader != null)
59-
retryInterval = Long.parseLong(retryAfterHeader);
60-
else
61-
retryInterval = (long)Math.pow(2.0, (double)executionCount) * DELAY_MILLISECONDS;
74+
long retryInterval = getRetryAfter(response, retryOptions.delay(), executionCount);
75+
try {
76+
Thread.sleep(retryInterval);
77+
} catch (InterruptedException e) {
78+
e.printStackTrace();
79+
}
6280
}
6381
return shouldRetry;
6482
}
65-
66-
public long getRetryInterval() {
67-
return retryInterval;
83+
84+
private long getRetryAfter(Response response, long delay, int executionCount) {
85+
String retryAfterHeader = response.header(RETRY_AFTER);
86+
long retryDelay = RetryOptions.DEFAULT_DELAY;
87+
if(retryAfterHeader != null) {
88+
retryDelay = Long.parseLong(retryAfterHeader);
89+
} else {
90+
retryDelay = (long)Math.pow(2.0, (double)executionCount) * DELAY_MILLISECONDS;
91+
retryDelay = executionCount < 2 ? retryDelay : retryDelay + delay + (long)Math.random();
92+
}
93+
return Math.min(retryDelay, RetryOptions.MAX_DELAY);
6894
}
6995

7096
private boolean checkStatus(int statusCode) {
71-
if (statusCode == MSClientErrorCodeTooManyRequests || statusCode == MSClientErrorCodeServiceUnavailable
72-
|| statusCode == MSClientErrorCodeGatewayTimeout)
73-
return true;
74-
return false;
97+
return (statusCode == MSClientErrorCodeTooManyRequests || statusCode == MSClientErrorCodeServiceUnavailable
98+
|| statusCode == MSClientErrorCodeGatewayTimeout);
7599
}
76100

77101
private boolean isBuffered(Response response, Request request) {
78102
String methodName = request.method();
103+
if(methodName.equalsIgnoreCase("GET") || methodName.equalsIgnoreCase("DELETE"))
104+
return true;
79105

80106
boolean isHTTPMethodPutPatchOrPost = methodName.equalsIgnoreCase("POST") ||
81107
methodName.equalsIgnoreCase("PUT") ||
82108
methodName.equalsIgnoreCase("PATCH");
83109

84-
//Header transferEncoding = response.getFirstHeader(TRANSFER_ENCODING);
85-
String transferEncoding = response.header(TRANSFER_ENCODING);
86-
boolean isTransferEncodingChunked = (transferEncoding != null) &&
87-
transferEncoding.equalsIgnoreCase("chunked");
88-
89-
if(request.body() != null && isHTTPMethodPutPatchOrPost && isTransferEncodingChunked)
90-
return false;
91-
return true;
110+
if(isHTTPMethodPutPatchOrPost) {
111+
boolean isStream = response.header(CONTENT_TYPE)!=null && response.header(CONTENT_TYPE).equalsIgnoreCase(APPLICATION_OCTET_STREAM);
112+
if(!isStream) {
113+
String transferEncoding = response.header(TRANSFER_ENCODING);
114+
boolean isTransferEncodingChunked = (transferEncoding != null) &&
115+
transferEncoding.equalsIgnoreCase(TRANSFER_ENCODING_CHUNKED);
116+
if(request.body() != null && isTransferEncodingChunked)
117+
return true;
118+
}
119+
}
120+
return false;
92121
}
93122

94123
@Override
95124
public Response intercept(Chain chain) throws IOException {
96125
Request request = chain.request();
97126

98127
Response response = chain.proceed(request);
99-
int executionCount = 0;
100-
while(retryRequest(response, executionCount, request)) {
128+
129+
// Use should retry pass along with this request
130+
RetryOptions retryOption = request.tag(RetryOptions.class);
131+
retryOption = retryOption != null ? retryOption : this.mRetryOption;
132+
133+
int executionCount = 1;
134+
while(retryRequest(response, executionCount, request, retryOption)) {
135+
request = request.newBuilder().addHeader(RETRY_ATTEMPT_HEADER, String.valueOf(executionCount)).build();
101136
executionCount++;
102137
response = chain.proceed(request);
103138
}

src/main/java/com/microsoft/graph/httpcore/middlewareoption/IShouldRetry.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
import okhttp3.Response;
55

66
public interface IShouldRetry {
7-
boolean shouldRetry(Response response, int executionCount, Request request);
7+
boolean shouldRetry(Response response, int executionCount, Request request, long delay);
88
}

src/main/java/com/microsoft/graph/httpcore/middlewareoption/RetryOptions.java

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,69 @@
55

66
public class RetryOptions implements IMiddlewareControl {
77
private IShouldRetry shouldretry;
8+
public static final IShouldRetry DEFAULT_SHOULD_RETRY = new IShouldRetry() {
9+
@Override
10+
public boolean shouldRetry(Response response, int executionCount, Request request, long delay) {
11+
return true;
12+
}
13+
};
814

15+
private int maxRetries;
16+
public static final int MAX_RETRIES = 10;
17+
public static final int DEFAULT_MAX_RETRIES = 3;
18+
19+
/*
20+
* Delay in seconds
21+
*/
22+
private long delay;
23+
public static final long DEFAULT_DELAY = 3; // 3 seconds default delay
24+
public static final long MAX_DELAY = 180; // 180 second max delay
25+
26+
/*
27+
* Create default instance of retry options, with default values of delay, max retries and shouldRetry callback.
28+
*/
929
public RetryOptions(){
10-
this(new IShouldRetry() {
11-
public boolean shouldRetry(Response response, int executionCount, Request request) {
12-
return true;
13-
}
14-
});
30+
this(DEFAULT_SHOULD_RETRY, DEFAULT_MAX_RETRIES, DEFAULT_DELAY);
1531
}
1632

17-
public RetryOptions(IShouldRetry shouldretry){
18-
this.shouldretry = shouldretry;
33+
/*
34+
* @param shouldRetry Retry callback to be called before making a retry
35+
* @param maxRetries Number of max retires for a request
36+
* @param delay Delay in seconds between retries
37+
*/
38+
public RetryOptions(IShouldRetry shouldRetry, int maxRetries, long delay) {
39+
if(delay > MAX_DELAY)
40+
throw new IllegalArgumentException("Delay cannot exceed " + MAX_DELAY);
41+
if(delay < 0)
42+
throw new IllegalArgumentException("Delay cannot be negative");
43+
if(maxRetries > MAX_RETRIES)
44+
throw new IllegalArgumentException("Max retires cannot exceed " + MAX_RETRIES);
45+
if(maxRetries < 0)
46+
throw new IllegalArgumentException("Max retires cannot be negative");
47+
48+
this.shouldretry = shouldRetry != null ? shouldRetry : DEFAULT_SHOULD_RETRY;
49+
this.maxRetries = maxRetries;
50+
this.delay = delay;
1951
}
2052

53+
/*
54+
* @return should retry callback
55+
*/
2156
public IShouldRetry shouldRetry() {
2257
return shouldretry;
2358
}
59+
60+
/*
61+
* @return Number of max retries
62+
*/
63+
public int maxRetries() {
64+
return maxRetries;
65+
}
66+
67+
/*
68+
* @return Delay in seconds between retries
69+
*/
70+
public long delay() {
71+
return delay;
72+
}
2473
}

0 commit comments

Comments
 (0)