Skip to content
Merged
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 @@ -9,7 +9,7 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;

using Humanizer;
using Microsoft.Win32;

namespace Microsoft.PowerShell.Commands
Expand All @@ -21,8 +21,15 @@ internal static class ContentHelper
// ContentType may not exist in response header. Return null if not.
internal static string? GetContentType(HttpResponseMessage response) => response.Content.Headers.ContentType?.MediaType;

internal static string? GetContentType(HttpRequestMessage request) => request.Content?.Headers.ContentType?.MediaType;

internal static Encoding GetDefaultEncoding() => Encoding.UTF8;

internal static string GetFriendlyContentLength(long? length) =>
length.HasValue
? $"{length.Value.Bytes().Humanize()} ({length.Value:#,0} bytes)"
: "unknown size";

internal static StringBuilder GetRawContentHeader(HttpResponseMessage response)
{
StringBuilder raw = new();
Expand Down Expand Up @@ -133,10 +140,13 @@ internal static bool IsXml([NotNullWhen(true)] string? contentType)
|| contentType.Equals("application/xml-external-parsed-entity", StringComparison.OrdinalIgnoreCase)
|| contentType.Equals("application/xml-dtd", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("+xml", StringComparison.OrdinalIgnoreCase);

return isXml;
}

internal static bool IsTextBasedContentType([NotNullWhen(true)] string? contentType)
=> IsText(contentType) || IsJson(contentType) || IsXml(contentType);

#endregion Internal Methods
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,22 @@ internal override void ProcessResponse(HttpResponseMessage response)
string? characterSet = WebResponseHelper.GetCharacterSet(response);
string str = StreamHelper.DecodeStream(responseStream, characterSet, out Encoding encoding, perReadTimeout, _cancelToken.Token);

string encodingVerboseName;
string friendlyName = "unknown";
string encodingWebName = "unknown";
string encodingPage = encoding.CodePage == -1 ? "unknown" : encoding.CodePage.ToString();
try
{
encodingVerboseName = encoding.HeaderName;
// NOTE: These are getter methods that may possibly throw a NotSupportedException exception,
// hence the try/catch
encodingWebName = encoding.WebName;
friendlyName = encoding.EncodingName;
}
catch
{
encodingVerboseName = string.Empty;
}

// NOTE: Tests use this verbose output to verify the encoding.
WriteVerbose($"Content encoding: {encodingVerboseName}");
// NOTE: Tests use this debug output to verify the encoding.
WriteDebug($"WebResponse content encoding: {encodingWebName} ({friendlyName}) CodePage: {encodingPage}");

// Determine the response type
RestReturnType returnType = CheckReturnType(response);
Expand Down Expand Up @@ -140,7 +144,7 @@ internal override void ProcessResponse(HttpResponseMessage response)

responseStream.Position = 0;
}

if (ShouldSaveToOutFile)
{
string outFilePath = WebResponseHelper.GetOutFilePath(response, _qualifiedOutFile);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ public abstract class WebRequestPSCmdlet : PSCmdlet, IDisposable
{
#region Fields

/// <summary>
/// Used to prefix the headers in debug and verbose messaging.
/// </summary>
internal const string DebugHeaderPrefix = "--- ";

/// <summary>
/// Cancellation token source.
/// </summary>
Expand Down Expand Up @@ -1280,40 +1285,27 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM
_cancelToken = new CancellationTokenSource();
try
{
long requestContentLength = request.Content is null ? 0 : request.Content.Headers.ContentLength.Value;

string reqVerboseMsg = string.Format(
CultureInfo.CurrentCulture,
WebCmdletStrings.WebMethodInvocationVerboseMsg,
request.Version,
request.Method,
requestContentLength);

WriteVerbose(reqVerboseMsg);

string reqDebugMsg = string.Format(
CultureInfo.CurrentCulture,
WebCmdletStrings.WebRequestDebugMsg,
request.ToString());
if (IsWriteVerboseEnabled())
{
WriteWebRequestVerboseInfo(currentRequest);
}

WriteDebug(reqDebugMsg);
if (IsWriteDebugEnabled())
{
WriteWebRequestDebugInfo(currentRequest);
}

response = client.SendAsync(currentRequest, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult();

string contentType = ContentHelper.GetContentType(response);
long? contentLength = response.Content.Headers.ContentLength;
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);

string resDebugMsg = string.Format(
CultureInfo.CurrentCulture,
WebCmdletStrings.WebResponseDebugMsg,
response.ToString());
if (IsWriteVerboseEnabled())
{
WriteWebResponseVerboseInfo(response);
}

WriteDebug(resDebugMsg);
if (IsWriteDebugEnabled())
{
WriteWebResponseDebugInfo(response);
}
}
catch (TaskCanceledException ex)
{
Expand Down Expand Up @@ -1437,13 +1429,206 @@ internal virtual void UpdateSession(HttpResponseMessage response)
{
ArgumentNullException.ThrowIfNull(response);
}

#endregion Virtual Methods

#region Helper Methods

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

private void WriteWebRequestVerboseInfo(HttpRequestMessage request)
{
try
{
// Typical Basic Example: 'WebRequest: v1.1 POST https://httpstat.us/200 with query length 6'
StringBuilder verboseBuilder = new(128);

// "Redact" the query string from verbose output, the details will be visible in Debug output
string uriWithoutQuery = request.RequestUri?.GetLeftPart(UriPartial.Path) ?? string.Empty;
verboseBuilder.Append($"WebRequest: v{request.Version} {request.Method} {uriWithoutQuery}");
if (request.RequestUri?.Query is not null && request.RequestUri.Query.Length > 1)
{
verboseBuilder.Append($" with query length {request.RequestUri.Query.Length - 1}");
}

string? requestContentType = ContentHelper.GetContentType(request);
if (requestContentType is not null)
{
verboseBuilder.Append($" with {requestContentType} payload");
}

long? requestContentLength = request.Content?.Headers?.ContentLength;
if (requestContentLength is not null)
{
verboseBuilder.Append($" with body size {ContentHelper.GetFriendlyContentLength(requestContentLength)}");
}
if (OutFile is not null)
Comment on lines +1460 to +1464
Copy link
Collaborator

Choose a reason for hiding this comment

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

Style: new line is needed after brace.

{
verboseBuilder.Append($" output to {QualifyFilePath(OutFile)}");
}

WriteVerbose(verboseBuilder.ToString().Trim());
}
catch (Exception ex)
{
// Just in case there are any edge cases we missed, we don't break workflows with an exception
WriteVerbose($"Failed to Write WebRequest Verbose Info: {ex} {ex.StackTrace}");
}
}

private void WriteWebRequestDebugInfo(HttpRequestMessage request)
{
try
{
// Typical basic example:
// WebRequest Detail
// ---QUERY
// test = 5
// --- HEADERS
// User - Agent: Mozilla / 5.0, (Linux;Ubuntu 24.04.2 LTS;en - US), PowerShell / 7.6.0
StringBuilder debugBuilder = new("WebRequest Detail" + Environment.NewLine, 512);

if (!string.IsNullOrEmpty(request.RequestUri?.Query))
{
debugBuilder.Append(DebugHeaderPrefix).AppendLine("QUERY");
string[] queryParams = request.RequestUri.Query.TrimStart('?').Split('&');
debugBuilder
.AppendJoin(Environment.NewLine, queryParams)
.AppendLine()
.AppendLine();
}

debugBuilder.Append(DebugHeaderPrefix).AppendLine("HEADERS");

foreach (var headerSet in new HttpHeaders?[] { request.Headers, request.Content?.Headers })
{
if (headerSet is null)
{
continue;
}

debugBuilder.AppendLine(headerSet.ToString());
}

if (request.Content is not null)
{
debugBuilder
.Append(DebugHeaderPrefix).AppendLine("BODY")
.AppendLine(request.Content switch
{
StringContent stringContent => stringContent
.ReadAsStringAsync(_cancelToken.Token)
.GetAwaiter().GetResult(),
MultipartFormDataContent multipartContent => "=> Multipart Form Content"
+ Environment.NewLine
+ multipartContent.ReadAsStringAsync(_cancelToken.Token)
.GetAwaiter().GetResult(),
ByteArrayContent byteContent => InFile is not null
? "[Binary content: "
+ ContentHelper.GetFriendlyContentLength(byteContent.Headers.ContentLength)
+ "]"
: byteContent.ReadAsStringAsync(_cancelToken.Token).GetAwaiter().GetResult(),
StreamContent streamContent =>
"[Stream content: " + ContentHelper.GetFriendlyContentLength(streamContent.Headers.ContentLength) + "]",
_ => "[Unknown content type]",
})
.AppendLine();
}

WriteDebug(debugBuilder.ToString().Trim());
}
catch (Exception ex)
{
// Just in case there are any edge cases we missed, we don't break workflows with an exception
WriteVerbose($"Failed to Write WebRequest Debug Info: {ex} {ex.StackTrace}");
}
}

private void WriteWebResponseVerboseInfo(HttpResponseMessage response)
{
try
{
// Typical basic example: WebResponse: 200 OK with text/plain payload body size 6 B (6 bytes)
StringBuilder verboseBuilder = new(128);
verboseBuilder.Append($"WebResponse: {(int)response.StatusCode} {response.ReasonPhrase ?? response.StatusCode.ToString()}");

string? responseContentType = ContentHelper.GetContentType(response);
if (responseContentType is not null)
{
verboseBuilder.Append($" with {responseContentType} payload");
}

long? responseContentLength = response.Content?.Headers?.ContentLength;
if (responseContentLength is not null)
{
verboseBuilder.Append($" with body size {ContentHelper.GetFriendlyContentLength(responseContentLength)}");
}

WriteVerbose(verboseBuilder.ToString().Trim());
}
catch (Exception ex)
{
// Just in case there are any edge cases we missed, we don't break workflows with an exception
WriteVerbose($"Failed to Write WebResponse Verbose Info: {ex} {ex.StackTrace}");
}
}

private void WriteWebResponseDebugInfo(HttpResponseMessage response)
{
try
{
// Typical basic example
// WebResponse Detail
// --- HEADERS
// Date: Fri, 09 May 2025 18:06:44 GMT
// Server: Kestrel
// Set-Cookie: ARRAffinity=ee0b467f95b53d8dcfe48aeeb4173f93cf819be6e4721f434341647f4695039d;Path=/;HttpOnly;Secure;Domain=httpstat.us, ARRAffinitySameSite=ee0b467f95b53d8dcfe48aeeb4173f93cf819be6e4721f434341647f4695039d;Path=/;HttpOnly;SameSite=None;Secure;Domain=httpstat.us
// Strict-Transport-Security: max-age=2592000
// Request-Context: appId=cid-v1:3548b0f5-7f75-492f-82bb-b6eb0e864e53
// Content-Length: 6
// Content-Type: text/plain
// --- BODY
// 200 OK
StringBuilder debugBuilder = new("WebResponse Detail" + Environment.NewLine, 512);

debugBuilder.Append(DebugHeaderPrefix).AppendLine("HEADERS");

foreach (var headerSet in new HttpHeaders?[] { response.Headers, response.Content?.Headers })
{
if (headerSet is null)
{
continue;
}

debugBuilder.AppendLine(headerSet.ToString());
}

if (response.Content is not null)
{
debugBuilder.Append(DebugHeaderPrefix).AppendLine("BODY");

if (ContentHelper.IsTextBasedContentType(ContentHelper.GetContentType(response)))
{
debugBuilder.AppendLine(
response.Content.ReadAsStringAsync(_cancelToken.Token)
.GetAwaiter().GetResult());
}
else
{
string friendlyContentLength = ContentHelper.GetFriendlyContentLength(
response.Content?.Headers?.ContentLength);
Copy link
Collaborator

@iSazonov iSazonov Jun 11, 2025

Choose a reason for hiding this comment

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

Headers is not null here.

debugBuilder.AppendLine($"[Binary content: {friendlyContentLength}]");
}
}

WriteDebug(debugBuilder.ToString().Trim());
}
catch (Exception ex)
{
// Just in case there are any edge cases we missed, we don't break workflows with an exception
WriteVerbose($"Failed to Write WebResponse Debug Info: {ex} {ex.StackTrace}");
}
}

private Uri PrepareUri(Uri uri)
{
uri = CheckProtocol(uri);
Expand Down Expand Up @@ -1478,6 +1663,7 @@ private static Uri CheckProtocol(Uri uri)

return uri.IsAbsoluteUri ? uri : new Uri("http://" + uri.OriginalString);
}
#nullable restore

private string QualifyFilePath(string path) => PathUtils.ResolveFilePath(filePath: path, command: this, isLiteralPath: true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,21 +234,9 @@
<data name="FollowingRelLinkVerboseMsg" xml:space="preserve">
<value>Following rel link {0}</value>
</data>
<data name="WebMethodInvocationVerboseMsg" xml:space="preserve">
<value>Requested HTTP/{0} {1} with {2}-byte payload</value>
</data>
<data name="WebMethodResumeFailedVerboseMsg" xml:space="preserve">
<value>The remote server indicated it could not resume downloading. The local file will be overwritten.</value>
</data>
<data name="WebResponseVerboseMsg" xml:space="preserve">
<value>Received HTTP/{0} {1}-byte response of content type {2}</value>
</data>
<data name="WebRequestDebugMsg" xml:space="preserve">
<value>Request {0}</value>
</data>
<data name="WebResponseDebugMsg" xml:space="preserve">
<value>Response {0}</value>
</data>
<data name="WebResponseNoSizeVerboseMsg" xml:space="preserve">
<value>Received HTTP/{0} response of content type {1} of unknown size</value>
</data>
Expand Down
Loading
Loading