private async Task <HttpSourceResult> GetAsync( Uri uri, bool ignoreNotFounds, ILogger log, CancellationToken token) { Func <Task <HttpResponseMessage> > request = () => SendWithCredentialSupportAsync( () => new HttpRequestMessage(HttpMethod.Get, uri), log, token); var response = await request(); try { if (ignoreNotFounds && response.StatusCode == HttpStatusCode.NotFound) { response.Dispose(); return(new HttpSourceResult(HttpSourceResultStatus.NotFound)); } if (response.StatusCode == HttpStatusCode.NoContent) { response.Dispose(); // Ignore reading and caching the empty stream. return(new HttpSourceResult(HttpSourceResultStatus.NoContent)); } response.EnsureSuccessStatusCode(); var networkStream = await response.Content.ReadAsStreamAsync(); var timeoutStream = new DownloadTimeoutStream(uri.ToString(), networkStream, DownloadTimeout); return(new HttpSourceResult( HttpSourceResultStatus.OpenedFromNetwork, null, timeoutStream)); } catch { try { response.Dispose(); } catch { // Nothing we can do here. } throw; } }
internal async Task <XDocument> LoadXmlAsync( string uri, bool ignoreNotFounds, ILogger log, CancellationToken token) { return(await _httpSource.ProcessResponseAsync( () => { var request = new HttpRequestMessage(HttpMethod.Get, uri); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/atom+xml")); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); return request; }, async response => { if (response.StatusCode == HttpStatusCode.OK) { var networkStream = await response.Content.ReadAsStreamAsync(); var timeoutStream = new DownloadTimeoutStream(uri, networkStream, _httpSource.DownloadTimeout); return LoadXml(timeoutStream); } else if (ignoreNotFounds && response.StatusCode == HttpStatusCode.NotFound) { // Treat "404 Not Found" as an empty response. return null; } else if (response.StatusCode == HttpStatusCode.NoContent) { // Always treat "204 No Content" as exactly that. return null; } else { throw new FatalProtocolException(string.Format( CultureInfo.CurrentCulture, Strings.Log_FailedToFetchV2Feed, uri, (int)response.StatusCode, response.ReasonPhrase)); } }, log, token)); }
/// <summary> /// Make an HTTP request while retrying after failed attempts or timeouts. /// </summary> /// <remarks> /// This method accepts a factory to create instances of the <see cref="HttpRequestMessage"/> because /// requests cannot always be used. For example, suppose the request is a POST and contains content /// of a stream that can only be consumed once. /// </remarks> public async Task <HttpResponseMessage> SendAsync( HttpRetryHandlerRequest request, string source, ILogger log, CancellationToken cancellationToken) { if (source == null) { throw new ArgumentNullException(nameof(source)); } // If specified via environment, override the default retry delay with the values provided if (_enhancedHttpRetryHelper.IsEnabled) { request.RetryDelay = TimeSpan.FromMilliseconds(_enhancedHttpRetryHelper.DelayInMilliseconds); } var tries = 0; HttpResponseMessage response = null; var success = false; while (tries < request.MaxTries && !success) { // There are many places where another variable named "MaxTries" is set to 1, // so the Delay() never actually occurs. // When opted in to "enhanced retry", do the delay and have it increase exponentially where applicable // (i.e. when "tries" is allowed to be > 1) if (tries > 0 || (_enhancedHttpRetryHelper.IsEnabled && request.IsRetry)) { // "Enhanced" retry: In the case where this is actually a 2nd-Nth try, back off exponentially with some random. // In many cases due to the external retry loop, this will be always be 1 * request.RetryDelay.TotalMilliseconds + 0-200 ms if (_enhancedHttpRetryHelper.IsEnabled) { if (tries >= 3 || (tries == 0 && request.IsRetry)) { log.LogVerbose("Enhanced retry: HttpRetryHandler is in a state that retry would have been abandoned or not waited if it were not enabled."); } await Task.Delay(TimeSpan.FromMilliseconds((Math.Pow(2, tries) * request.RetryDelay.TotalMilliseconds) + new Random().Next(200)), cancellationToken); } // Old behavior; always delay a constant amount else { await Task.Delay(request.RetryDelay, cancellationToken); } } tries++; success = true; using (var requestMessage = request.RequestFactory()) { var stopwatches = new List <Stopwatch>(2); var bodyStopwatch = new Stopwatch(); stopwatches.Add(bodyStopwatch); Stopwatch headerStopwatch = null; if (request.CompletionOption == HttpCompletionOption.ResponseHeadersRead) { headerStopwatch = new Stopwatch(); stopwatches.Add(headerStopwatch); } #if NET5_0 requestMessage.Options.Set(new HttpRequestOptionsKey <List <Stopwatch> >(StopwatchPropertyName), stopwatches); #else requestMessage.Properties[StopwatchPropertyName] = stopwatches; #endif var requestUri = requestMessage.RequestUri; try { // The only time that we will be disposing this existing response is if we have // successfully fetched an HTTP response but the response has an status code indicating // failure (i.e. HTTP status code >= 500). // // If we don't even get an HTTP response message because an exception is thrown, then there // is no response instance to dispose. Additionally, we cannot use a finally here because // the caller needs the response instance returned in a non-disposed state. // // Also, remember that if an HTTP server continuously returns a failure status code (like // 500 Internal Server Error), we will retry some number of times but eventually return the // response as-is, expecting the caller to check the status code as well. This results in the // success variable being set to false but the response being returned to the caller without // disposing it. response?.Dispose(); // Add common headers to the request after it is created by the factory. This includes // X-NuGet-Session-Id which is added to all nuget requests. foreach (var header in request.AddHeaders) { requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value); } log.LogInformation(" " + string.Format( CultureInfo.InvariantCulture, Strings.Http_RequestLog, requestMessage.Method, requestUri)); // Issue the request. var timeoutMessage = string.Format( CultureInfo.CurrentCulture, Strings.Http_Timeout, requestMessage.Method, requestUri, (int)request.RequestTimeout.TotalMilliseconds); response = await TimeoutUtility.StartWithTimeout( async timeoutToken => { bodyStopwatch.Start(); headerStopwatch?.Start(); var responseMessage = await request.HttpClient.SendAsync(requestMessage, request.CompletionOption, timeoutToken); headerStopwatch?.Stop(); return(responseMessage); }, request.RequestTimeout, timeoutMessage, cancellationToken); // Wrap the response stream so that the download can timeout. if (response.Content != null) { var networkStream = await response.Content.ReadAsStreamAsync(); var timeoutStream = new DownloadTimeoutStream(requestUri.ToString(), networkStream, request.DownloadTimeout); var inProgressEvent = new ProtocolDiagnosticInProgressHttpEvent( source, requestUri, headerStopwatch?.Elapsed, (int)response.StatusCode, isRetry: request.IsRetry || tries > 1, isCancelled: false, isLastAttempt: tries == request.MaxTries && request.IsLastAttempt); var diagnosticsStream = new ProtocolDiagnosticsStream(timeoutStream, inProgressEvent, bodyStopwatch, ProtocolDiagnostics.RaiseEvent); var newContent = new StreamContent(diagnosticsStream); // Copy over the content headers since we are replacing the HttpContent instance associated // with the response message. foreach (var header in response.Content.Headers) { newContent.Headers.TryAddWithoutValidation(header.Key, header.Value); } response.Content = newContent; } log.LogInformation(" " + string.Format( CultureInfo.InvariantCulture, Strings.Http_ResponseLog, response.StatusCode, requestUri, bodyStopwatch.ElapsedMilliseconds)); if ((int)response.StatusCode >= 500) { success = false; } } catch (OperationCanceledException) { response?.Dispose(); ProtocolDiagnostics.RaiseEvent(new ProtocolDiagnosticHttpEvent( timestamp: DateTime.UtcNow, source, requestUri, headerDuration: null, eventDuration: bodyStopwatch.Elapsed, httpStatusCode: null, bytes: 0, isSuccess: false, isRetry: request.IsRetry || tries > 1, isCancelled: true, isLastAttempt: tries == request.MaxTries && request.IsLastAttempt)); throw; } catch (Exception e) { success = false; response?.Dispose(); ProtocolDiagnostics.RaiseEvent(new ProtocolDiagnosticHttpEvent( timestamp: DateTime.UtcNow, source, requestUri, headerDuration: null, eventDuration: bodyStopwatch.Elapsed, httpStatusCode: null, bytes: 0, isSuccess: false, isRetry: request.IsRetry || tries > 1, isCancelled: false, isLastAttempt: tries == request.MaxTries && request.IsLastAttempt)); if (tries >= request.MaxTries) { throw; } log.LogInformation(string.Format( CultureInfo.CurrentCulture, Strings.Log_RetryingHttp, requestMessage.Method, requestUri, requestMessage) + Environment.NewLine + ExceptionUtilities.DisplayMessage(e)); } } } return(response); }
/// <summary> /// Make an HTTP request while retrying after failed attempts or timeouts. /// </summary> /// <remarks> /// This method accepts a factory to create instances of the <see cref="HttpRequestMessage"/> because /// requests cannot always be used. For example, suppose the request is a POST and contains content /// of a stream that can only be consumed once. /// </remarks> public async Task <HttpResponseMessage> SendAsync( HttpRetryHandlerRequest request, string source, ILogger log, CancellationToken cancellationToken) { if (source == null) { throw new ArgumentNullException(nameof(source)); } var tries = 0; HttpResponseMessage response = null; var success = false; while (tries < request.MaxTries && !success) { if (tries > 0) { await Task.Delay(request.RetryDelay, cancellationToken); } tries++; success = true; using (var requestMessage = request.RequestFactory()) { var stopwatches = new List <Stopwatch>(2); var bodyStopwatch = new Stopwatch(); stopwatches.Add(bodyStopwatch); Stopwatch headerStopwatch = null; if (request.CompletionOption == HttpCompletionOption.ResponseHeadersRead) { headerStopwatch = new Stopwatch(); stopwatches.Add(headerStopwatch); } requestMessage.Properties[StopwatchPropertyName] = stopwatches; var requestUri = requestMessage.RequestUri; try { // The only time that we will be disposing this existing response is if we have // successfully fetched an HTTP response but the response has an status code indicating // failure (i.e. HTTP status code >= 500). // // If we don't even get an HTTP response message because an exception is thrown, then there // is no response instance to dispose. Additionally, we cannot use a finally here because // the caller needs the response instance returned in a non-disposed state. // // Also, remember that if an HTTP server continuously returns a failure status code (like // 500 Internal Server Error), we will retry some number of times but eventually return the // response as-is, expecting the caller to check the status code as well. This results in the // success variable being set to false but the response being returned to the caller without // disposing it. response?.Dispose(); // Add common headers to the request after it is created by the factory. This includes // X-NuGet-Session-Id which is added to all nuget requests. foreach (var header in request.AddHeaders) { requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value); } log.LogInformation(" " + string.Format( CultureInfo.InvariantCulture, Strings.Http_RequestLog, requestMessage.Method, requestUri)); // Issue the request. var timeoutMessage = string.Format( CultureInfo.CurrentCulture, Strings.Http_Timeout, requestMessage.Method, requestUri, (int)request.RequestTimeout.TotalMilliseconds); response = await TimeoutUtility.StartWithTimeout( async timeoutToken => { bodyStopwatch.Start(); headerStopwatch?.Start(); var responseMessage = await request.HttpClient.SendAsync(requestMessage, request.CompletionOption, timeoutToken); headerStopwatch?.Stop(); return(responseMessage); }, request.RequestTimeout, timeoutMessage, cancellationToken); // Wrap the response stream so that the download can timeout. if (response.Content != null) { var networkStream = await response.Content.ReadAsStreamAsync(); var timeoutStream = new DownloadTimeoutStream(requestUri.ToString(), networkStream, request.DownloadTimeout); var inProgressEvent = new ProtocolDiagnosticInProgressHttpEvent( source, requestUri, headerStopwatch?.Elapsed, (int)response.StatusCode, isRetry: request.IsRetry || tries > 1, isCancelled: false, isLastAttempt: tries == request.MaxTries && request.IsLastAttempt); var diagnosticsStream = new ProtocolDiagnosticsStream(timeoutStream, inProgressEvent, bodyStopwatch, ProtocolDiagnostics.RaiseEvent); var newContent = new StreamContent(diagnosticsStream); // Copy over the content headers since we are replacing the HttpContent instance associated // with the response message. foreach (var header in response.Content.Headers) { newContent.Headers.TryAddWithoutValidation(header.Key, header.Value); } response.Content = newContent; } log.LogInformation(" " + string.Format( CultureInfo.InvariantCulture, Strings.Http_ResponseLog, response.StatusCode, requestUri, bodyStopwatch.ElapsedMilliseconds)); if ((int)response.StatusCode >= 500) { success = false; } } catch (OperationCanceledException) { response?.Dispose(); ProtocolDiagnostics.RaiseEvent(new ProtocolDiagnosticHttpEvent( timestamp: DateTime.UtcNow, source, requestUri, headerDuration: null, eventDuration: bodyStopwatch.Elapsed, httpStatusCode: null, bytes: 0, isSuccess: false, isRetry: request.IsRetry || tries > 1, isCancelled: true, isLastAttempt: tries == request.MaxTries && request.IsLastAttempt)); throw; } catch (Exception e) { success = false; response?.Dispose(); ProtocolDiagnostics.RaiseEvent(new ProtocolDiagnosticHttpEvent( timestamp: DateTime.UtcNow, source, requestUri, headerDuration: null, eventDuration: bodyStopwatch.Elapsed, httpStatusCode: null, bytes: 0, isSuccess: false, isRetry: request.IsRetry || tries > 1, isCancelled: false, isLastAttempt: tries == request.MaxTries && request.IsLastAttempt)); if (tries >= request.MaxTries) { throw; } log.LogInformation(string.Format( CultureInfo.CurrentCulture, Strings.Log_RetryingHttp, requestMessage.Method, requestUri, requestMessage) + Environment.NewLine + ExceptionUtilities.DisplayMessage(e)); } } } return(response); }
private async Task CreateCacheFileAsync( HttpCacheResult result, string uri, HttpResponseMessage response, HttpSourceCacheContext context, Action <Stream> ensureValidContents, CancellationToken cancellationToken) { // The update of a cached file is divided into two steps: // 1) Delete the old file. // 2) Create a new file with the same name. using (var fileStream = new FileStream( result.NewCacheFile, FileMode.Create, FileAccess.ReadWrite, FileShare.None, BufferSize, useAsync: true)) { using (var networkStream = await response.Content.ReadAsStreamAsync()) using (var timeoutStream = new DownloadTimeoutStream(uri, networkStream, DownloadTimeout)) { await timeoutStream.CopyToAsync(fileStream, 8192, cancellationToken); } // Validate the content before putting it into the cache. fileStream.Seek(0, SeekOrigin.Begin); ensureValidContents?.Invoke(fileStream); } if (File.Exists(result.CacheFile)) { // Process B can perform deletion on an opened file if the file is opened by process A // with FileShare.Delete flag. However, the file won't be actually deleted until A close it. // This special feature can cause race condition, so we never delete an opened file. if (!IsFileAlreadyOpen(result.CacheFile)) { File.Delete(result.CacheFile); } } // If the destination file doesn't exist, we can safely perform moving operation. // Otherwise, moving operation will fail. if (!File.Exists(result.CacheFile)) { File.Move( result.NewCacheFile, result.CacheFile); } // Even the file deletion operation above succeeds but the file is not actually deleted, // we can still safely read it because it means that some other process just updated it // and we don't need to update it with the same content again. result.Stream = new FileStream( result.CacheFile, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete, BufferSize, useAsync: true); }