Skip to content

Don't validate/materialize HTTP headers when logging them - #132010

Open
MihaZupan with Copilot wants to merge 5 commits into
mainfrom
copilot/fix-logging-http-message-handler
Open

Don't validate/materialize HTTP headers when logging them#132010
MihaZupan with Copilot wants to merge 5 commits into
mainfrom
copilot/fix-logging-http-message-handler

Conversation

Copilot AI commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

HttpHeadersLogValue enumerated HttpHeaders directly, which forces lazy parsing of known headers. With LogLevel.Trace enabled for Microsoft.Extensions.Http, merely logging a request re-serialized values added via TryAddWithoutValidation:

request.Headers.TryAddWithoutValidation("Accept", "application/vnd.example+json;version=1");
// sent as: Accept: application/vnd.example+json; version=1

Changes

  • Enumerate via HttpHeaders.NonValidated — header enumeration moves into an AddHeaders helper that uses the non-validated view, so logging no longer triggers parsing. netstandard2.0/net462 keep the previous enumeration under #else, since NonValidated is .NET 5+.
  • Store HeaderStringValues.ToString() per header — it already joins multiple values with ", ", matching the previous AppendJoin(", ", ...) output, so the rendered log text is unchanged.
  • Presize the values list via HttpHeadersNonValidated.Count.
  • Regression test in HttpHeadersLogValueTest asserting both the formatted output and that NonValidated still returns the raw value after formatting.

Note for reviewers

On .NET, the object value in the exposed IReadOnlyList<KeyValuePair<string, object>> changes from IEnumerable<string> to a single joined string. Message text is identical, but a structured-logging consumer casting the value to IEnumerable<string> would be affected. Happy to preserve the enumerable shape instead if that tradeoff isn't acceptable.

Copilot AI review requested due to automatic review settings August 7, 2026 15:21

Copilot AI left a comment

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.

Copilot wasn't able to review any files in this pull request.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
16 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 15:27

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/libraries/Microsoft.Extensions.Http/src/Logging/HttpHeadersLogValue.cs:91

  • Under #if NET, headers.NonValidated already materializes values as either a single string or a string[] via HttpHeaders.GetStoreValuesAsStringOrStringArray(...). The current implementation allocates a new string[] and copies all values again, which is redundant per-header allocation/copy on the Trace logging path.

Consider storing the HeaderStringValues directly (it remains non-validating and enumerates the same raw strings) to avoid the extra array allocation and copy.

                string[] headerValues = new string[kvp.Value.Count];
                int i = 0;
                foreach (string value in kvp.Value)
                {
                    headerValues[i++] = value;

Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 15:56
Copilot AI changed the title [WIP] Fix LoggingHttpMessageHandler behavior with TryAddWithoutValidation Don't validate/materialize HTTP headers when logging them Aug 7, 2026
Copilot AI requested a review from MihaZupan August 7, 2026 15:58
@MihaZupan MihaZupan added this to the 11.0.0 milestone Aug 7, 2026
@MihaZupan
MihaZupan marked this pull request as ready for review August 7, 2026 16:05

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

src/libraries/Microsoft.Extensions.Http/src/Logging/HttpHeadersLogValue.cs:85

  • Even when a header name should be redacted, AddHeaders currently stores the real header value in Values. Because HttpHeadersLogValue is used as an ILogger state object, structured loggers can serialize these values regardless of the ToString() implementation.

Consider passing the redaction predicate into AddHeaders and storing "*" for redacted headers (and in the NET case, avoid calling HeaderStringValues.ToString() when redacted).

        // Enumerate the headers without triggering validation/parsing of the values, so that logging
        // doesn't alter how the headers are subsequently serialized on the wire.
        private static void AddHeaders(List<KeyValuePair<string, object>> values, HttpHeaders headers)
        {
#if NET
            foreach (KeyValuePair<string, HeaderStringValues> kvp in headers.NonValidated)
            {
                values.Add(new KeyValuePair<string, object>(kvp.Key, kvp.Value.ToString()));
            }
#else
            foreach (KeyValuePair<string, IEnumerable<string>> kvp in headers)
            {
                values.Add(new KeyValuePair<string, object>(kvp.Key, kvp.Value));
            }
#endif
        }

src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/Logging/HttpHeadersLogValueTest.cs:70

  • The existing redaction test only validates HttpHeadersLogValue.ToString(). Since this type is passed as ILogger state, structured loggers (e.g., JSON console) can enumerate the state and emit the key/value pairs directly. To ensure RedactLoggedHeaders actually redacts in logs, add an assertion/test that enumerating HttpHeadersLogValue yields "*" for redacted headers (not the original value).
#if NET
        [Fact]
        public void HttpHeadersLogValue_DoesNotValidateHeaderValues()
        {
            var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com");
            request.Headers.TryAddWithoutValidation("Accept", "application/vnd.example+json;version=1");

            var httpHeadersLogValue = new HttpHeadersLogValue(HttpHeadersLogValue.Kind.Request, request.Headers, contentHeaders: null, _ => false);

            Assert.Equal(
                "Request Headers:" + Environment.NewLine +
                "Accept: application/vnd.example+json;version=1" + Environment.NewLine,
                httpHeadersLogValue.ToString());

            Assert.True(request.Headers.NonValidated.TryGetValues("Accept", out HeaderStringValues values));
            Assert.Equal("application/vnd.example+json;version=1", Assert.Single(values));
        }
#endif

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@MihaZupan
MihaZupan marked this pull request as draft August 7, 2026 16:23
Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 18:06

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/libraries/Microsoft.Extensions.Http/src/Logging/HttpHeadersLogValue.cs:81

  • This change also alters the structured logging payload: in NET builds the per-header Value is now always a single joined string (or "*") rather than an IEnumerable. Providers/consumers that previously treated these as arrays (or cast to IEnumerable) will observe different structured data even though the formatted message stays the same. If preserving the enumerable shape is important, the implementation likely needs an alternative representation (e.g., keep IEnumerable for structured state and only join when formatting).
        // Enumerate the headers without triggering validation/parsing of the values, so that logging
        // doesn't alter how the headers are subsequently serialized on the wire.
        private void AddHeaders(List<KeyValuePair<string, object>> values, HttpHeaders headers)
        {
#if NET
            foreach (KeyValuePair<string, HeaderStringValues> kvp in headers.NonValidated)
            {
                object value = _shouldRedactHeaderValue(kvp.Key) ? RedactedValue : kvp.Value.ToString();
                values.Add(new KeyValuePair<string, object>(kvp.Key, value));
            }

src/libraries/Microsoft.Extensions.Http/src/Logging/HttpHeadersLogValue.cs:79

  • In the NET path, using HeaderStringValues.ToString() will join multiple header values using the header-specific separator (e.g., Cookie uses "; "), whereas the previous implementation always joined with ", ". That means the formatted log text can change for multi-valued headers like Cookie, which seems to contradict the PR goal/description of keeping rendered log output unchanged.
            foreach (KeyValuePair<string, HeaderStringValues> kvp in headers.NonValidated)
            {
                object value = _shouldRedactHeaderValue(kvp.Key) ? RedactedValue : kvp.Value.ToString();
                values.Add(new KeyValuePair<string, object>(kvp.Key, value));

…TFMs

Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 19:02

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/libraries/Microsoft.Extensions.Http/src/Logging/HttpHeadersLogValue.cs:90

  • This change makes the structured logging payload store each header value as a single joined string (and the ToString path now casts (string)kvp.Value). Previously the payload exposed IEnumerable<string> per header, so structured-log consumers could access individual values without re-parsing the formatted string. This is a user-observable behavior change for providers that capture state, and it could break consumers that cast the value to IEnumerable<string>.

Consider preserving the previous structured shape (e.g., store HeaderStringValues / IEnumerable<string> as the object value and only join during formatting) while still enumerating via headers.NonValidated to avoid validation.

                string value = _shouldRedactHeaderValue(kvp.Key)
                    ? RedactedValue
#if NET
                    : kvp.Value.ToString();
#else
                    : string.Join(", ", kvp.Value);
#endif
                values.Add(new KeyValuePair<string, object>(kvp.Key, value));
            }

src/libraries/Microsoft.Extensions.Http/src/Logging/HttpHeadersLogValue.cs:88

  • On NET, HeaderStringValues.ToString() joins multi-values using the header-specific separator (HeaderStringValues.cs:45-50), which isn't always ", " (e.g., User-Agent/Server use whitespace per ProductInfoHeaderParser.cs:64-65). That means this change can alter the rendered log text for multi-valued headers, contradicting the PR description that the formatted output is unchanged. If keeping the exact previous formatting is required, join with ", " explicitly when constructing the per-header string.

This issue also appears on line 82 of the same file.

                string value = _shouldRedactHeaderValue(kvp.Key)
                    ? RedactedValue
#if NET
                    : kvp.Value.ToString();
#else
                    : string.Join(", ", kvp.Value);
#endif

@MihaZupan
MihaZupan marked this pull request as ready for review August 7, 2026 20:25
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LoggingHttpMessageHandler changes HttpHeaders added with TryAddWithoutValidation when Trace logging is enabled

3 participants