diff --git a/src/libraries/Microsoft.Extensions.Http/src/Logging/HttpHeadersLogValue.cs b/src/libraries/Microsoft.Extensions.Http/src/Logging/HttpHeadersLogValue.cs index c2c6223c90ba3c..c5241af88ca420 100644 --- a/src/libraries/Microsoft.Extensions.Http/src/Logging/HttpHeadersLogValue.cs +++ b/src/libraries/Microsoft.Extensions.Http/src/Logging/HttpHeadersLogValue.cs @@ -11,6 +11,8 @@ namespace Microsoft.Extensions.Http.Logging { internal sealed class HttpHeadersLogValue : IReadOnlyList> { + private const string RedactedValue = "*"; + private readonly Kind _kind; private readonly Func _shouldRedactHeaderValue; @@ -36,19 +38,13 @@ private List> Values { if (_values == null) { - var values = new List>(); + var values = new List>(GetHeaderCount(Headers) + GetHeaderCount(ContentHeaders)); - foreach (KeyValuePair> kvp in Headers) - { - values.Add(new KeyValuePair(kvp.Key, kvp.Value)); - } + AddHeaders(values, Headers); if (ContentHeaders != null) { - foreach (KeyValuePair> kvp in ContentHeaders) - { - values.Add(new KeyValuePair(kvp.Key, kvp.Value)); - } + AddHeaders(values, ContentHeaders); } _values = values; @@ -73,6 +69,36 @@ public KeyValuePair this[int index] public int Count => Values.Count; + // 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> values, HttpHeaders headers) + { +#if NET + foreach (KeyValuePair kvp in headers.NonValidated) +#else + foreach (KeyValuePair> kvp in headers) +#endif + { + string value = _shouldRedactHeaderValue(kvp.Key) + ? RedactedValue +#if NET + : kvp.Value.ToString(); +#else + : string.Join(", ", kvp.Value); +#endif + values.Add(new KeyValuePair(kvp.Key, value)); + } + } + + private static int GetHeaderCount(HttpHeaders? headers) + { +#if NET + return headers?.NonValidated.Count ?? 0; +#else + return 0; +#endif + } + public IEnumerator> GetEnumerator() { return Values.GetEnumerator(); @@ -95,29 +121,8 @@ public override string ToString() KeyValuePair kvp = Values[i]; builder.Append(kvp.Key); builder.Append(": "); - - if (_shouldRedactHeaderValue(kvp.Key)) - { - builder.Append('*'); - builder.AppendLine(); - } - else - { -#if NET - builder.AppendJoin(", ", (IEnumerable)kvp.Value); - builder.AppendLine(); -#else - foreach (object value in (IEnumerable)kvp.Value) - { - builder.Append(value); - builder.Append(", "); - } - - // Remove the extra ', ' - builder.Remove(builder.Length - 2, 2); - builder.AppendLine(); -#endif - } + builder.Append((string)kvp.Value); + builder.AppendLine(); } _formatted = builder.ToString(); diff --git a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/Logging/HttpHeadersLogValueTest.cs b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/Logging/HttpHeadersLogValueTest.cs index 81120214152c2c..b5a4b393188c73 100644 --- a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/Logging/HttpHeadersLogValueTest.cs +++ b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/Logging/HttpHeadersLogValueTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Net.Http; using System.Net.Http.Headers; using Xunit; @@ -47,7 +48,32 @@ public void HttpHeadersLogValue_ToString_HidesOnlyLogSensitiveHeadersValue() "unsecureHeader2: value2" + Environment.NewLine + "secureHeader2: *" + Environment.NewLine, result); + + // Redaction is applied to the structured values, not just to the formatted string. + Assert.Equal("secureHeader1", httpHeadersLogValue[0].Key); + Assert.Equal("*", httpHeadersLogValue[0].Value); + Assert.Equal("secureHeader2", httpHeadersLogValue[3].Key); + Assert.Equal("*", httpHeadersLogValue[3].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 private class TestHttpHeaders : HttpHeaders { } }