// ------------------------------------------------------------------------------
// 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;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
///
/// An implementation using standard .NET libraries.
///
public class HttpProvider : IHttpProvider
{
internal bool disposeHandler;
internal HttpClient httpClient;
internal HttpMessageHandler httpMessageHandler;
///
/// Constructs a new .
///
/// A serializer for serializing and deserializing JSON objects.
public HttpProvider(ISerializer serializer = null)
: this((HttpMessageHandler)null, true, serializer)
{
}
///
/// Constructs a new .
///
/// An HTTP client handler to pass to the for sending requests.
/// Whether or not to dispose the client handler on Dispose().
/// A serializer for serializing and deserializing JSON objects.
///
/// By default, HttpProvider disables automatic redirects and handles redirects to preserve authentication headers. If providing
/// an to the constructor and enabling automatic redirects this could cause issues with authentication
/// over the redirect.
///
public HttpProvider(HttpClientHandler httpClientHandler, bool disposeHandler, ISerializer serializer = null)
: this((HttpMessageHandler)httpClientHandler, disposeHandler, serializer)
{
}
///
/// Constructs a new .
///
/// An HTTP message handler to pass to the for sending requests.
/// Whether or not to dispose the client handler on Dispose().
/// A serializer for serializing and deserializing JSON objects.
public HttpProvider(HttpMessageHandler httpMessageHandler, bool disposeHandler, ISerializer serializer)
{
this.disposeHandler = disposeHandler;
this.httpMessageHandler = httpMessageHandler;
this.Serializer = serializer ?? new Serializer();
// NOTE: Override our pipeline when a httpMessageHandler is provided - httpMessageHandler can implement custom pipeline.
// This check won't be needed once we re-write the HttpProvider to work with GraphClientFactory.
if (this.httpMessageHandler == null)
{
this.httpMessageHandler = GraphClientFactory.GetNativePlatformHttpHandler();
this.httpClient = GraphClientFactory.Create(authenticationProvider: null, version: "v1.0", nationalCloud: GraphClientFactory.Global_Cloud, finalHandler: this.httpMessageHandler);
}
else
{
this.httpClient = new HttpClient(this.httpMessageHandler, this.disposeHandler);
}
this.httpClient.SetFeatureFlag(FeatureFlag.DefaultHttpProvider);
}
///
/// Gets or sets the cache control header for requests;
///
public CacheControlHeaderValue CacheControlHeader
{
get
{
return this.httpClient.DefaultRequestHeaders.CacheControl;
}
set
{
this.httpClient.DefaultRequestHeaders.CacheControl = value;
}
}
///
/// Gets or sets the overall request timeout.
///
public TimeSpan OverallTimeout
{
get
{
return this.httpClient.Timeout;
}
set
{
try
{
this.httpClient.Timeout = value;
}
catch (InvalidOperationException exception)
{
throw new ServiceException(
new Error
{
Code = ErrorConstants.Codes.NotAllowed,
Message = ErrorConstants.Messages.OverallTimeoutCannotBeSet,
},
exception);
}
}
}
///
/// Gets a serializer for serializing and deserializing JSON objects.
///
public ISerializer Serializer { get; private set; }
///
/// Disposes the HttpClient and HttpClientHandler instances.
///
public void Dispose()
{
if (this.httpClient != null)
{
this.httpClient.Dispose();
}
}
///
/// Sends the request.
///
/// The to send.
/// The .
public Task SendAsync(HttpRequestMessage request)
{
return this.SendAsync(request, HttpCompletionOption.ResponseContentRead, CancellationToken.None);
}
///
/// Sends the request.
///
/// The to send.
/// The to pass to the on send.
/// The for the request.
/// The .
public async Task SendAsync(
HttpRequestMessage request,
HttpCompletionOption completionOption,
CancellationToken cancellationToken)
{
var response = await this.SendRequestAsync(request, completionOption, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
using (response)
{
if (null != response.Content)
{
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
}
var errorResponse = await this.ConvertErrorResponseAsync(response).ConfigureAwait(false);
Error error = null;
if (errorResponse == null || errorResponse.Error == null)
{
if (response != null && response.StatusCode == HttpStatusCode.NotFound)
{
error = new Error { Code = ErrorConstants.Codes.ItemNotFound };
}
else
{
error = new Error
{
Code = ErrorConstants.Codes.GeneralException,
Message = ErrorConstants.Messages.UnexpectedExceptionResponse,
};
}
}
else
{
error = errorResponse.Error;
}
if (string.IsNullOrEmpty(error.ThrowSite))
{
IEnumerable throwsiteValues;
if (response.Headers.TryGetValues(CoreConstants.Headers.ThrowSiteHeaderName, out throwsiteValues))
{
error.ThrowSite = throwsiteValues.FirstOrDefault();
}
}
if (string.IsNullOrEmpty(error.ClientRequestId))
{
IEnumerable clientRequestId;
if (response.Headers.TryGetValues(CoreConstants.Headers.ClientRequestId, out clientRequestId))
{
error.ClientRequestId = clientRequestId.FirstOrDefault();
}
}
if (response.Content?.Headers.ContentType.MediaType == "application/json")
{
string rawResponseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ServiceException(error,
response.Headers,
response.StatusCode,
rawResponseBody);
}
else
{
// Pass through the response headers and status code to the ServiceException.
// System.Net.HttpStatusCode does not support RFC 6585, Additional HTTP Status Codes.
// Throttling status code 429 is in RFC 6586. The status code 429 will be passed through.
throw new ServiceException(error, response.Headers, response.StatusCode);
}
}
}
return response;
}
internal async Task SendRequestAsync(
HttpRequestMessage request,
HttpCompletionOption completionOption,
CancellationToken cancellationToken)
{
try
{
return await this.httpClient.SendAsync(request, completionOption, cancellationToken).ConfigureAwait(false);
}
catch (TaskCanceledException exception)
{
throw new ServiceException(
new Error
{
Code = ErrorConstants.Codes.Timeout,
Message = ErrorConstants.Messages.RequestTimedOut,
},
exception);
}
catch(ServiceException exception)
{
throw exception;
}
catch (Exception exception)
{
throw new ServiceException(
new Error
{
Code = ErrorConstants.Codes.GeneralException,
Message = ErrorConstants.Messages.UnexpectedExceptionOnSend,
},
exception);
}
}
///
/// Converts the into an object;
///
/// The to convert.
/// The object.
private async Task ConvertErrorResponseAsync(HttpResponseMessage response)
{
try
{
using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
return this.Serializer.DeserializeObject(responseStream);
}
}
catch (Exception)
{
// If there's an exception deserializing the error response return null and throw a generic
// ServiceException later.
return null;
}
}
}
}