Skip to content
Merged
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
30 changes: 30 additions & 0 deletions src/Microsoft.Graph.Core/Extensions/RequestExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------

namespace Microsoft.Graph
{
using System.Net.Http;
/// <summary>
/// Contains extension methods for <see cref="HttpRequestMessage"/>
/// </summary>
internal static class RequestExtensions
{
/// <summary>
/// Checks the HTTP request's content to determine if it's buffered or streamed content.
/// </summary>
/// <param name="httpRequestMessage">The <see cref="HttpRequestMessage"/>needs to be sent.</param>
/// <returns></returns>
internal static bool IsBuffered(this HttpRequestMessage httpRequestMessage)
{
HttpContent requestContent = httpRequestMessage.Content;

if ((httpRequestMessage.Method == HttpMethod.Put || httpRequestMessage.Method == HttpMethod.Post || httpRequestMessage.Method.Method.Equals("PATCH"))
&& requestContent != null && (requestContent.Headers.ContentLength == null || (int)requestContent.Headers.ContentLength == -1))
{
return false;
}
return true;
}
}
}
120 changes: 120 additions & 0 deletions src/Microsoft.Graph.Core/Requests/AuthenticationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------

namespace Microsoft.Graph
{
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Net;
/// <summary>
/// A <see cref="DelegatingHandler"/> implementation using standard .NET libraries.
/// </summary>
public class AuthenticationHandler: DelegatingHandler
{
/// <summary>
/// MaxRetry property for 401's
/// </summary>
public int MaxRetry { get; set; } = 1;

/// <summary>
/// AuthenticationProvider property
/// </summary>
public IAuthenticationProvider AuthenticationProvider { get; set; }

/// <summary>
/// Construct a new <see cref="AuthenticationHandler"/>
/// </summary>
public AuthenticationHandler()
{

}

/// <summary>
/// Construct a new <see cref="AuthenticationHandler"/>
/// <param name="authenticationProvider">An authentication provider to pass to <see cref="AuthenticationHandler"/> for authenticating requests.</param>
/// </summary>
public AuthenticationHandler(IAuthenticationProvider authenticationProvider)
{
AuthenticationProvider = authenticationProvider;
}

/// <summary>
/// Construct a new <see cref="AuthenticationHandler"/>
/// </summary>
/// <param name="authenticationProvider">An authentication provider to pass to <see cref="AuthenticationHandler"/> for authenticating requests.</param>
/// <param name="innerHandler">A HTTP message handler to pass to the <see cref="AuthenticationHandler"/> for sending requests.</param>
public AuthenticationHandler(IAuthenticationProvider authenticationProvider, HttpMessageHandler innerHandler)
{
InnerHandler = innerHandler;
AuthenticationProvider = authenticationProvider;
}

/// <summary>
/// Checks HTTP response message status code if it's unauthorized (401) or not
/// </summary>
/// <param name="httpResponseMessage">The <see cref="HttpResponseMessage"/>to send.</param>
/// <returns></returns>
private bool IsUnauthorized(HttpResponseMessage httpResponseMessage)
{
return httpResponseMessage.StatusCode == HttpStatusCode.Unauthorized;
}

/// <summary>
/// Retry sending HTTP request
/// </summary>
/// <param name="httpResponseMessage">The <see cref="HttpResponseMessage"/>to send.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>to send.</param>
/// <returns></returns>
private async Task<HttpResponseMessage> SendRetryAsync(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will need to consider the following behavior where an Authorization header is not expected per a workload:
https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/driveitem_createuploadsession#remarks
SendRetryAsync will cause an error if a Authorization header is provided in this scenario. Other than that, this PR looks good.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this won't be an issue as we stap the Authorization header only for graph.microsoft.com issues.

{
int retryAttempt = 0;
while (retryAttempt < MaxRetry)
{
var originalRequest = httpResponseMessage.RequestMessage;

// Authenticate request using AuthenticationProvider
await AuthenticationProvider.AuthenticateRequestAsync(originalRequest);
httpResponseMessage = await base.SendAsync(originalRequest, cancellationToken);

retryAttempt++;

if (!IsUnauthorized(httpResponseMessage) || !originalRequest.IsBuffered())
{
// Re-issue the request to get a new access token
return httpResponseMessage;
}
}

return httpResponseMessage;
}

/// <summary>
/// Sends a HTTP request and retries the request when the response is unauthorized.
/// This can happen when a token from the cache expires between graph getting the request and the backend receiving the request
/// </summary>
/// <param name="httpRequestMessage">The <see cref="HttpRequestMessage"/> to send.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the request.</param>
/// <returns></returns>
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken)
{
// Authenticate request using AuthenticationProvider
if (AuthenticationProvider != null)
{
await AuthenticationProvider.AuthenticateRequestAsync(httpRequestMessage);
}

HttpResponseMessage response = await base.SendAsync(httpRequestMessage, cancellationToken);

// Chcek if response is a 401 & is not a streamed body (is buffered)
if (IsUnauthorized(response) && httpRequestMessage.IsBuffered() && (AuthenticationProvider != null))
{
// re-issue the request to get a new access token
response = await SendRetryAsync(response, cancellationToken);
}

return response;
}
}
}
22 changes: 3 additions & 19 deletions src/Microsoft.Graph.Core/Requests/RetryHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Microsoft.Graph
using System.Net.Http;
using System.Net;
using System.Net.Http.Headers;

/// <summary>
/// An <see cref="DelegatingHandler"/> implementation using standard .NET libraries.
/// </summary>
Expand Down Expand Up @@ -54,7 +55,7 @@ protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage

var response = await base.SendAsync(httpRequest, cancellationToken);

if (IsRetry(response) && IsBuffered(httpRequest))
if (IsRetry(response) && httpRequest.IsBuffered())
{
response = await SendRetryAsync(response, cancellationToken);
}
Expand Down Expand Up @@ -94,7 +95,7 @@ public async Task<HttpResponseMessage> SendRetryAsync(HttpResponseMessage respon
// Call base.SendAsync to send the request
response = await base.SendAsync(request, cancellationToken);

if (!IsRetry(response) || !IsBuffered(request))
if (!IsRetry(response) || !request.IsBuffered())
{
return response;
}
Expand Down Expand Up @@ -126,23 +127,6 @@ public bool IsRetry(HttpResponseMessage response)
return false;
}

/// <summary>
/// Check the HTTP request's content to determine whether it can be retried or not.
/// </summary>
/// <param name="request">The <see cref="HttpRequestMessage"/>needs to be sent.</param>
/// <returns></returns>
private bool IsBuffered(HttpRequestMessage request)
{
HttpContent content = request.Content;

if ((request.Method == HttpMethod.Put || request.Method == HttpMethod.Post || request.Method.Method.Equals("PATCH"))
&& content != null && (content.Headers.ContentLength == null || (int)content.Headers.ContentLength == -1))
{
return false;
}
return true;

}

/// <summary>
/// Update Retry-Attempt header in the HTTP request
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------

namespace Microsoft.Graph.Core.Test.Extensions
{
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Net.Http;

[TestClass]
public class RequestExtensionsTests
{
[TestMethod]
public void IsBuffered_Get()
{
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, "http://example.com");
var response = httpRequest.IsBuffered();

Assert.IsTrue(response, "Unexpected content type");
}
[TestMethod]
public void IsBuffered_PostWithNoContent()
{
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Post, "http://example.com");
var response = httpRequest.IsBuffered();

Assert.IsTrue(response, "Unexpected content type");
}
[TestMethod]
public void IsBuffered_PostWithBufferStringContent()
{
byte[] data = new byte[] { 1, 2, 3, 4, 5 };
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Post, "http://example.com");
httpRequest.Content = new ByteArrayContent(data);
var response = httpRequest.IsBuffered();

Assert.IsTrue(response, "Unexpected content type");
}

[TestMethod]
public void IsBuffered_PutWithStreamStringContent()
{
var stringContent = new StringContent("Hello World");
var byteArrayContent = new ByteArrayContent(new byte[] { 1, 2, 3, 4, 5 });
var mutliformDataContent = new MultipartFormDataContent();
mutliformDataContent.Add(stringContent);
mutliformDataContent.Add(byteArrayContent);

HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Put, "http://example.com");
httpRequest.Content = mutliformDataContent;
httpRequest.Content.Headers.ContentLength = -1;
var response = httpRequest.IsBuffered();

Assert.IsFalse(response, "Unexpected content type");
}

[TestMethod]
public void IsBuffered_PatchWithStreamStringContent()
{
HttpRequestMessage httpRequest = new HttpRequestMessage(new HttpMethod("PATCH"), "http://example.com");
httpRequest.Content = new StringContent("Hello World");
httpRequest.Content.Headers.ContentLength = null;
var response = httpRequest.IsBuffered();

Assert.IsFalse(response, "Unexpected content type");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,14 @@
<Compile Include="Authentication\DelegateAuthenticationProviderTests.cs" />
<Compile Include="Exceptions\ErrorTests.cs" />
<Compile Include="Exceptions\ServiceExceptionTests.cs" />
<Compile Include="Extensions\RequestExtensionsTests.cs" />
<Compile Include="Helpers\ExtractSelectHelperTest.cs" />
<Compile Include="Helpers\StringHelperTests.cs" />
<Compile Include="Helpers\UrlHelperTests.cs" />
<Compile Include="Mocks\MockProgress.cs" />
<Compile Include="Mocks\MockRedirectHandler.cs" />
<Compile Include="Requests\AsyncMonitorTests.cs" />
<Compile Include="Requests\AuthenticationHandlerTests.cs" />
<Compile Include="Requests\BaseRequestBuilderTests.cs" />
<Compile Include="Requests\GraphClientFactoryTests.cs" />
<Compile Include="Requests\RedirectHandlerTests.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ public MockAuthenticationProvider()
{
this.SetupAllProperties();

this.Setup(provider => provider.AuthenticateRequestAsync(It.IsAny<HttpRequestMessage>())).Returns(Task.FromResult(0));
this.Setup(
provider => provider.AuthenticateRequestAsync(It.IsAny<HttpRequestMessage>()))
.Returns(Task.FromResult(0));
}
}
}
Loading