Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -580,10 +580,7 @@ protected override void ProcessRecord()

// Check if the Resume range was not satisfiable because the file already completed downloading.
// This happens when the local file is the same size as the remote file.
if (Resume.IsPresent
&& response.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable
&& response.Content.Headers.ContentRange.HasLength
&& response.Content.Headers.ContentRange.Length == _resumeFileSize)
if (IsResumeRangeAlreadyComplete(response))
{
_isSuccess = true;
WriteVerbose(string.Format(
Expand Down Expand Up @@ -1354,10 +1351,7 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM
// 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))
if (ShouldRetryWithoutResumeRange(response))
{
_cancelToken.Cancel();

Expand Down Expand Up @@ -1736,6 +1730,31 @@ private void ProcessAuthentication()

private bool IsPersistentSession() => MyInvocation.BoundParameters.ContainsKey(nameof(WebSession)) || MyInvocation.BoundParameters.ContainsKey(nameof(SessionVariable));

private bool IsResumeRangeAlreadyComplete(HttpResponseMessage response)
{
if (!Resume.IsPresent || response.StatusCode != HttpStatusCode.RequestedRangeNotSatisfiable)
{
return false;
}

ContentRangeHeaderValue contentRange = response.Content.Headers.ContentRange;

// RFC 9110 only says 416 responses SHOULD include Content-Range. Treat a missing
// header as an already-complete resume instead of failing with a null reference.
return contentRange is null || (contentRange.HasLength && contentRange.Length == _resumeFileSize);
Comment on lines +1742 to +1744
}

private bool ShouldRetryWithoutResumeRange(HttpResponseMessage response)
{
if (!Resume.IsPresent || response.StatusCode != HttpStatusCode.RequestedRangeNotSatisfiable)
{
return false;
}

ContentRangeHeaderValue contentRange = response.Content.Headers.ContentRange;
return contentRange is not null && contentRange.HasLength && contentRange.Length != _resumeFileSize;
}

/// <summary>
/// Sets the ContentLength property of the request and writes the specified content to the request's RequestStream.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2213,6 +2213,23 @@ Describe "Invoke-WebRequest tests" -Tags "Feature", "RequireAdminOnWindows" {
$response.StatusCode | Should -Be 416
$response.Headers.'Content-Range'[0] | Should -BeExactly "bytes */$referenceFileSize"
}

It "Invoke-WebRequest -Resume treats 416 without Content-Range as a completed download" {
# Download the entire file
$uri = Get-WebListenerUrl -Test 'Resume' -TestValue 'MissingContentRange'
$null = Invoke-WebRequest -Uri $uri -OutFile $outFile
$fileSize = Get-Item $outFile | Select-Object -ExpandProperty Length

$response = Invoke-WebRequest -Uri $uri -OutFile $outFile -Resume -PassThru

$outFileHash = Get-FileHash -Algorithm SHA256 -Path $outFile
$outFileHash.Hash | Should -BeExactly $referenceFileHash.Hash
Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $referenceFileSize
$response.Headers.'X-WebListener-Has-Range'[0] | Should -BeExactly 'true'
$response.Headers.'X-WebListener-Request-Range'[0] | Should -BeExactly "bytes=$fileSize-"
$response.StatusCode | Should -Be 416
$response.Headers.ContainsKey('Content-Range') | Should -BeFalse
}
}

Context "Invoke-WebRequest retry tests" {
Expand Down Expand Up @@ -4244,6 +4261,22 @@ Describe "Invoke-RestMethod tests" -Tags "Feature", "RequireAdminOnWindows" {
$Headers.'X-WebListener-Request-Range'[0] | Should -BeExactly "bytes=$fileSize-"
$Headers.'Content-Range'[0] | Should -BeExactly "bytes */$referenceFileSize"
}

It "Invoke-RestMethod -Resume treats 416 without Content-Range as a completed download" {
# Download the entire file
$uri = Get-WebListenerUrl -Test 'Resume' -TestValue 'MissingContentRange'
$null = Invoke-RestMethod -Uri $uri -OutFile $outFile
$fileSize = Get-Item $outFile | Select-Object -ExpandProperty Length

Invoke-RestMethod -Uri $uri -OutFile $outFile -ResponseHeadersVariable 'Headers' -Resume

$outFileHash = Get-FileHash -Algorithm SHA256 -Path $outFile
$outFileHash.Hash | Should -BeExactly $referenceFileHash.Hash
Get-Item $outFile | Select-Object -ExpandProperty Length | Should -Be $referenceFileSize
$Headers.'X-WebListener-Has-Range'[0] | Should -BeExactly 'true'
$Headers.'X-WebListener-Request-Range'[0] | Should -BeExactly "bytes=$fileSize-"
$Headers.ContainsKey('Content-Range') | Should -BeFalse
}
}

Context "Invoke-RestMethod retry tests" {
Expand Down
16 changes: 16 additions & 0 deletions test/tools/WebListener/Controllers/ResumeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ public async void NoResume()
await Response.Body.WriteAsync(FileBytes, 0, FileBytes.Length);
}

public async void MissingContentRange()
{
SetResumeResponseHeaders();
string rangeHeader;
if (TryGetRangeHeader(out rangeHeader))
{
Response.StatusCode = StatusCodes.Status416RequestedRangeNotSatisfiable;
return;
}
Comment on lines +80 to +85

Response.ContentType = MediaTypeNames.Application.Octet;
Response.ContentLength = FileBytes.Length;
Response.StatusCode = StatusCodes.Status200OK;
await Response.Body.WriteAsync(FileBytes, 0, FileBytes.Length);
}

public async void Bytes(int NumberBytes)
{
if (NumberBytes > FileBytes.Length || NumberBytes < 0)
Expand Down