Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
dec1116
[Feature] Added retry functionality for WebCmdlets
adityapatwardhan Dec 11, 2017
b277548
[Feature] Fixed comment
adityapatwardhan Dec 29, 2017
cb04980
Move retry logic to GetResponse
adityapatwardhan Jun 15, 2018
a27d25d
Move retry logic to GetResponse and address code review feedback
adityapatwardhan Jun 19, 2018
4aa1a4f
Add retry controller in WebListener
adityapatwardhan Jun 19, 2018
3a32e59
Fix bug in number of retries
adityapatwardhan Jun 19, 2018
8aa8109
Add response to retry controller
adityapatwardhan Jun 19, 2018
8597472
Fix retry loop bug
adityapatwardhan Jun 19, 2018
6e7df45
[Feature] Updated tests
adityapatwardhan Jun 19, 2018
2760e55
Address CodeFactor comments
adityapatwardhan Jun 20, 2018
c772bd6
[Feature] Address feedback about tests
adityapatwardhan Jun 20, 2018
5ffc956
Add WebListener documenation
adityapatwardhan Jun 20, 2018
fa066b9
[Feature] CodeFactor fixes
adityapatwardhan Jun 20, 2018
eb6946b
Add to spellings file
adityapatwardhan Jun 21, 2018
3bf800f
[Feature] Address code review feedback
adityapatwardhan Jun 25, 2018
7521fdd
[Feature] Added test for 'POST'
adityapatwardhan Jun 25, 2018
c4f3803
[Feature] Added sessionId echo for RetryController and updated test
adityapatwardhan Jun 26, 2018
23a8b3f
[Feature] Added tests for Invoke-RestMethod
adityapatwardhan Jun 26, 2018
cecc58a
[Feature] Moved tests in existing Describe blocks
adityapatwardhan Jun 27, 2018
661fcaa
[Feature] Fixed test title
adityapatwardhan Jun 27, 2018
e38edaa
Fix documentation
adityapatwardhan Jun 27, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .spelling
Original file line number Diff line number Diff line change
Expand Up @@ -1060,8 +1060,11 @@ v6.0.
#region test/tools/WebListener/README.md Overrides
- test/tools/WebListener/README.md
Auth
failureCode
failureCount
NoResume
NTLM
NumberBytes
ResponseHeaders
sessionId
#endregion
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Linq;
using System.Threading.Tasks;

namespace Microsoft.PowerShell.Commands
{
Expand Down Expand Up @@ -228,6 +229,20 @@ public virtual int MaximumRedirection
}
private int _maximumRedirection = -1;

/// <summary>
/// Gets or sets the MaximumRetryCount property, which determines the number of retries of a failed web request.
/// </summary>
[Parameter]
[ValidateRange(0, Int32.MaxValue)]
public virtual int MaximumRetryCount { get; set; } = 0;

/// <summary>
/// Gets or sets the RetryIntervalSec property, which determines the number seconds between retries.
/// </summary>
[Parameter]
[ValidateRange(1, Int32.MaxValue)]
public virtual int RetryIntervalSec { get; set; } = 5;

#endregion

#region Method
Expand Down Expand Up @@ -630,6 +645,14 @@ internal virtual void PrepareSession()
WebSession.Headers[key] = Headers[key].ToString();
}
}

if (MaximumRetryCount > 0)
{
WebSession.MaximumRetryCount = MaximumRetryCount;

// only set retry interval if retry count is set.
WebSession.RetryIntervalInSeconds = RetryIntervalSec;
}
}

#endregion Virtual Methods
Expand Down Expand Up @@ -662,7 +685,6 @@ internal bool ShouldResume
#endregion Helper Properties

#region Helper Methods

private Uri PrepareUri(Uri uri)
{
uri = CheckProtocol(uri);
Expand Down Expand Up @@ -1268,80 +1290,128 @@ static bool IsRedirectToGet(HttpStatusCode code)
);
}

private bool ShouldRetry(HttpStatusCode code)
{
int intCode = (int)code;

if (((intCode == 304) || (intCode >= 400 && intCode <= 599)) && WebSession.MaximumRetryCount > 0)
{
return true;
}

return false;
}

internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestMessage request, bool keepAuthorization)
{
if (client == null) { throw new ArgumentNullException("client"); }
if (request == null) { throw new ArgumentNullException("request"); }

// Track the current URI being used by various requests and re-requests.
var currentUri = request.RequestUri;
// Add 1 to account for the first request.
int totalRequests = WebSession.MaximumRetryCount + 1;
HttpRequestMessage req = request;
HttpResponseMessage response = null;

_cancelToken = new CancellationTokenSource();
HttpResponseMessage response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult();

if (keepAuthorization && IsRedirectCode(response.StatusCode) && response.Headers.Location != null)
do
{
_cancelToken.Cancel();
_cancelToken = null;
// Track the current URI being used by various requests and re-requests.
var currentUri = req.RequestUri;

// if explicit count was provided, reduce it for this redirection.
if (WebSession.MaximumRedirection > 0)
{
WebSession.MaximumRedirection--;
}
// For selected redirects that used POST, GET must be used with the
// redirected Location.
// Since GET is the default; POST only occurs when -Method POST is used.
if (Method == WebRequestMethod.Post && IsRedirectToGet(response.StatusCode))
_cancelToken = new CancellationTokenSource();
response = client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult();

if (keepAuthorization && IsRedirectCode(response.StatusCode) && response.Headers.Location != null)
{
// See https://msdn.microsoft.com/library/system.net.httpstatuscode(v=vs.110).aspx
Method = WebRequestMethod.Get;
_cancelToken.Cancel();
_cancelToken = null;

// if explicit count was provided, reduce it for this redirection.
if (WebSession.MaximumRedirection > 0)
{
WebSession.MaximumRedirection--;
}
// For selected redirects that used POST, GET must be used with the
// redirected Location.
// Since GET is the default; POST only occurs when -Method POST is used.
if (Method == WebRequestMethod.Post && IsRedirectToGet(response.StatusCode))
{
// See https://msdn.microsoft.com/library/system.net.httpstatuscode(v=vs.110).aspx
Method = WebRequestMethod.Get;
}

currentUri = new Uri(request.RequestUri, response.Headers.Location);
// Continue to handle redirection
using (client = GetHttpClient(handleRedirect: true))
using (HttpRequestMessage redirectRequest = GetRequest(currentUri))
{
response = GetResponse(client, redirectRequest, keepAuthorization);
}
}

currentUri = new Uri(request.RequestUri, response.Headers.Location);
// Continue to handle redirection
using (client = GetHttpClient(handleRedirect: true))
using (HttpRequestMessage redirectRequest = GetRequest(currentUri))
// Request again without the Range header because the server indicated the range was not satisfiable.
// This happens when the local file is larger than the remote file.
// If the size of the remote file is the same as the local file, there is nothing to resume.
if (Resume.IsPresent &&
response.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable &&
(response.Content.Headers.ContentRange.HasLength &&
response.Content.Headers.ContentRange.Length != _resumeFileSize))
{
response = GetResponse(client, redirectRequest, keepAuthorization);
}
}
_cancelToken.Cancel();

// Request again without the Range header because the server indicated the range was not satisfiable.
// This happens when the local file is larger than the remote file.
// If the size of the remote file is the same as the local file, there is nothing to resume.
if (Resume.IsPresent &&
response.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable &&
(response.Content.Headers.ContentRange.HasLength &&
response.Content.Headers.ContentRange.Length != _resumeFileSize))
{
_cancelToken.Cancel();
WriteVerbose(WebCmdletStrings.WebMethodResumeFailedVerboseMsg);

// Disable the Resume switch so the subsequent calls to GetResponse() and FillRequestStream()
// are treated as a standard -OutFile request. This also disables appending local file.
Resume = new SwitchParameter(false);

WriteVerbose(WebCmdletStrings.WebMethodResumeFailedVerboseMsg);
using (HttpRequestMessage requestWithoutRange = GetRequest(currentUri))
{
FillRequestStream(requestWithoutRange);
long requestContentLength = 0;
if (requestWithoutRange.Content != null)
{
requestContentLength = requestWithoutRange.Content.Headers.ContentLength.Value;
}

// Disable the Resume switch so the subsequent calls to GetResponse() and FillRequestStream()
// are treated as a standard -OutFile request. This also disables appending local file.
Resume = new SwitchParameter(false);
string reqVerboseMsg = String.Format(
CultureInfo.CurrentCulture,
WebCmdletStrings.WebMethodInvocationVerboseMsg,
requestWithoutRange.Method,
requestWithoutRange.RequestUri,
requestContentLength);
WriteVerbose(reqVerboseMsg);

using (HttpRequestMessage requestWithoutRange = GetRequest(currentUri))
return GetResponse(client, requestWithoutRange, keepAuthorization);
}
}

_resumeSuccess = response.StatusCode == HttpStatusCode.PartialContent;

// When MaximumRetryCount is not specified, the totalRequests == 1.
if (totalRequests > 1 && ShouldRetry(response.StatusCode))
{
FillRequestStream(requestWithoutRange);
long requestContentLength = 0;
if (requestWithoutRange.Content != null)
requestContentLength = requestWithoutRange.Content.Headers.ContentLength.Value;

string reqVerboseMsg = String.Format(CultureInfo.CurrentCulture,
WebCmdletStrings.WebMethodInvocationVerboseMsg,
requestWithoutRange.Method,
requestWithoutRange.RequestUri,
requestContentLength);
WriteVerbose(reqVerboseMsg);

return GetResponse(client, requestWithoutRange, keepAuthorization);
string retryMessage = string.Format(
CultureInfo.CurrentCulture,
WebCmdletStrings.RetryVerboseMsg,
RetryIntervalSec,
response.StatusCode);

WriteVerbose(retryMessage);

_cancelToken = new CancellationTokenSource();
Task.Delay(WebSession.RetryIntervalInSeconds * 1000, _cancelToken.Token).GetAwaiter().GetResult();
_cancelToken.Cancel();
_cancelToken = null;

req.Dispose();
req = GetRequest(currentUri);
FillRequestStream(req);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adityapatwardhan Can you either add a test for retry POST data or open an issue to have one added?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test for POST

}

totalRequests--;
}
while (totalRequests > 0 && !response.IsSuccessStatusCode);

_resumeSuccess = response.StatusCode == HttpStatusCode.PartialContent;
return response;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ public class WebRequestSession
/// </summary>
public int MaximumRedirection { get; set; }

/// <summary>
/// Gets or sets the count of retries for request failures.
/// </summary>
public int MaximumRetryCount { get; set; }

/// <summary>
/// Gets or sets the interval in seconds between retries.
/// </summary>
public int RetryIntervalInSeconds { get; set; }

/// <summary>
/// Construct a new instance of a WebRequestSession object.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,7 @@
<data name="WebResponseVerboseMsg" xml:space="preserve">
<value>received {0}-byte response of content type {1}</value>
</data>
<data name="RetryVerboseMsg" xml:space="preserve">
<value>Retrying after interval of {0} seconds. Status code for previous attempt: {1}</value>
</data>
</root>
2 changes: 1 addition & 1 deletion src/System.Management.Automation/engine/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ internal static bool IsValidPSEditionValue(string editionValue)
T policy = null;
#if !UNIX
// On Windows, group policy settings from registry take precedence.
// If the requested policy is not defined in registry, we query the configuration file.
// If the requested policy is not defined in registry, we query the configuration file.
policy = GetPolicySettingFromGPO<T>(preferenceOrder);
if (policy != null) { return policy; }
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1723,6 +1723,49 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" {
$response.Headers.'Content-Range'[0] | Should -BeExactly "bytes */$referenceFileSize"
}
}

Context "Invoke-WebRequest retry tests" {

It "Invoke-WebRequest can retry - <Name>" -TestCases @(
@{Name = "specified number of times - error 304"; failureCount = 2; failureCode = 304; retryCount = 2}
@{Name = "specified number of times - error 400"; failureCount = 3; failureCode = 400; retryCount = 3}
@{Name = "specified number of times - error 599"; failureCount = 1; failureCode = 599; retryCount = 2}
@{Name = "specified number of times - error 404"; failureCount = 2; failureCode = 404; retryCount = 2}
@{Name = "when retry count is higher than failure count"; failureCount = 2; failureCode = 404; retryCount = 4}
) {
param($failureCount, $retryCount, $failureCode)

$uri = Get-WebListenerUrl -Test 'Retry' -Query @{ sessionid = (New-Guid).Guid; failureCode = $failureCode; failureCount = $failureCount }
$commandStr = "Invoke-WebRequest -Uri '$uri' -MaximumRetryCount $retryCount -RetryIntervalSec 1"
$result = ExecuteWebCommand -command $commandStr

$result.output.StatusCode | Should -Be "200"
$jsonResult = $result.output.Content | ConvertFrom-Json
$jsonResult.failureResponsesSent | Should -Be $failureCount
}

It "Invoke-WebRequest should fail when failureCount is greater than MaximumRetryCount" {

$uri = Get-WebListenerUrl -Test 'Retry' -Query @{ sessionid = (New-Guid).Guid; failureCode = 400; failureCount = 4 }
$command = "Invoke-WebRequest -Uri '$uri' -MaximumRetryCount 1 -RetryIntervalSec 1"
$result = ExecuteWebCommand -command $command
$jsonError = $result.error | ConvertFrom-Json
$jsonError.error | Should -BeExactly 'Error: HTTP - 400 occurred.'
}

It "Invoke-WebRequest can retry with POST" {

$uri = Get-WebListenerUrl -Test 'Retry'
$sessionId = (New-Guid).Guid
$body = @{ sessionid = $sessionId; failureCode = 404; failureCount = 1 }
$commandStr = "Invoke-WebRequest -Uri '$uri' -MaximumRetryCount 2 -RetryIntervalSec 1 -Method POST -Body `$body"
$result = ExecuteWebCommand -command $commandStr

$result.output.StatusCode | Should -Be "200"
$jsonResult = $result.output.Content | ConvertFrom-Json
$jsonResult.SessionId | Should -BeExactly $sessionId
}
}
}

Describe "Invoke-RestMethod tests" -Tags "Feature" {
Expand Down Expand Up @@ -2969,6 +3012,33 @@ Describe "Invoke-RestMethod tests" -Tags "Feature" {
$Headers.'Content-Range'[0] | Should -BeExactly "bytes */$referenceFileSize"
}
}

Context "Invoke-RestMethod retry tests" {

It "Invoke-RestMethod can retry - specified number of times - error 304" {

$uri = Get-WebListenerUrl -Test 'Retry'
$sessionId = (New-Guid).Guid
$body = @{ sessionid = $sessionId; failureCode = 304; failureCount = 2 }
$commandStr = "Invoke-RestMethod -Uri '$uri' -MaximumRetryCount 2 -RetryIntervalSec 1 -Method POST -Body `$body"
$result = ExecuteWebCommand -command $commandStr

$result.output.failureResponsesSent | Should -Be 2
$result.output.sessionId | Should -BeExactly $sessionId
}

It "Invoke-RestMethod can retry with POST" {

$uri = Get-WebListenerUrl -Test 'Retry'
$sessionId = (New-Guid).Guid
$body = @{ sessionid = $sessionId; failureCode = 404; failureCount = 1 }
$commandStr = "Invoke-RestMethod -Uri '$uri' -MaximumRetryCount 2 -RetryIntervalSec 1 -Method POST -Body `$body"
$result = ExecuteWebCommand -command $commandStr

$result.output.failureResponsesSent | Should -Be 1
$result.output.sessionId | Should -BeExactly $sessionId
}
}
}

Describe "Validate Invoke-WebRequest and Invoke-RestMethod -InFile" -Tags "Feature" {
Expand Down Expand Up @@ -3077,3 +3147,4 @@ Describe "Web cmdlets tests using the cmdlet's aliases" -Tags "CI" {
$result.Hello | Should -Be "world"
}
}

1 change: 1 addition & 0 deletions test/tools/Modules/WebListener/WebListener.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ function Get-WebListenerUrl {
'Response',
'ResponseHeaders',
'Resume',
'Retry',
'/'
)]
[String]$Test,
Expand Down
Loading