diff --git a/CHANGELOG.md b/CHANGELOG.md index 415ab30fae..4c297d084b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added + - Added generation of command options for headers defined in the OpenAPI metadata source file. (Shell) +- Added retry, redirect, chaos and telemetry handler in java. ### Changed + - Simplified field deserialization.(PHP) [#1493](https://github.com/microsoft/kiota/issues/1493) - Fixed a bug where the generator would not strip the common namespace component id for models. [#1483](https://github.com/microsoft/kiota/issues/1483) - Simplified field deserialization. [#1490](https://github.com/microsoft/kiota/issues/1490) diff --git a/abstractions/java/lib/build.gradle b/abstractions/java/lib/build.gradle index 294bb3d631..77af8b2df6 100644 --- a/abstractions/java/lib/build.gradle +++ b/abstractions/java/lib/build.gradle @@ -49,7 +49,7 @@ publishing { publications { gpr(MavenPublication) { artifactId 'kiota-abstractions' - version '1.0.31' + version '1.0.32' from(components.java) } } diff --git a/abstractions/java/lib/src/main/java/com/microsoft/kiota/RequestOption.java b/abstractions/java/lib/src/main/java/com/microsoft/kiota/RequestOption.java index 8a07883605..9c542589c6 100644 --- a/abstractions/java/lib/src/main/java/com/microsoft/kiota/RequestOption.java +++ b/abstractions/java/lib/src/main/java/com/microsoft/kiota/RequestOption.java @@ -3,4 +3,5 @@ /** Represents a request option. */ public interface RequestOption { + public Class getType(); } \ No newline at end of file diff --git a/http/java/okhttp/lib/build.gradle b/http/java/okhttp/lib/build.gradle index 00951c11ff..3bfa89526e 100644 --- a/http/java/okhttp/lib/build.gradle +++ b/http/java/okhttp/lib/build.gradle @@ -29,14 +29,16 @@ repositories { dependencies { // Use JUnit Jupiter API for testing. testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' - + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2' // Use JUnit Jupiter Engine for testing. testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.mockito:mockito-inline:4.3.1' + // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:31.1-jre' api 'com.squareup.okhttp3:okhttp:4.9.3' - api 'com.microsoft.kiota:kiota-abstractions:1.0.29' + api 'com.microsoft.kiota:kiota-abstractions:1.0.32' } publishing { @@ -53,7 +55,7 @@ publishing { publications { gpr(MavenPublication) { artifactId 'kiota-http-okhttplibrary' - version '1.0.17' + version '1.0.18' from(components.java) } } diff --git a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/KiotaClientFactory.java b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/KiotaClientFactory.java index 78c3504a2c..277d76609c 100644 --- a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/KiotaClientFactory.java +++ b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/KiotaClientFactory.java @@ -3,6 +3,9 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import com.microsoft.kiota.http.middleware.RedirectHandler; +import com.microsoft.kiota.http.middleware.RetryHandler; + import okhttp3.Interceptor; import okhttp3.OkHttpClient; @@ -37,6 +40,9 @@ public static OkHttpClient.Builder Create(@Nullable final Interceptor[] intercep */ @Nonnull public static Interceptor[] CreateDefaultInterceptors() { - return new Interceptor[] {}; //TODO add the list of default interceptors when they are ready + return new Interceptor[] { + new RedirectHandler(), + new RetryHandler() + }; } } \ No newline at end of file diff --git a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java index 1fff94b18a..96872a6590 100644 --- a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java +++ b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java @@ -34,6 +34,7 @@ import com.microsoft.kiota.store.BackingStoreFactory; import com.microsoft.kiota.store.BackingStoreFactorySingleton; +import kotlin.OptIn; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -333,8 +334,9 @@ public void writeTo(BufferedSink sink) throws IOException { for (final Map.Entry header : requestInfo.headers.entrySet()) { requestBuilder.addHeader(header.getKey(), header.getValue()); } + for(final RequestOption option : requestInfo.getRequestOptions()) { - requestBuilder.tag(option); + requestBuilder.tag(option.getType(), option); } return requestBuilder.build(); } diff --git a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/ChaosHandler.java b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/ChaosHandler.java new file mode 100644 index 0000000000..f1adcb793d --- /dev/null +++ b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/ChaosHandler.java @@ -0,0 +1,63 @@ +package com.microsoft.kiota.http.middleware; + +import java.io.IOException; +import java.util.concurrent.ThreadLocalRandom; + +import javax.annotation.Nonnull; + +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * DO NOT USE IN PRODUCTION + * interceptor that randomly fails the responses for unit testing purposes + */ +public class ChaosHandler implements Interceptor { + /** + * constant string being used + */ + private static final String RETRY_AFTER = "Retry-After"; + /** + * Denominator for the failure rate (i.e. 1/X) + */ + private static final int failureRate = 3; + /** + * default value to return on retry after + */ + private static final String retryAfterValue = "10"; + /** + * body to respond on failed requests + */ + private static final String responseBody = "{\"error\": {\"code\": \"TooManyRequests\",\"innerError\": {\"code\": \"429\",\"date\": \"2020-08-18T12:51:51\",\"message\": \"Please retry after\",\"request-id\": \"94fb3b52-452a-4535-a601-69e0a90e3aa2\",\"status\": \"429\"},\"message\": \"Please retry again later.\"}}"; + /** + * Too many requests status code + */ + public static final int MSClientErrorCodeTooManyRequests = 429; + + @Override + @Nonnull + public Response intercept(@Nonnull final Chain chain) throws IOException { + Request request = chain.request(); + + final int dice = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE); + + if(dice % failureRate == 0) { + return new Response + .Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(MSClientErrorCodeTooManyRequests) + .message("Too Many Requests") + .addHeader(RETRY_AFTER, retryAfterValue) + .body(ResponseBody.create(responseBody, MediaType.get("application/json"))) + .build(); + } else { + return chain.proceed(request); + } + } + +} diff --git a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/RedirectHandler.java b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/RedirectHandler.java new file mode 100644 index 0000000000..091bb366da --- /dev/null +++ b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/RedirectHandler.java @@ -0,0 +1,136 @@ +package com.microsoft.kiota.http.middleware; + +import static java.net.HttpURLConnection.HTTP_MOVED_PERM; +import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; +import static java.net.HttpURLConnection.HTTP_SEE_OTHER; +import static okhttp3.internal.http.StatusLine.HTTP_PERM_REDIRECT; +import static okhttp3.internal.http.StatusLine.HTTP_TEMP_REDIRECT; + +import java.io.IOException; +import java.net.ProtocolException; + +import javax.annotation.Nullable; +import javax.annotation.Nonnull; + +import com.microsoft.kiota.http.middleware.options.RedirectHandlerOption; + +import okhttp3.HttpUrl; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * Middleware that determines whether a redirect information should be followed or not, and follows it if necessary. + */ +public class RedirectHandler implements Interceptor{ + + private RedirectHandlerOption mRedirectOption; + + /** + * Initialize using default redirect options, default IShouldRedirect and max redirect value + */ + public RedirectHandler() { + this(null); + } + + /** + * Initialize using custom redirect options. + * @param redirectOption pass instance of redirect options to be used + */ + public RedirectHandler(@Nullable final RedirectHandlerOption redirectOption) { + this.mRedirectOption = redirectOption; + if(redirectOption == null) { + this.mRedirectOption = new RedirectHandlerOption(); + } + } + + boolean isRedirected(Request request, Response response, int redirectCount, RedirectHandlerOption redirectOption) throws IOException { + // Check max count of redirects reached + if(redirectCount > redirectOption.maxRedirects()) return false; + + // Location header empty then don't redirect + final String locationHeader = response.header("location"); + if(locationHeader == null) + return false; + + // If any of 301,302,303,307,308 then redirect + final int statusCode = response.code(); + if(statusCode == HTTP_PERM_REDIRECT || //308 + statusCode == HTTP_MOVED_PERM || //301 + statusCode == HTTP_TEMP_REDIRECT || //307 + statusCode == HTTP_SEE_OTHER || //303 + statusCode == HTTP_MOVED_TEMP) //302 + return true; + + return false; + } + + Request getRedirect( + final Request request, + final Response userResponse) throws ProtocolException { + String location = userResponse.header("Location"); + if (location == null || location.length() == 0) return null; + + // For relative URL in location header, the new url to redirect is relative to original request + if(location.startsWith("/")) { + if(request.url().toString().endsWith("/")) { + location = location.substring(1); + } + location = request.url() + location; + } + + HttpUrl requestUrl = userResponse.request().url(); + + HttpUrl locationUrl = userResponse.request().url().resolve(location); + + // Don't follow redirects to unsupported protocols. + if (locationUrl == null) return null; + + // Most redirects don't include a request body. + Request.Builder requestBuilder = userResponse.request().newBuilder(); + + // When redirecting across hosts, drop all authentication headers. This + // is potentially annoying to the application layer since they have no + // way to retain them. + boolean sameScheme = locationUrl.scheme().equalsIgnoreCase(requestUrl.scheme()); + boolean sameHost = locationUrl.host().toString().equalsIgnoreCase(requestUrl.host().toString()); + if (!sameScheme || !sameHost) { + requestBuilder.removeHeader("Authorization"); + } + + // Response status code 303 See Other then POST changes to GET + if(userResponse.code() == HTTP_SEE_OTHER) { + requestBuilder.method("GET", null); + } + + return requestBuilder.url(locationUrl).build(); + } + + // Intercept request and response made to network + @Override + @Nonnull + public Response intercept(@Nonnull final Chain chain) throws IOException { + Request request = chain.request(); + Response response = null; + int requestsCount = 1; + + // Use should retry pass along with this request + RedirectHandlerOption redirectOption = request.tag(RedirectHandlerOption.class); + redirectOption = redirectOption != null ? redirectOption : this.mRedirectOption; + + while(true) { + response = chain.proceed(request); + final boolean shouldRedirect = isRedirected(request, response, requestsCount, redirectOption) + && redirectOption.shouldRedirect().shouldRedirect(response); + if(!shouldRedirect) break; + + final Request followup = getRedirect(request, response); + if(followup != null) { + response.close(); + request = followup; + requestsCount++; + } + } + return response; + } +} diff --git a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/RetryHandler.java b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/RetryHandler.java new file mode 100644 index 0000000000..607f41d744 --- /dev/null +++ b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/RetryHandler.java @@ -0,0 +1,212 @@ +package com.microsoft.kiota.http.middleware; + +import java.io.IOException; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; + +import javax.annotation.Nullable; +import javax.annotation.Nonnull; + +import com.microsoft.kiota.http.middleware.options.IShouldRetry; +import com.microsoft.kiota.http.middleware.options.RetryHandlerOption; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * The middleware responsible for retrying requests when they fail because of transient issues + */ +public class RetryHandler implements Interceptor{ + + private RetryHandlerOption mRetryOption; + + /** + * Header name to track the retry attempt number + */ + private static final String RETRY_ATTEMPT_HEADER = "Retry-Attempt"; + /** + * Header name for the retry after information + */ + private static final String RETRY_AFTER = "Retry-After"; + + /** + * Too many requests status code + */ + public static final int MSClientErrorCodeTooManyRequests = 429; + /** + * Service unavailable status code + */ + public static final int MSClientErrorCodeServiceUnavailable = 503; + /** + * Gateway timeout status code + */ + public static final int MSClientErrorCodeGatewayTimeout = 504; + + /** + * One second as milliseconds + */ + private static final long DELAY_MILLISECONDS = 1000; + + + /** + * @param retryOption Create Retry handler using retry option + */ + public RetryHandler(@Nullable final RetryHandlerOption retryOption) { + this.mRetryOption = retryOption; + if(this.mRetryOption == null) { + this.mRetryOption = new RetryHandlerOption(); + } + } + /** + * Initialize retry handler with default retry option + */ + public RetryHandler() { + this(null); + } + + boolean retryRequest(Response response, int executionCount, Request request, RetryHandlerOption retryOption) { + + // Should retry option + // Use should retry common for all requests + IShouldRetry shouldRetryCallback = null; + if(retryOption != null) { + shouldRetryCallback = retryOption.shouldRetry(); + } + + boolean shouldRetry = false; + // Status codes 429 503 504 + int statusCode = response.code(); + // Only requests with payloads that are buffered/rewindable are supported. + // Payloads with forward only streams will be have the responses returned + // without any retry attempt. + shouldRetry = + retryOption != null + && executionCount <= retryOption.maxRetries() + && checkStatus(statusCode) && isBuffered(request) + && shouldRetryCallback != null + && shouldRetryCallback.shouldRetry(retryOption.delay(), executionCount, request, response); + + if(shouldRetry) { + long retryInterval = getRetryAfter(response, retryOption.delay(), executionCount); + try { + Thread.sleep(retryInterval); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + return shouldRetry; + } + + /** + * Get retry after in milliseconds + * @param response Response + * @param delay Delay in seconds + * @param executionCount Execution count of retries + * @return Retry interval in milliseconds + */ + long getRetryAfter(Response response, long delay, int executionCount) { + String retryAfterHeader = response.header(RETRY_AFTER); + double retryDelay = -1; + if(retryAfterHeader != null) { + retryDelay = tryParseTimeHeader(retryAfterHeader); + if(retryDelay == -1) { + retryDelay = tryParseDateHeader(retryAfterHeader); + } + } else if( retryAfterHeader == null || retryDelay == -1) { + retryDelay = exponentialBackOffDelay(delay, executionCount); + } + return (long)Math.min(retryDelay, RetryHandlerOption.MAX_DELAY * DELAY_MILLISECONDS); + } + + double tryParseTimeHeader(String retryAfterHeader){ + double retryDelay = -1; + try { + retryDelay = Integer.parseInt(retryAfterHeader) * DELAY_MILLISECONDS; + } catch (NumberFormatException e) { + return retryDelay; + } + return retryDelay; + } + + double tryParseDateHeader(String retryAfterHeader){ + double retryDelay = -1; + try { + DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME; + Instant headerTime = Instant.from(formatter.parse(retryAfterHeader)); + Instant now = Instant.now(); + if(headerTime.isAfter(now)) { + retryDelay = ChronoUnit.MILLIS.between(now, headerTime); + } + } catch (DateTimeParseException e) { + return retryDelay; + } + return retryDelay; + } + + private double exponentialBackOffDelay(double delay, int executionCount) { + double retryDelay = RetryHandlerOption.DEFAULT_DELAY * DELAY_MILLISECONDS; + retryDelay = (double)((Math.pow(2.0, (double)executionCount)-1)*0.5); + retryDelay = (executionCount < 2 ? delay : retryDelay + delay) + (double)Math.random(); + retryDelay *= DELAY_MILLISECONDS; + return retryDelay; + } + + boolean checkStatus(int statusCode) { + return (statusCode == MSClientErrorCodeTooManyRequests || statusCode == MSClientErrorCodeServiceUnavailable + || statusCode == MSClientErrorCodeGatewayTimeout); + } + + boolean isBuffered(final Request request) { + final String methodName = request.method(); + + final boolean isHTTPMethodPutPatchOrPost = methodName.equalsIgnoreCase("POST") || + methodName.equalsIgnoreCase("PUT") || + methodName.equalsIgnoreCase("PATCH"); + + final RequestBody requestBody = request.body(); + if(isHTTPMethodPutPatchOrPost && requestBody != null) { + try { + return requestBody.contentLength() != -1L; + } catch (IOException ex) { + // expected + return false; + } + } + return true; + } + + public RetryHandlerOption getRetryOptions(){ + return this.mRetryOption; + } + + @Override + @Nonnull + public Response intercept(@Nonnull final Chain chain) throws IOException { + Request request = chain.request(); + Response response = chain.proceed(request); + + // Use should retry pass along with this request + RetryHandlerOption retryOption = request.tag(RetryHandlerOption.class); + if(retryOption == null) { retryOption = mRetryOption; } + + int executionCount = 1; + while(retryRequest(response, executionCount, request, retryOption)) { + request = request.newBuilder().addHeader(RETRY_ATTEMPT_HEADER, String.valueOf(executionCount)).build(); + executionCount++; + if(response != null) { + final ResponseBody body = response.body(); + if(body != null) + body.close(); + response.close(); + } + response = chain.proceed(request); + } + return response; + } + +} diff --git a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/TelemetryHandler.java b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/TelemetryHandler.java new file mode 100644 index 0000000000..5dc1c53620 --- /dev/null +++ b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/TelemetryHandler.java @@ -0,0 +1,62 @@ +package com.microsoft.kiota.http.middleware; + +import java.io.IOException; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.microsoft.kiota.http.middleware.options.TelemetryHandlerOption; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * TelemetryHandler implementation using OkHttp3 + */ +public class TelemetryHandler implements Interceptor{ + + private TelemetryHandlerOption _telemetryHandlerOption; + + /** + * TelemetryHandler no param constructor + */ + public TelemetryHandler() { + this(null); + } + + /** + * TelemetryHandler constructor with passed in options. + * @param telemetryHandlerOption The user specified telemetryHandlerOptions + */ + public TelemetryHandler(@Nullable TelemetryHandlerOption telemetryHandlerOption) { + if (telemetryHandlerOption == null) { + this._telemetryHandlerOption = new TelemetryHandlerOption(); + } + this._telemetryHandlerOption = telemetryHandlerOption; + } + + /** + * Send the HttpRequest after telemetryOptions are handled + * @param chain + * @return + * @throws IOException + */ + @Override + public Response intercept(@Nonnull Chain chain) throws IOException { + final Request request = chain.request(); + + TelemetryHandlerOption telemetryHandlerOption = request.tag(TelemetryHandlerOption.class); + if(telemetryHandlerOption == null) { telemetryHandlerOption = this._telemetryHandlerOption; } + + //Simply forward request if TelemetryConfigurator is set to null intentionally. + if(telemetryHandlerOption == null || telemetryHandlerOption.TelemetryConfigurator == null) { + return chain.proceed(request); + } + + //Use the TelemetryConfigurator set by the user to enrich the request as desired. + Request enrichedRequest = telemetryHandlerOption.TelemetryConfigurator.apply(request); + return chain.proceed(enrichedRequest); + } + +} diff --git a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/options/IShouldRedirect.java b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/options/IShouldRedirect.java new file mode 100644 index 0000000000..e078e676b2 --- /dev/null +++ b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/options/IShouldRedirect.java @@ -0,0 +1,17 @@ +package com.microsoft.kiota.http.middleware.options; + +import javax.annotation.Nonnull; + +import okhttp3.Response; + +/** + * Indicates whether a specific response redirect information should be followed + */ +public interface IShouldRedirect { + /** + * Determines whether to follow the redirect information + * @param response current response + * @return whether the handler should follow the redirect information + */ + boolean shouldRedirect(@Nonnull final Response response); +} diff --git a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/options/IShouldRetry.java b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/options/IShouldRetry.java new file mode 100644 index 0000000000..3770725ee2 --- /dev/null +++ b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/options/IShouldRetry.java @@ -0,0 +1,21 @@ +package com.microsoft.kiota.http.middleware.options; + +import okhttp3.Request; +import okhttp3.Response; + +import javax.annotation.Nonnull; + +/** + * Indicates whether a specific request should be retried + */ +public interface IShouldRetry { + /** + * Determines whether a specific request should be retried + * @param delay the delay to wait before retrying + * @param executionCount number of retry attempts + * @param request current request + * @param response current response + * @return whether the specific request should be retried by the handler + */ + boolean shouldRetry(long delay, int executionCount, @Nonnull final Request request, @Nonnull final Response response); +} diff --git a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/options/RedirectHandlerOption.java b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/options/RedirectHandlerOption.java new file mode 100644 index 0000000000..9ffccb26a8 --- /dev/null +++ b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/options/RedirectHandlerOption.java @@ -0,0 +1,69 @@ +package com.microsoft.kiota.http.middleware.options; + +import javax.annotation.Nullable; + +import com.microsoft.kiota.RequestOption; + +import javax.annotation.Nonnull; + +/** + * Options to be passed to the redirect middleware. + */ +public class RedirectHandlerOption implements RequestOption { + private int maxRedirects; + /** + * The default maximum number of redirects to follow + */ + public static final int DEFAULT_MAX_REDIRECTS = 5; + /** + * The absolute maxium number of redirects that can be followed + */ + public static final int MAX_REDIRECTS = 20; + + private IShouldRedirect shouldRedirect; + /** + * Default redirect evaluation, always follow redirect information. + */ + public static final IShouldRedirect DEFAULT_SHOULD_REDIRECT = response -> true; + + /** + * Create default instance of redirect options, with default values of max redirects and should redirect + */ + public RedirectHandlerOption() { + this(DEFAULT_MAX_REDIRECTS, DEFAULT_SHOULD_REDIRECT); + } + + /** + * @param maxRedirects Max redirects to occur + * @param shouldRedirect Should redirect callback called before every redirect + */ + public RedirectHandlerOption(int maxRedirects, @Nullable final IShouldRedirect shouldRedirect) { + if(maxRedirects < 0) + throw new IllegalArgumentException("Max redirects cannot be negative"); + if(maxRedirects > MAX_REDIRECTS) + throw new IllegalArgumentException("Max redirect cannot exceed " + MAX_REDIRECTS); + + this.maxRedirects = maxRedirects; + this.shouldRedirect = shouldRedirect != null ? shouldRedirect : DEFAULT_SHOULD_REDIRECT; + } + + /** + * @return max redirects + */ + public int maxRedirects() { + return this.maxRedirects; + } + + /** + * @return should redirect + */ + @Nonnull + public IShouldRedirect shouldRedirect() { + return this.shouldRedirect; + } + + @Override + public Class getType() { + return (Class) RedirectHandlerOption.class; + } +} diff --git a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/options/RetryHandlerOption.java b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/options/RetryHandlerOption.java new file mode 100644 index 0000000000..d44c3a6844 --- /dev/null +++ b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/options/RetryHandlerOption.java @@ -0,0 +1,102 @@ +package com.microsoft.kiota.http.middleware.options; + +import javax.annotation.Nullable; + +import com.microsoft.kiota.RequestOption; + +import okhttp3.Request; +import okhttp3.Response; + +import java.util.function.Function; + +import javax.annotation.Nonnull; + +/** + * The options to be passed to the retry middleware. + */ +public class RetryHandlerOption implements RequestOption{ + private IShouldRetry mShouldretry; + /** + * Default retry evaluation, always retry. + */ + public static final IShouldRetry DEFAULT_SHOULD_RETRY = (delay, executionCount, request, response) -> true; + + private int mMaxRetries; + /** + * Absolute maximum number of retries + */ + public static final int MAX_RETRIES = 10; + /** + * Default maximum number of retries + */ + public static final int DEFAULT_MAX_RETRIES = 3; + + /** + * Delay in seconds + */ + private long mDelay; + /** + * Default retry delay + */ + public static final long DEFAULT_DELAY = 3; + /** + * Absolute maximum retry delay + */ + public static final long MAX_DELAY = 180; + + /** + * Create default instance of retry options, with default values of delay, max retries and shouldRetry callback. + */ + public RetryHandlerOption(){ + this(DEFAULT_SHOULD_RETRY, DEFAULT_MAX_RETRIES, DEFAULT_DELAY); + } + + /** + * Create an instance with provided values + * @param shouldRetry Retry callback to be called before making a retry + * @param maxRetries Number of max retires for a request + * @param delay Delay in seconds between retries + */ + @SuppressWarnings("LambdaLast") + public RetryHandlerOption(@Nullable final IShouldRetry shouldRetry, int maxRetries, long delay) { + if(delay > MAX_DELAY) + throw new IllegalArgumentException("Delay cannot exceed " + MAX_DELAY); + if(delay < 0) + throw new IllegalArgumentException("Delay cannot be negative"); + if(maxRetries > MAX_RETRIES) + throw new IllegalArgumentException("Max retries cannot exceed " + MAX_RETRIES); + if(maxRetries < 0) + throw new IllegalArgumentException("Max retries cannot be negative"); + + this.mShouldretry = shouldRetry == null ? DEFAULT_SHOULD_RETRY : shouldRetry; + this.mMaxRetries = maxRetries; + this.mDelay = delay; + } + + /** + * @return should retry callback + */ + @Nonnull + public IShouldRetry shouldRetry() { + return mShouldretry; + } + + /** + * @return Number of max retries + */ + public int maxRetries() { + return mMaxRetries; + } + + /** + * @return Delay in seconds between retries + */ + public long delay() { + return mDelay; + } + + @Override + public Class getType() { + return (Class) RetryHandlerOption.class; + } +} \ No newline at end of file diff --git a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/options/TelemetryHandlerOption.java b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/options/TelemetryHandlerOption.java new file mode 100644 index 0000000000..2b8b93aa1c --- /dev/null +++ b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/middleware/options/TelemetryHandlerOption.java @@ -0,0 +1,23 @@ +package com.microsoft.kiota.http.middleware.options; + +import java.util.function.Function; + +import com.microsoft.kiota.RequestOption; + +import okhttp3.Request; + +/** + * TelemetryHandlerOption class + */ +public class TelemetryHandlerOption implements RequestOption { + + /** + * A delegate which can be called to configure the Request with desired telemetry values. + */ + public Function TelemetryConfigurator = (request) -> request; + + @Override + public Class getType() { + return (Class) TelemetryHandlerOption.class; + } +} diff --git a/http/java/okhttp/lib/src/test/java/com/microsoft/kiota/http/middleware/RedirectHandlerTests.java b/http/java/okhttp/lib/src/test/java/com/microsoft/kiota/http/middleware/RedirectHandlerTests.java new file mode 100644 index 0000000000..9472bbf06d --- /dev/null +++ b/http/java/okhttp/lib/src/test/java/com/microsoft/kiota/http/middleware/RedirectHandlerTests.java @@ -0,0 +1,282 @@ +package com.microsoft.kiota.http.middleware; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.ProtocolException; + +import com.microsoft.kiota.http.middleware.options.RedirectHandlerOption; + +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.internal.http.StatusLine; + +import org.junit.jupiter.api.Test; + +public class RedirectHandlerTests { + + String testurl = "https://graph.microsoft.com/v1.0"; + String differenthosturl = "https://graph.abc.com/v1.0/"; + String testmeurl = "https://graph.microsoft.com/v1.0/me/"; + + @Test + public void testIsRedirectedFailureByNoLocationHeader() throws IOException { + RedirectHandler redirectHandler = new RedirectHandler(); + Request httpget = new Request.Builder().url(testmeurl).build(); + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_MOVED_TEMP) + .message("Moved Temporarily") + .request(httpget) + .build(); + boolean isRedirected = redirectHandler.isRedirected(httpget, response, 0, new RedirectHandlerOption()); + assertTrue(!isRedirected); + } + + @Test + public void testIsRedirectedFailureByStatusCodeBadRequest() throws IOException { + RedirectHandler redirectHandler = new RedirectHandler(); + Request httpget = new Request.Builder().url(testmeurl).build(); + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_BAD_REQUEST) + .message( "Bad Request") + .addHeader("location", testmeurl) + .request(httpget) + .build(); + boolean isRedirected = redirectHandler.isRedirected(httpget, response, 0, new RedirectHandlerOption()); + assertTrue(!isRedirected); + } + + @Test + public void testIsRedirectedSuccessWithStatusCodeMovedTemporarily() throws IOException { + RedirectHandler redirectHandler = new RedirectHandler(); + Request httpget = new Request.Builder().url(testmeurl).build(); + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_MOVED_TEMP) + .message("Moved Temporarily") + .addHeader("location", testmeurl) + .request(httpget) + .build(); + boolean isRedirected = redirectHandler.isRedirected(httpget, response, 0, new RedirectHandlerOption()); + assertTrue(isRedirected); + } + + @Test + public void testIsRedirectedSuccessWithStatusCodeMovedPermanently() throws IOException { + RedirectHandler redirectHandler = new RedirectHandler(); + Request httpget = new Request.Builder().url(testmeurl).build(); + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_MOVED_PERM) + .message("Moved Permanently") + .addHeader("location", testmeurl) + .request(httpget) + .build(); + boolean isRedirected = redirectHandler.isRedirected(httpget, response, 0, new RedirectHandlerOption()); + assertTrue(isRedirected); + } + + @Test + public void testIsRedirectedSuccessWithStatusCodeTemporaryRedirect() throws IOException { + RedirectHandler redirectHandler = new RedirectHandler(); + Request httpget = new Request.Builder().url(testmeurl).build(); + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(StatusLine.HTTP_TEMP_REDIRECT) + .message("Temporary Redirect") + .addHeader("location", testmeurl) + .request(httpget) + .build(); + boolean isRedirected = redirectHandler.isRedirected(httpget, response,0,new RedirectHandlerOption()); + assertTrue(isRedirected); + } + + @Test + public void testIsRedirectedSuccessWithStatusCodeSeeOther() throws IOException { + RedirectHandler redirectHandler = new RedirectHandler(); + Request httpget = new Request.Builder().url(testmeurl).build(); + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_SEE_OTHER) + .message( "See Other") + .addHeader("location", testmeurl) + .request(httpget) + .build(); + boolean isRedirected = redirectHandler.isRedirected(httpget, response,0,new RedirectHandlerOption()); + assertTrue(isRedirected); + } + + @Test + public void testGetRedirectForGetMethod() { + RedirectHandler redirectHandler = new RedirectHandler(); + Request httpget = new Request.Builder().url(testurl).build(); + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_MOVED_TEMP) + .message("Moved Temporarily") + .addHeader("location", testmeurl) + .request(httpget) + .build(); + try { + Request request = redirectHandler.getRedirect(httpget, response); + assertTrue(request != null); + final String method = request.method(); + assertTrue(method.equalsIgnoreCase("GET")); + } catch (ProtocolException e) { + e.printStackTrace(); + fail("Redirect handler testGetRedirectForGetMethod failure"); + } + } + + @Test + public void testGetRedirectForGetMethodForAuthHeader() { + RedirectHandler redirectHandler = new RedirectHandler(); + Request httpget = new Request.Builder().url(testurl).header("Authorization", "TOKEN").build(); + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_MOVED_TEMP) + .message("Moved Temporarily") + .addHeader("location", differenthosturl) + .request(httpget) + .build(); + + try { + Request request = redirectHandler.getRedirect(httpget, response); + assertTrue(request != null); + final String method = request.method(); + assertTrue(method.equalsIgnoreCase("GET")); + String header = request.header("Authorization"); + assertTrue(header == null); + } catch (ProtocolException e) { + e.printStackTrace(); + fail("Redirect handler testGetRedirectForGetMethodForAuthHeader failure"); + } + } + + @Test + public void testGetRedirectForHeadMethod() { + RedirectHandler redirectHandler = new RedirectHandler(); + Request httphead = new Request.Builder().url(testurl).method("HEAD", null).build(); + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_MOVED_TEMP) + .message("Moved Temporarily") + .addHeader("location", testmeurl) + .request(httphead) + .build(); + try { + Request request = redirectHandler.getRedirect(httphead, response); + assertTrue(request != null); + final String method = request.method(); + assertTrue(method.equalsIgnoreCase("HEAD")); + } catch (ProtocolException e) { + e.printStackTrace(); + fail("Redirect handler testGetRedirectForHeadMethod failure"); + } + } + + @Test + public void testGetRedirectForPostMethod() { + RedirectHandler redirectHandler = new RedirectHandler(); + RequestBody body = RequestBody.create("", MediaType.parse("application/json")); + Request httppost = new Request.Builder().url(testurl).post(body).build(); + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_MOVED_TEMP) + .message("Moved Temporarily") + .addHeader("location", testmeurl) + .request(httppost) + .build(); + try { + Request request = redirectHandler.getRedirect(httppost, response); + assertTrue(request != null); + final String method = request.method(); + assertTrue(method.equalsIgnoreCase("POST")); + } catch (ProtocolException e) { + e.printStackTrace(); + fail("Redirect handler testGetRedirectForPostMethod failure"); + } + } + + @Test + public void testGetRedirectForPostMethodWithStatusCodeSeeOther() { + RedirectHandler redirectHandler = new RedirectHandler(); + Request httppost = new Request.Builder().url(testurl).build(); + + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_SEE_OTHER) + .message("See Other") + .addHeader("location", testmeurl) + .request(httppost) + .build(); + + try { + Request request = redirectHandler.getRedirect(httppost, response); + assertTrue(request != null); + final String method = request.method(); + assertTrue(method.equalsIgnoreCase("GET")); + } catch (ProtocolException e) { + e.printStackTrace(); + fail("Redirect handler testGetRedirectForPostMethod1 failure"); + } + } + + @Test + public void testGetRedirectForRelativeURL() throws ProtocolException { + RedirectHandler redirectHandler = new RedirectHandler(); + Request httppost = new Request.Builder().url(testurl).build(); + + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_SEE_OTHER) + .message("See Other") + .addHeader("location", "/testrelativeurl") + .request(httppost) + .build(); + + Request request = redirectHandler.getRedirect(httppost, response); + assertTrue(request.url().toString().compareTo(testurl+"/testrelativeurl") == 0); + } + + @Test + public void testGetRedirectRelativeLocationRequestURLwithSlash() throws ProtocolException { + RedirectHandler redirectHandler = new RedirectHandler(); + Request httppost = new Request.Builder().url(testmeurl).build(); + + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_SEE_OTHER) + .message("See Other") + .addHeader("location", "/testrelativeurl") + .request(httppost) + .build(); + Request request = redirectHandler.getRedirect(httppost, response); + String expected = "https://graph.microsoft.com/v1.0/me/testrelativeurl"; + assertTrue(request.url().toString().compareTo(expected) == 0); + } + @Test + public void testIsRedirectedIsFalseIfExceedsMaxRedirects() throws ProtocolException, IOException { + RedirectHandlerOption options = new RedirectHandlerOption(0, null); + RedirectHandler redirectHandler = new RedirectHandler(options); + Request httppost = new Request.Builder().url(testmeurl).build(); + + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_SEE_OTHER) + .message("See Other") + .addHeader("location", "/testrelativeurl") + .request(httppost) + .build(); + boolean isRedirected = redirectHandler.isRedirected(httppost, response, 1, options); + assertFalse(isRedirected); + } +} diff --git a/http/java/okhttp/lib/src/test/java/com/microsoft/kiota/http/middleware/RetryHandlerTests.java b/http/java/okhttp/lib/src/test/java/com/microsoft/kiota/http/middleware/RetryHandlerTests.java new file mode 100644 index 0000000000..86b3a2f10b --- /dev/null +++ b/http/java/okhttp/lib/src/test/java/com/microsoft/kiota/http/middleware/RetryHandlerTests.java @@ -0,0 +1,243 @@ +package com.microsoft.kiota.http.middleware; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +import java.net.HttpURLConnection; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import com.microsoft.kiota.authentication.AuthenticationProvider; +import com.microsoft.kiota.http.OkHttpRequestAdapter; +import com.microsoft.kiota.http.middleware.options.IShouldRetry; +import com.microsoft.kiota.http.middleware.options.RetryHandlerOption; + +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okio.BufferedSink; + +public class RetryHandlerTests { + + @InjectMocks + public OkHttpRequestAdapter adapter = new OkHttpRequestAdapter(mock(AuthenticationProvider.class)); + + private final String RFC1123Pattern = "EEE, dd MMM yyyy HH:mm:ss z"; + SimpleDateFormat formatter = new SimpleDateFormat(RFC1123Pattern, Locale.ENGLISH); + + + @Test + public void RetryHandlerConstructorDefaults() { + RetryHandler retryHandler = new RetryHandler(); + RetryHandlerOption retryHandlerOption = new RetryHandlerOption(); + + assertEquals(retryHandler.getRetryOptions().delay(), retryHandlerOption.delay()); + assertEquals(retryHandler.getRetryOptions().maxRetries(), retryHandlerOption.maxRetries()); + assertEquals(retryHandler.getRetryOptions().shouldRetry(), retryHandlerOption.shouldRetry()); + } + + @Test + public void RetryHandlerWithRetryOptionConstructor() throws SecurityException, IllegalArgumentException { + RetryHandlerOption retryHandlerOption = new RetryHandlerOption(); + RetryHandler retryHandler = new RetryHandler(retryHandlerOption); + + Request request = new Request.Builder().url("https://graph.microsoft.com/v1.0/me").build(); + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_GATEWAY_TIMEOUT) + .message("Gateway Timeout") + .request(request).build(); + + assertTrue(retryHandler.retryRequest(response, 1, request, retryHandlerOption)); + } + + @Test + public void RetryHandlerWithCustomOptions() throws SecurityException, IllegalAccessException { + IShouldRetry shouldRetry = new IShouldRetry() { + public boolean shouldRetry(long delay, int executionCount, Request request, Response response){ + return false; + } + }; + + RetryHandlerOption retryHandlerOption = new RetryHandlerOption(shouldRetry, 5, 0); + RetryHandler retryHandler = new RetryHandler(retryHandlerOption); + + Request request = new Request.Builder().url("https://graph.microsoft.com/v1.0/me").build(); + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_GATEWAY_TIMEOUT) + .message("Gateway Timeout") + .request(request).build(); + + assertTrue(!retryHandler.retryRequest(response, 0, request, retryHandlerOption)); + } + + @Test + public void TestRetryWithMaxRetryAttemps() { + RetryHandler retryHandler = new RetryHandler(); + Request request = new Request.Builder().url("https://graph.microsoft.com/v1.0/me").build(); + + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_GATEWAY_TIMEOUT) + .message("Gateway Timeout") + .request(request) + .build(); + RetryHandlerOption retryHandlerOption = new RetryHandlerOption(); + int numberOfRetrys = RetryHandlerOption.DEFAULT_MAX_RETRIES + 1; + + assertFalse(retryHandler.retryRequest(response, numberOfRetrys, request, retryHandlerOption)); + } + + @Test + public void TestRetryWithUnacceptableStatusCode() { + RetryHandler retryHandler = new RetryHandler(); + + Request request = new Request.Builder().url("https://graph.microsoft.com/v1.0/me").build(); + //Response with code 500 should not trigger a retry + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(500) + .message("Internal Server Error") + .request(request) + .build(); + + assertFalse(retryHandler.retryRequest(response, 1, request, new RetryHandlerOption())); + } + + @Test + public void TestRetryWithTransferEncoding() { + RetryHandler retryHandler = new RetryHandler(); + + Request request = new Request.Builder().url("https://graph.microsoft.com/v1.0/me") + .post(RequestBody.create("TEST", MediaType.parse("application/json"))).build(); + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_GATEWAY_TIMEOUT) + .message("Gateway Timeout") + .request(request) + .addHeader("Transfer-Encoding", "chunked") + .build(); + + assertTrue(retryHandler.retryRequest(response, 1, request, new RetryHandlerOption())); + } + + @Test + public void TestRetryWithExponentialBackOff() { + RetryHandler retryHandler = new RetryHandler(); + + Request request = new Request.Builder().url("https://graph.microsoft.com/v1.0/me") + .post(RequestBody.create("TEST", MediaType.parse("application/json"))).build(); + Response response = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(HttpURLConnection.HTTP_GATEWAY_TIMEOUT) + .message("Gateway Timeout") + .request(request) + .addHeader("Transfer-Encoding", "chunked") + .build(); + + assertTrue(retryHandler.retryRequest(response, 1, request, new RetryHandlerOption())); + } + + @Test + public void TestGetRetryAfterUsingHeader() { + RetryHandler retryHandler = new RetryHandler(); + long delay = retryHandler.getRetryAfter(tooManyRequestResponse().newBuilder().addHeader("Retry-After", "60").build(), 1, 1); + assertTrue(delay == 60000); + delay = retryHandler.getRetryAfter(tooManyRequestResponse().newBuilder().addHeader("Retry-After", "1").build(), 2, 3); + assertTrue(delay == 1000); + + + formatter.setTimeZone(TimeZone.getTimeZone(ZoneId.of("GMT"))); + Instant futureTime = Instant.now().plus(25, ChronoUnit.SECONDS); //Make the retry after time 25 seconds from time of running test + String retryAfterString = formatter.format(new Date().from(futureTime)); + + delay = retryHandler.getRetryAfter(tooManyRequestResponse().newBuilder().addHeader("Retry-After", retryAfterString).build(), 2, 3); + assertTrue(delay > 23000); //Delay will not be exactly 25000 due to processing time, but should be close + assertTrue(delay != 180000); //Ensure it has not simply fallen back to default + + } + + @Test + public void TestGetRetryOnFirstExecution() { + RetryHandler retryHandler = new RetryHandler(); + long delay = retryHandler.getRetryAfter(tooManyRequestResponse(), 3, 1); + assertTrue(delay>3000); + delay = retryHandler.getRetryAfter(tooManyRequestResponse(), 3, 2); + assertTrue(delay>4000); + } + + @Test + public void TestGetRetryAfterMaxDelayExceeded() { + RetryHandler retryHandler = new RetryHandler(); + long delay = retryHandler.getRetryAfter(tooManyRequestResponse(), 190, 1); + assertTrue(delay == 180000); + + //Ensure fallback to max works with retry after header of date type + formatter.setTimeZone(TimeZone.getTimeZone(ZoneId.of("GMT"))); + Instant futureTime = Instant.now().plus(181, ChronoUnit.SECONDS); + String retryAfterString = formatter.format(new Date().from(futureTime)); + + delay = retryHandler.getRetryAfter(tooManyRequestResponse().newBuilder().addHeader("Retry-After", retryAfterString).build(), 2, 3); + assertTrue(delay == 180000); + + } + + @Test + public void testIsBuffered() { + final RetryHandler retryHandler = new RetryHandler(); + Request request = new Request.Builder().url("https://localhost").method("GET", null).build(); + assertTrue(retryHandler.isBuffered(request), "Get Request is buffered"); + + request = new Request.Builder().url("https://localhost").method("DELETE", null).build(); + assertTrue(retryHandler.isBuffered(request), "Delete Request is buffered"); + + request = new Request.Builder().url("https://localhost") + .method("POST", + RequestBody.create("{\"key\": 42 }", MediaType.parse("application/json"))) + .build(); + assertTrue(retryHandler.isBuffered(request), "Post Request is buffered"); + + request = new Request.Builder().url("https://localhost") + .method("POST", + new RequestBody() { + + @Override + public MediaType contentType() { + return MediaType.parse("application/octet-stream"); + } + + @Override + public void writeTo(BufferedSink sink) { + // TODO Auto-generated method stub + + } + }) + .build(); + assertFalse(retryHandler.isBuffered(request), "Post Stream Request is not buffered"); + } + + Response tooManyRequestResponse() { + return new Response.Builder() + .code(429) + .message("Too Many Requests") + .request(new Request.Builder().url("https://graph.microsoft.com/v1.0/me").build()) + .protocol(Protocol.HTTP_2) + .build(); + } + + + +} diff --git a/http/java/okhttp/lib/src/test/java/com/microsoft/kiota/http/middleware/TelemetryHandlerTests.java b/http/java/okhttp/lib/src/test/java/com/microsoft/kiota/http/middleware/TelemetryHandlerTests.java new file mode 100644 index 0000000000..7d1577f68c --- /dev/null +++ b/http/java/okhttp/lib/src/test/java/com/microsoft/kiota/http/middleware/TelemetryHandlerTests.java @@ -0,0 +1,86 @@ +package com.microsoft.kiota.http.middleware; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; + +import com.microsoft.kiota.HttpMethod; +import com.microsoft.kiota.RequestInformation; +import com.microsoft.kiota.authentication.AuthenticationProvider; +import com.microsoft.kiota.http.KiotaClientFactory; +import com.microsoft.kiota.http.OkHttpRequestAdapter; +import com.microsoft.kiota.http.middleware.options.RetryHandlerOption; +import com.microsoft.kiota.http.middleware.options.TelemetryHandlerOption; + +import okhttp3.*; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +public class TelemetryHandlerTests { + + @InjectMocks + public OkHttpRequestAdapter adapter = new OkHttpRequestAdapter(mock(AuthenticationProvider.class)); + + @Test + public void DefaultTelemetryHandlerDoesNotChangeRequest() throws IOException { + + TelemetryHandler telemetryHandler = new TelemetryHandler(); + Interceptor[] interceptorArr = {telemetryHandler}; + Request request = new Request.Builder().url("https://graph.microsoft.com/v1.0/users/").build(); + OkHttpClient.Builder builder = KiotaClientFactory.Create(interceptorArr); + Response response = builder.build().newCall(request).execute(); + + assertEquals(0, response.request().headers().size()); + } + + @Test + public void TelemetryHandlerSelectivelyEnrichesRequestsBasedOnRequestMiddleWare() throws IOException, URISyntaxException, NoSuchMethodException, + InvocationTargetException, IllegalAccessException { + Method method = OkHttpRequestAdapter.class.getDeclaredMethod("getRequestFromRequestInformation",RequestInformation.class); + method.setAccessible(true); + + RequestInformation requestInfo = new RequestInformation(); + requestInfo.httpMethod = HttpMethod.GET; + requestInfo.setUri(new URI("https://graph.microsoft.com/v1.0/users/")); + + //Configure telemetry at the request level + TelemetryHandlerOption telemetryHandlerOption = new TelemetryHandlerOption(); + telemetryHandlerOption.TelemetryConfigurator = (request) -> { + return request.newBuilder().addHeader("SdkVersion","x.x.x").build(); + }; + requestInfo.addRequestOptions(telemetryHandlerOption, new RetryHandlerOption()); + TelemetryHandler telemetryHandler = new TelemetryHandler(); + Interceptor[] interceptors = {telemetryHandler}; + + Request request = (Request) method.invoke(adapter, requestInfo); + OkHttpClient client = KiotaClientFactory.Create(interceptors).build(); + Response response = client.newCall(request).execute(); + + assertTrue(response.request().header("SdkVersion").contains("x.x.x")); + assertEquals(1, response.request().headers().size()); + } + + @Test + public void TelemetryHandlerGloballyEnrichesRequest() throws IOException { + //Configure telemetry at the handler level + TelemetryHandlerOption telemetryHandlerOption = new TelemetryHandlerOption(); + telemetryHandlerOption.TelemetryConfigurator = (request) -> { + return request.newBuilder().addHeader("SdkVersion", "x.x.x").build(); + }; + + TelemetryHandler telemetryHandler = new TelemetryHandler(telemetryHandlerOption); + Interceptor[] interceptorArr = {telemetryHandler}; + Request request = new Request.Builder().url("https://graph.microsoft.com/v1.0/users/").build(); + OkHttpClient client = KiotaClientFactory.Create(interceptorArr).build(); + Response response = client.newCall(request).execute(); + + assertTrue(response.request().header("SdkVersion").contains("x.x.x")); + assertEquals(1, response.request().headers().size()); + } +}