| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | using Google.Apis.Core.Util; |
| | using Google.Apis.Logging; |
| | using Google.Apis.Testing; |
| | using System; |
| | using System.Collections.Generic; |
| | using System.Linq; |
| | using System.Net; |
| | using System.Net.Http; |
| | using System.Net.Http.Headers; |
| | using System.Text; |
| | using System.Threading; |
| | using System.Threading.Tasks; |
| |
|
| | namespace Google.Apis.Http |
| | { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | public class ConfigurableMessageHandler : DelegatingHandler |
| | { |
| | private const string QuotaProjectHeaderName = "x-goog-user-project"; |
| |
|
| | |
| | private static readonly ILogger Logger = ApplicationContext.Logger.ForType<ConfigurableMessageHandler>(); |
| |
|
| | |
| | [VisibleForTestOnly] |
| | public const int MaxAllowedNumTries = 20; |
| |
|
| | |
| | |
| | |
| | public const string UnsuccessfulResponseHandlerKey = "__UnsuccessfulResponseHandlerKey"; |
| |
|
| | |
| | |
| | |
| | public const string ExceptionHandlerKey = "__ExceptionHandlerKey"; |
| |
|
| | |
| | |
| | |
| | public const string ExecuteInterceptorKey = "__ExecuteInterceptorKey"; |
| |
|
| | |
| | |
| | |
| | public const string ResponseStreamInterceptorProviderKey = "__ResponseStreamInterceptorProviderKey"; |
| |
|
| | |
| | |
| | |
| | public const string CredentialKey = "__CredentialKey"; |
| |
|
| | |
| | |
| | |
| | internal const string UniverseDomainKey = "__UniverseDomainKey"; |
| |
|
| | |
| | |
| | |
| | public const string MaxRetriesKey = "__MaxRetriesKey"; |
| |
|
| | |
| | private static readonly string ApiVersion = Google.Apis.Util.Utilities.GetLibraryVersion(); |
| |
|
| | |
| | private static readonly string UserAgentSuffix = "google-api-dotnet-client/" + ApiVersion + " (gzip)"; |
| |
|
| | #region IHttpUnsuccessfulResponseHandler, IHttpExceptionHandler and IHttpExecuteInterceptor lists |
| |
|
| | #region Lock objects |
| |
|
| | |
| | |
| | private readonly object unsuccessfulResponseHandlersLock = new object(); |
| | private readonly object exceptionHandlersLock = new object(); |
| | private readonly object executeInterceptorsLock = new object(); |
| |
|
| | #endregion |
| |
|
| | |
| | private readonly IList<IHttpUnsuccessfulResponseHandler> unsuccessfulResponseHandlers = |
| | new List<IHttpUnsuccessfulResponseHandler>(); |
| |
|
| | |
| | private readonly IList<IHttpExceptionHandler> exceptionHandlers = |
| | new List<IHttpExceptionHandler>(); |
| |
|
| | |
| | private readonly IList<IHttpExecuteInterceptor> executeInterceptors = |
| | new List<IHttpExecuteInterceptor>(); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | [Obsolete("Use AddUnsuccessfulResponseHandler or RemoveUnsuccessfulResponseHandler instead.")] |
| | public IList<IHttpUnsuccessfulResponseHandler> UnsuccessfulResponseHandlers |
| | { |
| | get { return unsuccessfulResponseHandlers; } |
| | } |
| |
|
| | |
| | public void AddUnsuccessfulResponseHandler(IHttpUnsuccessfulResponseHandler handler) |
| | { |
| | lock (unsuccessfulResponseHandlersLock) |
| | { |
| | unsuccessfulResponseHandlers.Add(handler); |
| | } |
| | } |
| |
|
| | |
| | public void RemoveUnsuccessfulResponseHandler(IHttpUnsuccessfulResponseHandler handler) |
| | { |
| | lock (unsuccessfulResponseHandlersLock) |
| | { |
| | unsuccessfulResponseHandlers.Remove(handler); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | [Obsolete("Use AddExceptionHandler or RemoveExceptionHandler instead.")] |
| | public IList<IHttpExceptionHandler> ExceptionHandlers |
| | { |
| | get { return exceptionHandlers; } |
| | } |
| |
|
| | |
| | public void AddExceptionHandler(IHttpExceptionHandler handler) |
| | { |
| | lock (exceptionHandlersLock) |
| | { |
| | exceptionHandlers.Add(handler); |
| | } |
| | } |
| |
|
| | |
| | public void RemoveExceptionHandler(IHttpExceptionHandler handler) |
| | { |
| | lock (exceptionHandlersLock) |
| | { |
| | exceptionHandlers.Remove(handler); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | [Obsolete("Use AddExecuteInterceptor or RemoveExecuteInterceptor instead.")] |
| | public IList<IHttpExecuteInterceptor> ExecuteInterceptors |
| | { |
| | get { return executeInterceptors; } |
| | } |
| |
|
| | |
| | public void AddExecuteInterceptor(IHttpExecuteInterceptor interceptor) |
| | { |
| | lock (executeInterceptorsLock) |
| | { |
| | executeInterceptors.Add(interceptor); |
| | } |
| | } |
| |
|
| | |
| | public void RemoveExecuteInterceptor(IHttpExecuteInterceptor interceptor) |
| | { |
| | lock (executeInterceptorsLock) |
| | { |
| | executeInterceptors.Remove(interceptor); |
| | } |
| | } |
| |
|
| | #endregion |
| |
|
| | private int _loggingRequestId = 0; |
| |
|
| | private ILogger _instanceLogger = Logger; |
| |
|
| | |
| | |
| | |
| | |
| | internal ILogger InstanceLogger |
| | { |
| | get { return _instanceLogger; } |
| | set { _instanceLogger = value.ForType<ConfigurableMessageHandler>(); } |
| | } |
| |
|
| | |
| | private int numTries = 3; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | public int NumTries |
| | { |
| | get { return numTries; } |
| | set |
| | { |
| | if (value > MaxAllowedNumTries || value < 1) |
| | { |
| | throw new ArgumentOutOfRangeException("NumTries"); |
| | } |
| | numTries = value; |
| | } |
| | } |
| |
|
| | |
| | private int numRedirects = 10; |
| |
|
| | |
| | |
| | |
| | |
| | public int NumRedirects |
| | { |
| | get { return numRedirects; } |
| | set |
| | { |
| | if (value > MaxAllowedNumTries || value < 1) |
| | { |
| | throw new ArgumentOutOfRangeException("NumRedirects"); |
| | } |
| | numRedirects = value; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | public bool FollowRedirect { get; set; } |
| |
|
| | |
| | public bool IsLoggingEnabled { get; set; } |
| |
|
| | |
| | |
| | |
| | [Flags] |
| | public enum LogEventType |
| | { |
| | |
| | |
| | |
| | None = 0, |
| |
|
| | |
| | |
| | |
| | RequestUri = 1, |
| |
|
| | |
| | |
| | |
| | RequestHeaders = 2, |
| |
|
| | |
| | |
| | |
| | |
| | RequestBody = 4, |
| |
|
| | |
| | |
| | |
| | ResponseStatus = 8, |
| |
|
| | |
| | |
| | |
| | ResponseHeaders = 16, |
| |
|
| | |
| | |
| | |
| | |
| | ResponseBody = 32, |
| |
|
| | |
| | |
| | |
| | ResponseAbnormal = 64, |
| | } |
| |
|
| | |
| | |
| | |
| | public LogEventType LogEvents { get; set; } |
| |
|
| | |
| | public string ApplicationName { get; set; } |
| |
|
| | |
| | public string GoogleApiClientHeader { get; set; } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | public IHttpExecuteInterceptor Credential { get; set; } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | public string UniverseDomain { get; set; } |
| |
|
| | |
| | public ConfigurableMessageHandler(HttpMessageHandler httpMessageHandler) |
| | : base(httpMessageHandler) |
| | { |
| | |
| | FollowRedirect = true; |
| | IsLoggingEnabled = true; |
| | LogEvents = LogEventType.RequestUri | LogEventType.ResponseStatus | LogEventType.ResponseAbnormal; |
| | } |
| |
|
| | private void LogHeaders(string initialText, HttpHeaders headers1, HttpHeaders headers2) |
| | { |
| | var headers = (headers1 ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>()) |
| | .Concat(headers2 ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>()).ToList(); |
| | var args = new object[headers.Count * 2]; |
| | var fmt = new StringBuilder(headers.Count * 32); |
| | fmt.Append(initialText); |
| | var argBuilder = new StringBuilder(); |
| | for (int i = 0; i < headers.Count; i++) |
| | { |
| | fmt.Append($"\n [{{{i * 2}}}] '{{{1 + i * 2}}}'"); |
| | args[i * 2] = headers[i].Key; |
| | argBuilder.Clear(); |
| | args[1 + i * 2] = string.Join("; ", headers[i].Value); |
| | } |
| | InstanceLogger.Debug(fmt.ToString(), args); |
| | } |
| |
|
| | private async Task LogBody(string fmtText, HttpContent content) |
| | { |
| | |
| | var bodyBytes = content != null ? await content.ReadAsByteArrayAsync().ConfigureAwait(false) : new byte[0]; |
| | char[] bodyChars = new char[bodyBytes.Length]; |
| | for (int i = 0; i < bodyBytes.Length; i++) |
| | { |
| | var b = bodyBytes[i]; |
| | bodyChars[i] = b >= 32 && b <= 126 ? (char) b : '.'; |
| | } |
| | InstanceLogger.Debug(fmtText, new string(bodyChars)); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, |
| | CancellationToken cancellationToken) |
| | { |
| | var loggable = IsLoggingEnabled && InstanceLogger.IsDebugEnabled; |
| | string loggingRequestId = ""; |
| | if (loggable) |
| | { |
| | loggingRequestId = Interlocked.Increment(ref _loggingRequestId).ToString("X8"); |
| | } |
| |
|
| | int maxRetries = GetEffectiveMaxRetries(request); |
| | int triesRemaining = maxRetries; |
| | int redirectRemaining = NumRedirects; |
| |
|
| | Exception lastException = null; |
| |
|
| | |
| | var userAgent = (ApplicationName == null ? "" : ApplicationName + " ") + UserAgentSuffix; |
| | |
| | |
| | request.Headers.Add("User-Agent", userAgent); |
| | var apiClientHeader = GoogleApiClientHeader; |
| | if (apiClientHeader != null) |
| | { |
| | request.Headers.Add("x-goog-api-client", apiClientHeader); |
| | } |
| |
|
| | |
| | |
| | if (UniverseDomain is not null) |
| | { |
| | request.SetOption(UniverseDomainKey, UniverseDomain); |
| | } |
| |
|
| | HttpResponseMessage response = null; |
| | do |
| | { |
| | response?.Dispose(); |
| | response = null; |
| | lastException = null; |
| |
|
| | |
| | cancellationToken.ThrowIfCancellationRequested(); |
| |
|
| | |
| | List<IHttpExecuteInterceptor> interceptors; |
| | lock (executeInterceptorsLock) |
| | { |
| | interceptors = executeInterceptors.ToList(); |
| | } |
| | if (request.TryGetOption(ExecuteInterceptorKey, out List<IHttpExecuteInterceptor> perCallinterceptors)) |
| | { |
| | interceptors.AddRange(perCallinterceptors); |
| | } |
| |
|
| | |
| | foreach (var interceptor in interceptors) |
| | { |
| | await interceptor.InterceptAsync(request, cancellationToken).ConfigureAwait(false); |
| | } |
| |
|
| | |
| | |
| | CheckValidAfterInterceptors(request); |
| |
|
| | await CredentialInterceptAsync(request, cancellationToken).ConfigureAwait(false); |
| |
|
| | if (loggable) |
| | { |
| | if ((LogEvents & LogEventType.RequestUri) != 0) |
| | { |
| | InstanceLogger.Debug("Request[{0}] (triesRemaining={1}) URI: '{2}'", loggingRequestId, triesRemaining, request.RequestUri); |
| | } |
| | if ((LogEvents & LogEventType.RequestHeaders) != 0) |
| | { |
| | LogHeaders($"Request[{loggingRequestId}] Headers:", request.Headers, request.Content?.Headers); |
| | } |
| | if ((LogEvents & LogEventType.RequestBody) != 0) |
| | { |
| | await LogBody($"Request[{loggingRequestId}] Body: '{{0}}'", request.Content).ConfigureAwait(false); |
| | } |
| | } |
| | try |
| | { |
| | |
| | response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); |
| | } |
| | catch (Exception ex) |
| | { |
| | lastException = ex; |
| | } |
| |
|
| | |
| | if (response == null || ((int) response.StatusCode >= 400 || (int) response.StatusCode < 200)) |
| | { |
| | triesRemaining--; |
| | } |
| |
|
| | |
| | if (response == null) |
| | { |
| | var exceptionHandled = false; |
| |
|
| | |
| | List<IHttpExceptionHandler> handlers; |
| | lock (exceptionHandlersLock) |
| | { |
| | handlers = exceptionHandlers.ToList(); |
| | } |
| | if (request.TryGetOption(ExceptionHandlerKey, out List<IHttpExceptionHandler> perCallHandlers)) |
| | { |
| | handlers.AddRange(perCallHandlers); |
| | } |
| |
|
| | |
| | foreach (var handler in handlers) |
| | { |
| | exceptionHandled |= await handler.HandleExceptionAsync(new HandleExceptionArgs |
| | { |
| | Request = request, |
| | Exception = lastException, |
| | TotalTries = maxRetries, |
| | CurrentFailedTry = maxRetries - triesRemaining, |
| | CancellationToken = cancellationToken |
| | }).ConfigureAwait(false); |
| | } |
| |
|
| | if (!exceptionHandled) |
| | { |
| | InstanceLogger.Error(lastException, |
| | "Response[{0}] Exception was thrown while executing a HTTP request and it wasn't handled", loggingRequestId); |
| | throw lastException; |
| | } |
| | else if (loggable && (LogEvents & LogEventType.ResponseAbnormal) != 0) |
| | { |
| | InstanceLogger.Debug("Response[{0}] Exception {1} was thrown, but it was handled by an exception handler", |
| | loggingRequestId, lastException.Message); |
| | } |
| | } |
| | else |
| | { |
| | if (loggable) |
| | { |
| | if ((LogEvents & LogEventType.ResponseStatus) != 0) |
| | { |
| | InstanceLogger.Debug("Response[{0}] Response status: {1} '{2}'", loggingRequestId, response.StatusCode, response.ReasonPhrase); |
| | } |
| | if ((LogEvents & LogEventType.ResponseHeaders) != 0) |
| | { |
| | LogHeaders($"Response[{loggingRequestId}] Headers:", response.Headers, response.Content?.Headers); |
| | } |
| | if ((LogEvents & LogEventType.ResponseBody) != 0) |
| | { |
| | await LogBody($"Response[{loggingRequestId}] Body: '{{0}}'", response.Content).ConfigureAwait(false); |
| | } |
| | } |
| | if (response.IsSuccessStatusCode) |
| | { |
| | |
| | triesRemaining = 0; |
| | } |
| | else |
| | { |
| | bool errorHandled = false; |
| |
|
| | |
| | List<IHttpUnsuccessfulResponseHandler> handlers; |
| | lock (unsuccessfulResponseHandlersLock) |
| | { |
| | handlers = unsuccessfulResponseHandlers.ToList(); |
| | } |
| | if (request.TryGetOption(UnsuccessfulResponseHandlerKey, out List<IHttpUnsuccessfulResponseHandler> perCallHandlers)) |
| | { |
| | handlers.AddRange(perCallHandlers); |
| | } |
| |
|
| | var handlerArgs = new HandleUnsuccessfulResponseArgs |
| | { |
| | Request = request, |
| | Response = response, |
| | TotalTries = maxRetries, |
| | CurrentFailedTry = maxRetries - triesRemaining, |
| | CancellationToken = cancellationToken |
| | }; |
| |
|
| | |
| | foreach (var handler in handlers) |
| | { |
| | try |
| | { |
| | errorHandled |= await handler.HandleResponseAsync(handlerArgs).ConfigureAwait(false); |
| | } |
| | catch when (DisposeAndReturnFalse(response)) { } |
| |
|
| | bool DisposeAndReturnFalse(IDisposable disposable) |
| | { |
| | disposable.Dispose(); |
| | return false; |
| | } |
| | } |
| |
|
| | errorHandled |= await CredentialHandleResponseAsync(handlerArgs).ConfigureAwait(false); |
| |
|
| | if (!errorHandled) |
| | { |
| | if (FollowRedirect && HandleRedirect(response)) |
| | { |
| | if (redirectRemaining-- == 0) |
| | { |
| | triesRemaining = 0; |
| | } |
| |
|
| | errorHandled = true; |
| | if (loggable && (LogEvents & LogEventType.ResponseAbnormal) != 0) |
| | { |
| | InstanceLogger.Debug("Response[{0}] Redirect response was handled successfully. Redirect to {1}", |
| | loggingRequestId, response.Headers.Location); |
| | } |
| | } |
| | else |
| | { |
| | if (loggable && (LogEvents & LogEventType.ResponseAbnormal) != 0) |
| | { |
| | InstanceLogger.Debug("Response[{0}] An abnormal response wasn't handled. Status code is {1}", |
| | loggingRequestId, response.StatusCode); |
| | } |
| |
|
| | |
| | triesRemaining = 0; |
| | } |
| | } |
| | else if (loggable && (LogEvents & LogEventType.ResponseAbnormal) != 0) |
| | { |
| | InstanceLogger.Debug("Response[{0}] An abnormal response was handled by an unsuccessful response handler. " + |
| | "Status Code is {1}", loggingRequestId, response.StatusCode); |
| | } |
| | } |
| | } |
| | } while (triesRemaining > 0); |
| |
|
| | |
| | if (response == null) |
| | { |
| | InstanceLogger.Error(lastException, "Request[{0}] Exception was thrown while executing a HTTP request", loggingRequestId); |
| | throw lastException; |
| | } |
| | else if (!response.IsSuccessStatusCode && loggable && (LogEvents & LogEventType.ResponseAbnormal) != 0) |
| | { |
| | InstanceLogger.Debug("Response[{0}] Abnormal response is being returned. Status Code is {1}", loggingRequestId, response.StatusCode); |
| | } |
| |
|
| | return response; |
| | } |
| |
|
| | private void CheckValidAfterInterceptors(HttpRequestMessage request) |
| | { |
| | if (request.Headers.Contains(QuotaProjectHeaderName)) |
| | { |
| | throw new InvalidOperationException($"{QuotaProjectHeaderName} header can only be added through the credential or through the <Product>ClientBuilder."); |
| | } |
| | } |
| |
|
| | private async Task CredentialInterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken) |
| | { |
| | var effectiveCredential = GetEffectiveCredential(request); |
| | if (effectiveCredential != null) |
| | { |
| | await effectiveCredential.InterceptAsync(request, cancellationToken).ConfigureAwait(false); |
| | } |
| | } |
| |
|
| | private async Task<bool> CredentialHandleResponseAsync(HandleUnsuccessfulResponseArgs args) |
| | { |
| | var effectiveCredential = GetEffectiveCredential(args.Request); |
| | if (effectiveCredential is IHttpUnsuccessfulResponseHandler handler) |
| | { |
| | return await handler.HandleResponseAsync(args).ConfigureAwait(false); |
| | } |
| |
|
| | return false; |
| | } |
| |
|
| | private IHttpExecuteInterceptor GetEffectiveCredential(HttpRequestMessage request) => |
| | request.TryGetOption(CredentialKey, out IHttpExecuteInterceptor callCredential) ? callCredential : Credential; |
| |
|
| | private int GetEffectiveMaxRetries(HttpRequestMessage request) => |
| | request.TryGetOption(MaxRetriesKey, out int perRequestMaxRetries) ? perRequestMaxRetries : NumTries; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | private bool HandleRedirect(HttpResponseMessage message) |
| | { |
| | |
| | var uri = message.Headers.Location; |
| | if (!message.IsRedirectStatusCode() || uri == null) |
| | { |
| | return false; |
| | } |
| |
|
| | var request = message.RequestMessage; |
| | request.RequestUri = new Uri(request.RequestUri, uri); |
| | |
| | if (message.StatusCode == HttpStatusCode.SeeOther) |
| | { |
| | request.Method = HttpMethod.Get; |
| | } |
| | |
| | request.Headers.Remove("Authorization"); |
| | request.Headers.IfMatch.Clear(); |
| | request.Headers.IfNoneMatch.Clear(); |
| | request.Headers.IfModifiedSince = null; |
| | request.Headers.IfUnmodifiedSince = null; |
| | request.Headers.Remove("If-Range"); |
| | return true; |
| | } |
| | } |
| | } |
| |
|