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
5 changes: 5 additions & 0 deletions src/code/FindHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,12 @@ private IEnumerable<PSResourceInfo> SearchByNames(ServerApiCall currentServer, R

responses = response.GetAwaiter().GetResult();

bool requestFailed = !errorMsgs.IsEmpty;
Utils.WriteOutConcurrentQueue(_cmdletPassedIn, errorMsgs, warningMsgs, debugMsgs, verboseMsgs);
if (requestFailed)
{
continue;
}
}
else
{
Expand Down
4 changes: 2 additions & 2 deletions src/code/PublishHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ private bool PushNupkg(string outputNupkgDir, string repoName, string repoUri, N
// look in PS repo for how httpRequestExceptions are handled

// Unfortunately there is no response message are no status codes provided with the exception and no
var ex = new ArgumentException(String.Format("Repository '{0}': {1}", repoName, e.Message));
var ex = new ArgumentException(String.Format("Repository '{0}': {1}", repoName, e.Message) + Utils.GetPSGalleryStatusMessage(publishLocation, e.Message));
if (e.Message.Contains("400"))
{
if (e.Message.Contains("Api"))
Expand Down Expand Up @@ -850,7 +850,7 @@ private bool PushNupkg(string outputNupkgDir, string repoName, string repoUri, N
else
{
error = new ErrorRecord(
new ArgumentException($"Repository '{repoName}': {e.InnerException.Message}"),
new ArgumentException($"Repository '{repoName}': {e.InnerException.Message}" + Utils.GetPSGalleryStatusMessage(publishLocation, e.InnerException.Message)),
"ProtocolFailError",
ErrorCategory.ProtocolError,
this);
Expand Down
55 changes: 55 additions & 0 deletions src/code/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,61 @@ public static string FormatRequestsExceptions(Exception exception, HttpRequestMe
return exMsg;
}

public const string PSGalleryStatusUri = "https://aka.ms/psgallery";

/// <summary>
/// Formats an HTTP request exception and, when the request targeted the PowerShell Gallery and failed
/// with a 5xx status code, appends a pointer to the PowerShell Gallery status page.
/// </summary>
public static string FormatRequestsExceptions(Exception exception, HttpRequestMessage request, HttpStatusCode statusCode)
{
return FormatRequestsExceptions(exception, request) + GetPSGalleryStatusMessage(request?.RequestUri, statusCode);
}

/// <summary>
/// Returns a sentence pointing to the PowerShell Gallery status page when the request targeted the
/// PowerShell Gallery and failed with a 5xx status code; otherwise returns an empty string.
/// </summary>
public static string GetPSGalleryStatusMessage(Uri requestUri, HttpStatusCode statusCode)
{
return IsPSGalleryServiceError(requestUri, statusCode)
? $" The PowerShell Gallery may be experiencing an issue. Check {PSGalleryStatusUri} for the current service status."
: string.Empty;
}

/// <summary>
/// Same as GetPSGalleryStatusMessage, for callers (such as NuGet's PushRunner) that only expose the
/// status code inside an exception message like "Response status code does not indicate success: 503 (...)".
/// </summary>
public static string GetPSGalleryStatusMessage(string requestUri, string exceptionMessage)
{
if (string.IsNullOrEmpty(exceptionMessage) || !Uri.TryCreate(requestUri, UriKind.Absolute, out Uri uri))
{
return string.Empty;
}

Match match = Regex.Match(exceptionMessage, @"status code does not indicate success: (\d{3})", RegexOptions.IgnoreCase);
return match.Success ? GetPSGalleryStatusMessage(uri, (HttpStatusCode)int.Parse(match.Groups[1].Value)) : string.Empty;
Comment on lines +259 to +260
}

/// <summary>
/// True when the request targeted the PowerShell Gallery and failed with a server-side (5xx) error.
/// 404 is deliberately excluded because it can legitimately mean the requested resource doesn't exist.
/// </summary>
public static bool IsPSGalleryServiceError(Uri requestUri, HttpStatusCode statusCode)
{
if (requestUri == null || !requestUri.IsAbsoluteUri)
{
return false;
}

string host = requestUri.Host;
bool isPSGallery = host.Equals("powershellgallery.com", StringComparison.OrdinalIgnoreCase) ||
host.EndsWith(".powershellgallery.com", StringComparison.OrdinalIgnoreCase);

return isPSGallery && (int)statusCode >= 500 && (int)statusCode < 600;
}

public static string FormatCredentialRequestExceptions(Exception exception)
{
string exMsg = $"'{exception.Message}' Re-run the command with -Credential.";
Expand Down
74 changes: 54 additions & 20 deletions src/code/V2ServerAPICalls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ public override async Task<FindResults> FindNameAsync(string packageName, bool i
}

var requestUrlV2 = $"{Repository.Uri}/FindPackagesById()?{queryBuilder.BuildQueryString()}";
string response = string.Empty;
string response = null;
try
{
response = await HttpRequestCallAsync(requestUrlV2, errorMsgs, warningMsgs, debugMsgs, verboseMsgs);
Expand Down Expand Up @@ -476,6 +476,12 @@ public override async Task<FindResults> FindNameAsync(string packageName, bool i
}
}

// The request failure has already been reported; don't also report a parse/not-found error.
if (response == null)
{
return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v2FindResponseType);
}

int count = GetCountFromResponse(response, out string errStr);
if (errStr != null)
{
Expand Down Expand Up @@ -832,7 +838,7 @@ public override async Task<FindResults> FindVersionAsync(string packageName, str
}

var requestUrlV2 = $"{Repository.Uri}/FindPackagesById()?{queryBuilder.BuildQueryString()}";
string response = string.Empty;
string response = null;
try
{
response = await HttpRequestCallAsync(requestUrlV2, errorMsgs, warningMsgs, debugMsgs, verboseMsgs);
Expand Down Expand Up @@ -874,6 +880,12 @@ public override async Task<FindResults> FindVersionAsync(string packageName, str
}
}

// The request failure has already been reported; don't also report a parse/not-found error.
if (response == null)
{
return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v2FindResponseType);
}

int count = GetCountFromResponse(response, out string errStr);
if (errStr != null)
{
Expand Down Expand Up @@ -1071,11 +1083,13 @@ private string HttpRequestCall(string requestUrlV2, out ErrorRecord errRecord)

/// <summary>
/// Helper method that makes the HTTP request for the V2 server protocol url passed in for find APIs.
/// Returns null if the request failed (the failure has already been added to errorMsgs), so callers
/// must not report an additional parse/not-found error for it.
/// </summary>
private async Task<string> HttpRequestCallAsync(string requestUrlV2, ConcurrentQueue<ErrorRecord> errorMsgs, ConcurrentQueue<string> warningMsgs, ConcurrentQueue<string> debugMsgs, ConcurrentQueue<string> verboseMsgs)
{
debugMsgs.Enqueue("In V2ServerAPICalls::HttpRequestCallAsync()");
string response = string.Empty;
string response = null;

try
{
Expand All @@ -1087,23 +1101,23 @@ private async Task<string> HttpRequestCallAsync(string requestUrlV2, ConcurrentQ
catch (ResourceNotFoundException e)
{
errorMsgs.Enqueue(new ErrorRecord(
new ResourceNotFoundException($"Failure when attempting to make request for '{requestUrlV2}'", e),
e,
"ResourceNotFound",
ErrorCategory.InvalidResult,
this));
}
catch (UnauthorizedException e)
{
errorMsgs.Enqueue(new ErrorRecord(
new UnauthorizedException($"Failure when attempting to make request for '{requestUrlV2}'", e),
new UnauthorizedException($"Failure when attempting to make request for '{requestUrlV2}'. {e.Message}", e),
"UnauthorizedRequest",
ErrorCategory.InvalidResult,
this));
}
catch (HttpRequestException e)
{
errorMsgs.Enqueue(new ErrorRecord(
new HttpRequestException($"Failure when attempting to make request for '{requestUrlV2}'", e),
e,
"HttpRequestCallFailure",
ErrorCategory.ConnectionError,
this));
Expand Down Expand Up @@ -1132,18 +1146,28 @@ private async Task<HttpContent> HttpRequestCallForContentAsync(string requestUrl
{
debugMsgs.Enqueue("In V2ServerAPICalls::HttpRequestCallForContentAsync()");
HttpContent content = null;
bool requestFailed = true;

try
{
debugMsgs.Enqueue($"Request url is '{requestUrlV2}'");
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrlV2);

content = await SendV2RequestForContentAsync(request, _sessionClient);
requestFailed = false;
}
catch (ResourceNotFoundException e)
{
errorMsgs.Enqueue(new ErrorRecord(
e,
"ResourceNotFound",
ErrorCategory.ObjectNotFound,
this));
}
catch (HttpRequestException e)
{
errorMsgs.Enqueue(new ErrorRecord(
new HttpRequestException($"Failure when attempting to make request for content with '{requestUrlV2}'", e),
e,
"HttpRequestFailure",
ErrorCategory.ConnectionError,
this));
Expand All @@ -1165,7 +1189,8 @@ private async Task<HttpContent> HttpRequestCallForContentAsync(string requestUrl
this));
}

if (content == null || string.IsNullOrEmpty(content.ToString()))
// Only report an empty response if the request itself didn't already report a failure.
if (!requestFailed && (content == null || string.IsNullOrEmpty(content.ToString())))
{
errorMsgs.Enqueue(new ErrorRecord(
new ArgumentException($"Response returned is null or empty. Request for content made: '{requestUrlV2}'"),
Expand All @@ -1190,6 +1215,14 @@ private HttpContent HttpRequestCallForContent(string requestUrlV2, out ErrorReco

content = SendV2RequestForContentAsync(request, _sessionClient).GetAwaiter().GetResult();
}
catch (ResourceNotFoundException e)
{
errRecord = new ErrorRecord(
exception: e,
"ResourceNotFound",
ErrorCategory.ObjectNotFound,
this);
}
catch (HttpRequestException e)
{
errRecord = new ErrorRecord(
Expand Down Expand Up @@ -1715,6 +1748,12 @@ public override async Task<FindResults> FindVersionGlobbingAsync(string packageN

var initialResponse = await FindVersionGlobbingAsync(packageName, versionRange, includePrerelease, type, skip, getOnlyLatest, errorMsgs, warningMsgs, debugMsgs, verboseMsgs);

// The request failure has already been reported; don't also report a parse/not-found error.
if (initialResponse == null)
{
return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v2FindResponseType);
}

int initialCount = GetCountFromResponse(initialResponse, out string errStr);
if (errStr != null)
{
Expand Down Expand Up @@ -1743,7 +1782,7 @@ public override async Task<FindResults> FindVersionGlobbingAsync(string packageN
// skip 100
skip += 100;
var tmpResponse = await FindVersionGlobbingAsync(packageName, versionRange, includePrerelease, type, skip, getOnlyLatest, errorMsgs, warningMsgs, debugMsgs, verboseMsgs);
responses.Add(tmpResponse);
responses.Add(tmpResponse ?? string.Empty);
count--;
}
}
Expand Down Expand Up @@ -1924,14 +1963,9 @@ private async Task<Stream> InstallVersionAsync(string packageName, string versio

var response = await HttpRequestCallForContentAsync(requestUrlV2, errorMsgs, warningMsgs, debugMsgs, verboseMsgs);

// HttpRequestCallForContentAsync has already reported why no content was returned.
if (response is null)
{
errorMsgs.Enqueue(new ErrorRecord(
new Exception($"No content was returned by repository '{Repository.Name}'"),
"InstallFailureContentNullv2Async",
ErrorCategory.InvalidResult,
this));

return new MemoryStream();
}

Expand Down Expand Up @@ -2027,7 +2061,7 @@ public static async Task<string> SendV2RequestAsync(HttpRequestMessage message,
{
if (responseStatusCode.Equals(HttpStatusCode.NotFound))
{
throw new ResourceNotFoundException(Utils.FormatRequestsExceptions(e, message));
throw new ResourceNotFoundException(Utils.FormatRequestsExceptions(e, message, responseStatusCode));
}
// ADO feed will return a 401 if a package does not exist on the feed, with the following message:
// 401 (Unauthorized - No local versions of package 'NonExistentModule'; please provide authentication to access
Expand All @@ -2036,13 +2070,13 @@ public static async Task<string> SendV2RequestAsync(HttpRequestMessage message,
{
if (e.Message.Contains("access versions from upstream that have not yet been saved to your feed"))
{
throw new ResourceNotFoundException(Utils.FormatRequestsExceptions(e, message));
throw new ResourceNotFoundException(Utils.FormatRequestsExceptions(e, message, responseStatusCode));
}

throw new UnauthorizedException(Utils.FormatCredentialRequestExceptions(e));
}

throw new HttpRequestException(Utils.FormatRequestsExceptions(e, message));
throw new HttpRequestException(Utils.FormatRequestsExceptions(e, message, responseStatusCode));
}
catch (ArgumentNullException e)
{
Expand Down Expand Up @@ -2072,14 +2106,14 @@ public static async Task<HttpContent> SendV2RequestForContentAsync(HttpRequestMe
{
if (responseStatusCode.Equals(HttpStatusCode.NotFound))
{
throw new ResourceNotFoundException(Utils.FormatRequestsExceptions(e, message));
throw new ResourceNotFoundException(Utils.FormatRequestsExceptions(e, message, responseStatusCode));
}
if (responseStatusCode.Equals(HttpStatusCode.Unauthorized))
{
throw new UnauthorizedException(Utils.FormatCredentialRequestExceptions(e));
}

throw new HttpRequestException(Utils.FormatRequestsExceptions(e, message));
throw new HttpRequestException(Utils.FormatRequestsExceptions(e, message, responseStatusCode));
}
catch (ArgumentNullException e)
{
Expand Down
17 changes: 17 additions & 0 deletions test/PublishPSResourceTests/PublishPSResource.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,23 @@ function script:CreateTestModule
'@ | Out-File -FilePath $moduleSrc
}

Describe 'PowerShell Gallery status hint' -tags 'CI' {
BeforeAll {
$utilsType = [Microsoft.PowerShell.PSResourceGet.Cmdlets.PublishPSResource].Assembly.GetType('Microsoft.PowerShell.PSResourceGet.UtilClasses.Utils')
$statusMessageMethod = $utilsType.GetMethod('GetPSGalleryStatusMessage', [type[]]@([uri], [System.Net.HttpStatusCode]))
}

It 'includes the hint for a 503 from the PowerShell Gallery' {
$hint = $statusMessageMethod.Invoke($null, @([uri]'https://www.powershellgallery.com/api/v2', [System.Net.HttpStatusCode]::ServiceUnavailable))
$hint | Should -Match 'https://aka.ms/psgallery'
}

It 'does not include the hint for a 503 from a non-Gallery host' {
$hint = $statusMessageMethod.Invoke($null, @([uri]'https://example.org/api/v2', [System.Net.HttpStatusCode]::ServiceUnavailable))
$hint | Should -BeNullOrEmpty
}
}

Describe "Test Publish-PSResource" -tags 'CI' {
BeforeAll {
Get-NewPSResourceRepositoryFile
Expand Down
Loading
Loading