Skip to content

Commit

Permalink
Rename TimeoutSec and add NetworkTimeoutSec
Browse files Browse the repository at this point in the history
Rename TimeoutSec and add NetworkTimeoutSec

TimeoutSec becomes ConnectTimeoutSec and has same behaviour as before.
NetworkTimeoutSec specifies the maximum allowed time between reads from the
network when streaming http response messages.

The default for both items is to have no timeout.

Co-Authored-By: CarloToso <105941898+CarloToso@users.noreply.github.com>
  • Loading branch information
stevenebutler and CarloToso committed Apr 24, 2023
1 parent 3547aca commit 73de5c3
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 94 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
Expand All @@ -23,17 +24,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 @@ -153,7 +156,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, perReadMillisecondsTimeout, cancellationToken);
Encoding = encoding;
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,12 @@ internal override void ProcessResponse(HttpResponseMessage response)
ArgumentNullException.ThrowIfNull(response);
ArgumentNullException.ThrowIfNull(_cancelToken);

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

if (ShouldWriteToPipeline)
{
using var responseStream = new BufferingStreamReader(baseResponseStream, _cancelToken.Token);
using var responseStream = new BufferingStreamReader(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 charSet = WebResponseHelper.GetCharacterSet(response);

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

object obj = null;
Exception ex = null;
Expand Down Expand Up @@ -137,12 +137,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 @@ -351,18 +351,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 @@ -397,7 +399,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 ConnectTimeOutSec 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 ConnectTimeoutSec { get; set; }

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

/// <summary>
/// Gets or sets the Headers property.
Expand Down Expand Up @@ -497,6 +511,8 @@ public virtual string CustomMethod

internal bool ShouldWriteToPipeline => !ShouldSaveToOutFile || PassThru;

internal TimeSpan NetworkTimeout => NetworkTimeoutSec > 0 ? TimeSpan.FromSeconds(NetworkTimeoutSec) : Timeout.InfiniteTimeSpan;

#endregion Helper Properties

#region Abstract Methods
Expand Down Expand Up @@ -570,7 +586,7 @@ protected override void ProcessRecord()
string respVerboseMsg = contentLength is null
? string.Format(CultureInfo.CurrentCulture, WebCmdletStrings.WebResponseNoSizeVerboseMsg, contentType)
: string.Format(CultureInfo.CurrentCulture, WebCmdletStrings.WebResponseVerboseMsg, contentLength, contentType);

WriteVerbose(respVerboseMsg);

bool _isSuccess = response.IsSuccessStatusCode;
Expand Down Expand Up @@ -621,12 +637,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 = NetworkTimeout;
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 @@ -666,7 +689,7 @@ protected override void ProcessRecord()

ThrowTerminatingError(er);
}
finally
finally
{
_cancelToken?.Dispose();
_cancelToken = null;
Expand Down Expand Up @@ -970,7 +993,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 +1043,7 @@ internal virtual void PrepareSession()
WebSession.RetryIntervalInSeconds = RetryIntervalSec;
}

WebSession.TimeoutSec = TimeoutSec;
WebSession.ConnectTimeoutSec = ConnectTimeoutSec;
}

internal virtual HttpClient GetHttpClient(bool handleRedirect)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,24 @@ public class WebResponseObject

#endregion Properties

#region Protected Fields

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

#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>
Expand All @@ -86,9 +96,11 @@ public WebResponseObject(HttpResponseMessage response, CancellationToken cancell
/// </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.perReadMillisecondsTimeout = perReadTimeout;
SetResponse(response, contentStream, cancellationToken);
InitializeContent();
InitializeRawContent(response);
Expand Down Expand Up @@ -151,7 +163,7 @@ private void SetResponse(HttpResponseMessage response, Stream contentStream, Can
}

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(), perReadMillisecondsTimeout, 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 @@ -5,6 +5,7 @@
using System.IO;
using System.Management.Automation;
using System.Net.Http;
using System.Threading;

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

TimeSpan perReadTimeout = NetworkTimeout;
Stream responseStream = StreamHelper.GetResponseStream(response, _cancelToken.Token);
if (ShouldWriteToPipeline)
{
Expand All @@ -43,8 +44,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 @@ -61,7 +63,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 73de5c3

Please sign in to comment.