Skip to content

Commit

Permalink
WebCmdlets: Rename -TimeoutSec to -ConnectionTimeoutSeconds (with…
Browse files Browse the repository at this point in the history
… alias) and add `-OperationTimeoutSeconds` (#19558)
  • Loading branch information
stevenebutler committed Jun 19, 2023
1 parent 0cbcf7b commit d614858
Show file tree
Hide file tree
Showing 9 changed files with 374 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#nullable enable

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
Expand All @@ -26,17 +27,19 @@ public class BasicHtmlWebResponseObject : WebResponseObject
/// Initializes a new instance of the <see cref="BasicHtmlWebResponseObject"/> class.
/// </summary>
/// <param name="response">The response.</param>
/// <param name="perReadTimeout">Time permitted between reads or Timeout.InfiniteTimeSpan for no timeout.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public BasicHtmlWebResponseObject(HttpResponseMessage response, CancellationToken cancellationToken) : this(response, null, cancellationToken) { }
public BasicHtmlWebResponseObject(HttpResponseMessage response, TimeSpan perReadTimeout, CancellationToken cancellationToken) : this(response, null, perReadTimeout, cancellationToken) { }

/// <summary>
/// Initializes a new instance of the <see cref="BasicHtmlWebResponseObject"/> class
/// with the specified <paramref name="contentStream"/>.
/// </summary>
/// <param name="response">The response.</param>
/// <param name="contentStream">The content stream associated with the response.</param>
/// <param name="perReadTimeout">Time permitted between reads or Timeout.InfiniteTimeSpan for no timeout.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public BasicHtmlWebResponseObject(HttpResponseMessage response, Stream? contentStream, CancellationToken cancellationToken) : base(response, contentStream, cancellationToken)
public BasicHtmlWebResponseObject(HttpResponseMessage response, Stream? contentStream, TimeSpan perReadTimeout, CancellationToken cancellationToken) : base(response, contentStream, perReadTimeout, cancellationToken)
{
InitializeContent(cancellationToken);
InitializeRawContent(response);
Expand Down Expand Up @@ -157,7 +160,7 @@ protected void InitializeContent(CancellationToken cancellationToken)
// Fill the Content buffer
string? characterSet = WebResponseHelper.GetCharacterSet(BaseResponse);

Content = StreamHelper.DecodeStream(RawContentStream, characterSet, out Encoding encoding, cancellationToken);
Content = StreamHelper.DecodeStream(RawContentStream, characterSet, out Encoding encoding, perReadTimeout, cancellationToken);
Encoding = encoding;
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,12 @@ internal override void ProcessResponse(HttpResponseMessage response)
ArgumentNullException.ThrowIfNull(response);
ArgumentNullException.ThrowIfNull(_cancelToken);

TimeSpan perReadTimeout = ConvertTimeoutSecondsToTimeSpan(OperationTimeoutSeconds);
Stream baseResponseStream = StreamHelper.GetResponseStream(response, _cancelToken.Token);

if (ShouldWriteToPipeline)
{
using BufferingStreamReader responseStream = new(baseResponseStream, _cancelToken.Token);
using BufferingStreamReader responseStream = new(baseResponseStream, perReadTimeout, _cancelToken.Token);

// First see if it is an RSS / ATOM feed, in which case we can
// stream it - unless the user has overridden it with a return type of "XML"
Expand All @@ -96,8 +97,7 @@ internal override void ProcessResponse(HttpResponseMessage response)
{
// Try to get the response encoding from the ContentType header.
string? characterSet = WebResponseHelper.GetCharacterSet(response);

string str = StreamHelper.DecodeStream(responseStream, characterSet, out Encoding encoding, _cancelToken.Token);
string str = StreamHelper.DecodeStream(responseStream, characterSet, out Encoding encoding, perReadTimeout, _cancelToken.Token);

string encodingVerboseName;
try
Expand Down Expand Up @@ -139,12 +139,12 @@ internal override void ProcessResponse(HttpResponseMessage response)
}
}
else if (ShouldSaveToOutFile)
{
{
string outFilePath = WebResponseHelper.GetOutFilePath(response, _qualifiedOutFile);

WriteVerbose(string.Create(System.Globalization.CultureInfo.InvariantCulture, $"File Name: {Path.GetFileName(_qualifiedOutFile)}"));

StreamHelper.SaveStreamToFile(baseResponseStream, outFilePath, this, response.Content.Headers.ContentLength.GetValueOrDefault(), _cancelToken.Token);
StreamHelper.SaveStreamToFile(baseResponseStream, outFilePath, this, response.Content.Headers.ContentLength.GetValueOrDefault(), perReadTimeout, _cancelToken.Token);
}

if (!string.IsNullOrEmpty(StatusCodeVariable))
Expand Down Expand Up @@ -349,18 +349,20 @@ public enum RestReturnType

internal class BufferingStreamReader : Stream
{
internal BufferingStreamReader(Stream baseStream, CancellationToken cancellationToken)
internal BufferingStreamReader(Stream baseStream, TimeSpan perReadTimeout, CancellationToken cancellationToken)
{
_baseStream = baseStream;
_streamBuffer = new MemoryStream();
_length = long.MaxValue;
_copyBuffer = new byte[4096];
_perReadTimeout = perReadTimeout;
_cancellationToken = cancellationToken;
}

private readonly Stream _baseStream;
private readonly MemoryStream _streamBuffer;
private readonly byte[] _copyBuffer;
private readonly TimeSpan _perReadTimeout;
private readonly CancellationToken _cancellationToken;

public override bool CanRead => true;
Expand Down Expand Up @@ -395,7 +397,7 @@ public override int Read(byte[] buffer, int offset, int count)
// If we don't have enough data to fill this from memory, cache more.
// We try to read 4096 bytes from base stream every time, so at most we
// may cache 4095 bytes more than what is required by the Read operation.
int bytesRead = _baseStream.ReadAsync(_copyBuffer, 0, _copyBuffer.Length, _cancellationToken).GetAwaiter().GetResult();
int bytesRead = _baseStream.ReadAsync(_copyBuffer.AsMemory(), _perReadTimeout, _cancellationToken).GetAwaiter().GetResult();

if (_streamBuffer.Position < _streamBuffer.Length)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,25 @@ public abstract class WebRequestPSCmdlet : PSCmdlet, IDisposable
public virtual SwitchParameter DisableKeepAlive { get; set; }

/// <summary>
/// Gets or sets the TimeOut property.
/// Gets or sets the ConnectionTimeoutSeconds property.
/// </summary>
/// <remarks>
/// This property applies to sending the request and receiving the response headers only.
/// </remarks>
[Alias("TimeoutSec")]
[Parameter]
[ValidateRange(0, int.MaxValue)]
public virtual int TimeoutSec { get; set; }
public virtual int ConnectionTimeoutSeconds { get; set; }

/// <summary>
/// Gets or sets the OperationTimeoutSeconds property.
/// </summary>
/// <remarks>
/// This property applies to each read operation when receiving the response body.
/// </remarks>
[Parameter]
[ValidateRange(0, int.MaxValue)]
public virtual int OperationTimeoutSeconds { get; set; }

/// <summary>
/// Gets or sets the Headers property.
Expand Down Expand Up @@ -570,7 +584,7 @@ protected override void ProcessRecord()
string respVerboseMsg = contentLength is null
? string.Format(CultureInfo.CurrentCulture, WebCmdletStrings.WebResponseNoSizeVerboseMsg, response.Version, contentType)
: string.Format(CultureInfo.CurrentCulture, WebCmdletStrings.WebResponseVerboseMsg, response.Version, contentLength, contentType);

WriteVerbose(respVerboseMsg);

bool _isSuccess = response.IsSuccessStatusCode;
Expand Down Expand Up @@ -621,12 +635,19 @@ protected override void ProcessRecord()
string detailMsg = string.Empty;
try
{
string error = StreamHelper.GetResponseString(response, _cancelToken.Token);
// We can't use ReadAsStringAsync because it doesn't have per read timeouts
TimeSpan perReadTimeout = ConvertTimeoutSecondsToTimeSpan(OperationTimeoutSeconds);
string characterSet = WebResponseHelper.GetCharacterSet(response);
var responseStream = StreamHelper.GetResponseStream(response, _cancelToken.Token);
int initialCapacity = (int)Math.Min(contentLength ?? StreamHelper.DefaultReadBuffer, StreamHelper.DefaultReadBuffer);
var bufferedStream = new WebResponseContentMemoryStream(responseStream, initialCapacity, this, contentLength, perReadTimeout, _cancelToken.Token);
string error = StreamHelper.DecodeStream(bufferedStream, characterSet, out Encoding encoding, perReadTimeout, _cancelToken.Token);
detailMsg = FormatErrorMessage(error, contentType);
}
catch
catch (Exception ex)
{
// Catch all
er.ErrorDetails = new ErrorDetails(ex.ToString());
}

if (!string.IsNullOrEmpty(detailMsg))
Expand Down Expand Up @@ -656,6 +677,11 @@ protected override void ProcessRecord()
WriteError(er);
}
}
catch (TimeoutException ex)
{
ErrorRecord er = new(ex, "OperationTimeoutReached", ErrorCategory.OperationTimeout, null);
ThrowTerminatingError(er);
}
catch (HttpRequestException ex)
{
ErrorRecord er = new(ex, "WebCmdletWebResponseException", ErrorCategory.InvalidOperation, request);
Expand All @@ -666,7 +692,7 @@ protected override void ProcessRecord()

ThrowTerminatingError(er);
}
finally
finally
{
_cancelToken?.Dispose();
_cancelToken = null;
Expand Down Expand Up @@ -970,7 +996,7 @@ internal virtual void PrepareSession()
}
else
{
webProxy.UseDefaultCredentials = ProxyUseDefaultCredentials;
webProxy.UseDefaultCredentials = ProxyUseDefaultCredentials;
}

// We don't want to update the WebSession unless the proxies are different
Expand Down Expand Up @@ -1020,7 +1046,7 @@ internal virtual void PrepareSession()
WebSession.RetryIntervalInSeconds = RetryIntervalSec;
}

WebSession.TimeoutSec = TimeoutSec;
WebSession.ConnectionTimeout = ConvertTimeoutSecondsToTimeSpan(ConnectionTimeoutSeconds);
}

internal virtual HttpClient GetHttpClient(bool handleRedirect)
Expand Down Expand Up @@ -1263,8 +1289,24 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM
Uri currentUri = currentRequest.RequestUri;

_cancelToken = new CancellationTokenSource();
response = client.SendAsync(currentRequest, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult();
try
{
response = client.SendAsync(currentRequest, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult();
}
catch (TaskCanceledException ex)
{
if (ex.InnerException is TimeoutException)
{
// HTTP Request timed out
ErrorRecord er = new(ex, "ConnectionTimeoutReached", ErrorCategory.OperationTimeout, null);
ThrowTerminatingError(er);
}
else
{
throw;
}

}
if (handleRedirect
&& _maximumRedirection is not 0
&& IsRedirectCode(response.StatusCode)
Expand Down Expand Up @@ -1388,6 +1430,9 @@ internal virtual void UpdateSession(HttpResponseMessage response)
#endregion Virtual Methods

#region Helper Methods

internal static TimeSpan ConvertTimeoutSecondsToTimeSpan(int timeout) => timeout > 0 ? TimeSpan.FromSeconds(timeout) : Timeout.InfiniteTimeSpan;

private Uri PrepareUri(Uri uri)
{
uri = CheckProtocol(uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,24 +72,36 @@ public class WebResponseObject

#endregion Properties

#region Protected Fields

/// <summary>
/// Time permitted between reads or Timeout.InfiniteTimeSpan for no timeout.
/// </summary>
protected TimeSpan perReadTimeout;

#endregion Protected Fields

#region Constructors

/// <summary>
/// Initializes a new instance of the <see cref="WebResponseObject"/> class.
/// </summary>
/// <param name="response">The Http response.</param>
/// <param name="perReadTimeout">Time permitted between reads or Timeout.InfiniteTimeSpan for no timeout.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public WebResponseObject(HttpResponseMessage response, CancellationToken cancellationToken) : this(response, null, cancellationToken) { }
public WebResponseObject(HttpResponseMessage response, TimeSpan perReadTimeout, CancellationToken cancellationToken) : this(response, null, perReadTimeout, cancellationToken) { }

/// <summary>
/// Initializes a new instance of the <see cref="WebResponseObject"/> class
/// with the specified <paramref name="contentStream"/>.
/// </summary>
/// <param name="response">Http response.</param>
/// <param name="contentStream">The http content stream.</param>
/// <param name="perReadTimeout">Time permitted between reads or Timeout.InfiniteTimeSpan for no timeout.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public WebResponseObject(HttpResponseMessage response, Stream? contentStream, CancellationToken cancellationToken)
public WebResponseObject(HttpResponseMessage response, Stream? contentStream, TimeSpan perReadTimeout, CancellationToken cancellationToken)
{
this.perReadTimeout = perReadTimeout;
SetResponse(response, contentStream, cancellationToken);
InitializeContent();
InitializeRawContent(response);
Expand Down Expand Up @@ -149,7 +161,7 @@ private void SetResponse(HttpResponseMessage response, Stream? contentStream, Ca
}

int initialCapacity = (int)Math.Min(contentLength, StreamHelper.DefaultReadBuffer);
RawContentStream = new WebResponseContentMemoryStream(st, initialCapacity, cmdlet: null, response.Content.Headers.ContentLength.GetValueOrDefault(), cancellationToken);
RawContentStream = new WebResponseContentMemoryStream(st, initialCapacity, cmdlet: null, response.Content.Headers.ContentLength.GetValueOrDefault(), perReadTimeout, cancellationToken);
}

// Set the position of the content stream to the beginning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.IO;
using System.Management.Automation;
using System.Net.Http;
using System.Threading;

namespace Microsoft.PowerShell.Commands
{
Expand Down Expand Up @@ -35,7 +36,7 @@ public InvokeWebRequestCommand() : base()
internal override void ProcessResponse(HttpResponseMessage response)
{
ArgumentNullException.ThrowIfNull(response);

TimeSpan perReadTimeout = ConvertTimeoutSecondsToTimeSpan(OperationTimeoutSeconds);
Stream responseStream = StreamHelper.GetResponseStream(response, _cancelToken.Token);
if (ShouldWriteToPipeline)
{
Expand All @@ -45,8 +46,9 @@ internal override void ProcessResponse(HttpResponseMessage response)
StreamHelper.ChunkSize,
this,
response.Content.Headers.ContentLength.GetValueOrDefault(),
perReadTimeout,
_cancelToken.Token);
WebResponseObject ro = WebResponseHelper.IsText(response) ? new BasicHtmlWebResponseObject(response, responseStream, _cancelToken.Token) : new WebResponseObject(response, responseStream, _cancelToken.Token);
WebResponseObject ro = WebResponseHelper.IsText(response) ? new BasicHtmlWebResponseObject(response, responseStream, perReadTimeout, _cancelToken.Token) : new WebResponseObject(response, responseStream, perReadTimeout, _cancelToken.Token);
ro.RelationLink = _relationLink;
WriteObject(ro);

Expand All @@ -63,7 +65,7 @@ internal override void ProcessResponse(HttpResponseMessage response)

WriteVerbose(string.Create(System.Globalization.CultureInfo.InvariantCulture, $"File Name: {Path.GetFileName(_qualifiedOutFile)}"));

StreamHelper.SaveStreamToFile(responseStream, outFilePath, this, response.Content.Headers.ContentLength.GetValueOrDefault(), _cancelToken.Token);
StreamHelper.SaveStreamToFile(responseStream, outFilePath, this, response.Content.Headers.ContentLength.GetValueOrDefault(), perReadTimeout, _cancelToken.Token);
}
}

Expand Down
Loading

0 comments on commit d614858

Please sign in to comment.