-
Notifications
You must be signed in to change notification settings - Fork 8.2k
Improve verbose and debug logging level messaging in web cmdlets #25510
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
iSazonov
merged 16 commits into
PowerShell:master
from
JustinGrote:justingrote/issue25492
Jun 11, 2025
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
bf0eb7c
WebRequest Cmdlets: Improve Verbose and Debug Log
JustinGrote 36de8b1
Incorporate latest @iSazonov feedback
JustinGrote 85ac955
Always emit content encoding message
JustinGrote 511cdf1
Update message for debug
JustinGrote a226692
DebugHeaderPrefix Consistency
JustinGrote d28c5d5
Replace request with currentRequest
JustinGrote da5229c
Fix inconsistent method name
JustinGrote 43175db
Test: Adjust to test for codepage (more deterministic)
JustinGrote f4f446f
Remove Unused do/while loop that caused test deadlock.
JustinGrote 4d28460
Add back OutFile to Verbose Output
JustinGrote 8484532
Revert "Remove Unused do/while loop that caused test deadlock."
JustinGrote 06b64cb
Remove misplaced verbosebuilder
JustinGrote 81c18a5
Revert Accidental totalrequests-- change
JustinGrote e3b68de
Revert disabling request verbose/debug info
JustinGrote 1395758
Minor formatting and spacing cleanup
JustinGrote fb3adbf
make body size response message consistent with request
JustinGrote File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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> | ||
|
|
@@ -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) | ||
| { | ||
|
|
@@ -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) | ||
JustinGrote marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| 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 | ||
JustinGrote marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
iSazonov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| 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) | ||
JustinGrote marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| + "]" | ||
| : 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) | ||
JustinGrote marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| try | ||
| { | ||
| // Typical basic example: WebResponse: 200 OK with text/plain payload body size 6 B (6 bytes) | ||
| StringBuilder verboseBuilder = new(128); | ||
JustinGrote marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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) | ||
JustinGrote marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| 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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
|
@@ -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); | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.