From 439afe952275d4d13ecf1483d415d83c19477937 Mon Sep 17 00:00:00 2001 From: Todd Date: Thu, 7 Dec 2023 23:25:55 -0600 Subject: [PATCH] #778 DefautRequestHeaders sync bug --- src/Flurl.Http/FlurlClient.cs | 28 ++++++++++++++++++++++++++-- test/Flurl.Test/Http/HeadersTests.cs | 13 +++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index b23dd0a2..474d8568 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -7,6 +7,8 @@ using Flurl.Http.Testing; using Flurl.Util; using System.Collections.Generic; +using System.Net.Http.Headers; +using System.Reflection; namespace Flurl.Http { @@ -83,12 +85,34 @@ internal FlurlClient(HttpClient httpClient, string baseUrl, FlurlHttpSettings se EventHandlers = eventHandlers ?? new List<(FlurlEventType, IFlurlEventHandler)>(); Headers = headers ?? new NameValueList(false); // header names are case-insensitive https://stackoverflow.com/a/5259004/62600 - foreach (var header in httpClient.DefaultRequestHeaders.SelectMany(h => h.Value, (kv, v) => (kv.Key, v))) { - if (!Headers.Contains(header.Key)) + + foreach (var header in GetHeadersFromHttpClient(httpClient)) { + if (!Headers.Contains(header.Name)) Headers.Add(header); } } + // reflection is (relatively) expensive, so keep a cache of HttpRequestHeaders properties + // https://learn.microsoft.com/en-us/dotnet/api/system.net.http.headers.httprequestheaders?#properties + private static IDictionary _reqHeaderProps = + typeof(HttpRequestHeaders).GetProperties().ToDictionary(p => p.Name.ToLower(), p => p); + + private static IEnumerable<(string Name, string Value)> GetHeadersFromHttpClient(HttpClient httpClient) { + foreach (var h in httpClient.DefaultRequestHeaders) { + // MS isn't making this easy. In some cases, a header value will be split into multiple values, but when iterating the collection + // there's no way to know exactly how to piece them back together. The standard says multiple values should be comma-delimited, + // but with User-Agent they need to be space-delimited. ToString() on properties like UserAgent do this correctly though, so when spinning + // through the collection we'll try to match the header name to a property and ToString() it, otherwise we'll comma-delimit the values. + if (_reqHeaderProps.TryGetValue(h.Key.Replace("-", "").ToLower(), out var prop)) { + var val = prop.GetValue(httpClient.DefaultRequestHeaders).ToString(); + yield return (h.Key, val); + } + else { + yield return (h.Key, string.Join(",", h.Value)); + } + } + } + /// public string BaseUrl { get; set; } diff --git a/test/Flurl.Test/Http/HeadersTests.cs b/test/Flurl.Test/Http/HeadersTests.cs index 684ddd40..6795e174 100644 --- a/test/Flurl.Test/Http/HeadersTests.cs +++ b/test/Flurl.Test/Http/HeadersTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using Flurl.Http; using Flurl.Http.Configuration; @@ -147,6 +148,18 @@ public class ClientHeadersTests : HeadersTestsBase { protected override IFlurlClient CreateContainer() => new FlurlClient(); protected override IFlurlRequest GetRequest(IFlurlClient cli) => cli.Request("http://api.com"); + + [Test] + public void can_copy_multi_value_header_from_HttpClient() { + var userAgent = "Mozilla/5.0 (X11; Linux i686; rv:109.0) Gecko/20100101 Firefox/120.0"; + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("user-agent", userAgent); + httpClient.DefaultRequestHeaders.TryAddWithoutValidation("foo", new[] {"a", "b", "c"}); + var flurlClient = new FlurlClient(httpClient); + + Assert.AreEqual(userAgent, flurlClient.Headers[0].Value); + Assert.AreEqual("a,b,c", flurlClient.Headers[1].Value); + } } [TestFixture]