From edbe2caa85a56b66194722ee398fa32a67078829 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Wed, 26 Mar 2025 16:58:23 -0700 Subject: [PATCH 1/7] Add setting to compress HTTP request bodies. This opt-in setting will cause the HttpContent to be compressed with "Content-Encoding: gzip". --- src/Facility.Core/Http/HttpClientService.cs | 63 ++++++++++++++++++- .../Http/HttpClientServiceDefaults.cs | 5 ++ .../Http/HttpClientServiceSettings.cs | 8 +++ .../Http/HttpClientServiceSettingsTests.cs | 1 + 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/Facility.Core/Http/HttpClientService.cs b/src/Facility.Core/Http/HttpClientService.cs index b5b31e70..795feeea 100644 --- a/src/Facility.Core/Http/HttpClientService.cs +++ b/src/Facility.Core/Http/HttpClientService.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.IO.Compression; using System.Net.Http.Headers; using System.Net.ServerSentEvents; using System.Runtime.CompilerServices; @@ -20,6 +21,7 @@ protected HttpClientService(HttpClientServiceSettings? settings, HttpClientServi m_httpClient = settings.HttpClient ?? s_defaultHttpClient; m_aspects = settings.Aspects; + m_shouldCompressRequest = settings.ShouldCompressRequest ?? ((defaults.CompressRequests ?? false) ? (_ => true) : (_ => false)); m_synchronous = settings.Synchronous; m_skipRequestValidation = settings.SkipRequestValidation; m_skipResponseValidation = settings.SkipResponseValidation; @@ -101,8 +103,66 @@ protected HttpClientService(HttpClientServiceSettings? settings, Uri? defaultBas { var contentType = mapping.RequestBodyContentType ?? requestHeaders?.GetContentType(); httpRequest.Content = GetHttpContentSerializer(requestBody.GetType()).CreateHttpContent(requestBody, contentType); - if (m_disableChunkedTransfer) + if (m_shouldCompressRequest(request)) + httpRequest.Content = await CompressContentAsync(httpRequest.Content, cancellationToken).ConfigureAwait(false); + else if (m_disableChunkedTransfer) await httpRequest.Content.LoadIntoBufferAsync().ConfigureAwait(false); + + async static Task CompressContentAsync(HttpContent httpContent, CancellationToken cancellationToken) + { + Stream? contentStream = null; + Stream? compressedContentStream = null; + + try + { + // copy the existing HTTP content into a memory stream + contentStream = new MemoryStream(); +#if NET5_0_OR_GREATER + await httpContent.CopyToAsync(contentStream, cancellationToken).ConfigureAwait(false); +#else + await httpContent.CopyToAsync(contentStream).ConfigureAwait(false); +#endif + contentStream.Position = 0; + + // compress that memory stream with gzip + compressedContentStream = new MemoryStream(); + using (var gzipStream = new GZipStream(compressedContentStream, CompressionLevel.Optimal, leaveOpen: true)) + using (var bufferedStream = new BufferedStream(gzipStream, 8192)) + { +#pragma warning disable CA1849 // Deliberately using synchronous copy for CPU-bound operation + contentStream.CopyTo(bufferedStream); +#pragma warning restore CA1849 // Call async methods when in an async method + } + + contentStream.Position = 0; + compressedContentStream.Position = 0; + + // use the compressed result if it's fewer bytes to send + var useCompressedContent = compressedContentStream.Length < contentStream.Length; + + // always construct a new HttpContent object (because we consumed the Stream from the original one) + var newHttpContent = new StreamContent(useCompressedContent ? compressedContentStream : contentStream); + if (useCompressedContent) + compressedContentStream = null; + else + contentStream = null; + + // copy the headers from the original HttpContent object, adding Content-Encoding if compressed + foreach (var header in httpContent.Headers) + newHttpContent.Headers.TryAddWithoutValidation(header.Key, header.Value); + if (useCompressedContent) + newHttpContent.Headers.ContentEncoding.Add("gzip"); + + return newHttpContent; + } + finally + { +#pragma warning disable CA1849 // No need to asynchronously dispose a MemoryStream + contentStream?.Dispose(); + compressedContentStream?.Dispose(); +#pragma warning restore CA1849 // Call async methods when in an async method + } + } } // send the HTTP request and get the HTTP response @@ -472,6 +532,7 @@ private HttpContentSerializer GetHttpContentSerializer(Type objectType) => private readonly HttpClient m_httpClient; private readonly IReadOnlyList? m_aspects; + private readonly Func m_shouldCompressRequest; private readonly bool m_synchronous; private readonly bool m_skipRequestValidation; private readonly bool m_skipResponseValidation; diff --git a/src/Facility.Core/Http/HttpClientServiceDefaults.cs b/src/Facility.Core/Http/HttpClientServiceDefaults.cs index 6e9d6d52..3bf3493d 100644 --- a/src/Facility.Core/Http/HttpClientServiceDefaults.cs +++ b/src/Facility.Core/Http/HttpClientServiceDefaults.cs @@ -14,4 +14,9 @@ public sealed class HttpClientServiceDefaults /// The default content serializer. /// public HttpContentSerializer? ContentSerializer { get; set; } + + /// + /// True to compress all requests by default. + /// + public bool? CompressRequests { get; set; } } diff --git a/src/Facility.Core/Http/HttpClientServiceSettings.cs b/src/Facility.Core/Http/HttpClientServiceSettings.cs index ca00298a..b7ca1ee1 100644 --- a/src/Facility.Core/Http/HttpClientServiceSettings.cs +++ b/src/Facility.Core/Http/HttpClientServiceSettings.cs @@ -30,6 +30,13 @@ public sealed class HttpClientServiceSettings /// public HttpContentSerializer? TextSerializer { get; set; } + /// + /// An optional callback function that determines if a request should be compressed. + /// + /// Request bodies will be compressed with Content-Encoding: gzip. Even when this callback + /// returns true, the request may be sent uncompressed if compressing would make it larger. + public Func? ShouldCompressRequest { get; set; } + /// /// True to disable chunked transfer encoding (default false). /// @@ -66,6 +73,7 @@ public sealed class HttpClientServiceSettings ContentSerializer = ContentSerializer, BytesSerializer = BytesSerializer, TextSerializer = TextSerializer, + ShouldCompressRequest = ShouldCompressRequest, DisableChunkedTransfer = DisableChunkedTransfer, Aspects = Aspects?.ToList(), Synchronous = Synchronous, diff --git a/tests/Facility.Core.UnitTests/Http/HttpClientServiceSettingsTests.cs b/tests/Facility.Core.UnitTests/Http/HttpClientServiceSettingsTests.cs index 307d2971..7cd1f12f 100644 --- a/tests/Facility.Core.UnitTests/Http/HttpClientServiceSettingsTests.cs +++ b/tests/Facility.Core.UnitTests/Http/HttpClientServiceSettingsTests.cs @@ -17,6 +17,7 @@ public async Task CloneSettings() ContentSerializer = HttpContentSerializer.Create(SystemTextJsonServiceSerializer.Instance), BytesSerializer = BytesHttpContentSerializer.Instance, TextSerializer = TextHttpContentSerializer.Instance, + ShouldCompressRequest = _ => true, DisableChunkedTransfer = true, Aspects = [], Synchronous = true, From e0f490d7075ee2714173b85d060afa9fa585ebe6 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Thu, 27 Mar 2025 13:17:45 -0700 Subject: [PATCH 2/7] Remove delegate and just use Boolean setting. --- src/Facility.Core/Http/HttpClientService.cs | 11 ++++++----- src/Facility.Core/Http/HttpClientServiceDefaults.cs | 2 +- src/Facility.Core/Http/HttpClientServiceSettings.cs | 8 ++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Facility.Core/Http/HttpClientService.cs b/src/Facility.Core/Http/HttpClientService.cs index 795feeea..6ffeb70b 100644 --- a/src/Facility.Core/Http/HttpClientService.cs +++ b/src/Facility.Core/Http/HttpClientService.cs @@ -21,7 +21,7 @@ protected HttpClientService(HttpClientServiceSettings? settings, HttpClientServi m_httpClient = settings.HttpClient ?? s_defaultHttpClient; m_aspects = settings.Aspects; - m_shouldCompressRequest = settings.ShouldCompressRequest ?? ((defaults.CompressRequests ?? false) ? (_ => true) : (_ => false)); + m_enableRequestCompression = settings.CompressRequests ?? defaults.CompressRequests; m_synchronous = settings.Synchronous; m_skipRequestValidation = settings.SkipRequestValidation; m_skipResponseValidation = settings.SkipResponseValidation; @@ -103,9 +103,9 @@ protected HttpClientService(HttpClientServiceSettings? settings, Uri? defaultBas { var contentType = mapping.RequestBodyContentType ?? requestHeaders?.GetContentType(); httpRequest.Content = GetHttpContentSerializer(requestBody.GetType()).CreateHttpContent(requestBody, contentType); - if (m_shouldCompressRequest(request)) + if (m_enableRequestCompression) httpRequest.Content = await CompressContentAsync(httpRequest.Content, cancellationToken).ConfigureAwait(false); - else if (m_disableChunkedTransfer) + if (m_disableChunkedTransfer) await httpRequest.Content.LoadIntoBufferAsync().ConfigureAwait(false); async static Task CompressContentAsync(HttpContent httpContent, CancellationToken cancellationToken) @@ -115,7 +115,7 @@ async static Task CompressContentAsync(HttpContent httpContent, Can try { - // copy the existing HTTP content into a memory stream + // copy the existing HTTP content into a memory stream; we will use this MemoryStream as the request body if compressing fails contentStream = new MemoryStream(); #if NET5_0_OR_GREATER await httpContent.CopyToAsync(contentStream, cancellationToken).ConfigureAwait(false); @@ -161,6 +161,7 @@ async static Task CompressContentAsync(HttpContent httpContent, Can contentStream?.Dispose(); compressedContentStream?.Dispose(); #pragma warning restore CA1849 // Call async methods when in an async method + httpContent.Dispose(); } } } @@ -532,7 +533,7 @@ private HttpContentSerializer GetHttpContentSerializer(Type objectType) => private readonly HttpClient m_httpClient; private readonly IReadOnlyList? m_aspects; - private readonly Func m_shouldCompressRequest; + private readonly bool m_enableRequestCompression; private readonly bool m_synchronous; private readonly bool m_skipRequestValidation; private readonly bool m_skipResponseValidation; diff --git a/src/Facility.Core/Http/HttpClientServiceDefaults.cs b/src/Facility.Core/Http/HttpClientServiceDefaults.cs index 3bf3493d..d1db9141 100644 --- a/src/Facility.Core/Http/HttpClientServiceDefaults.cs +++ b/src/Facility.Core/Http/HttpClientServiceDefaults.cs @@ -18,5 +18,5 @@ public sealed class HttpClientServiceDefaults /// /// True to compress all requests by default. /// - public bool? CompressRequests { get; set; } + public bool CompressRequests { get; set; } } diff --git a/src/Facility.Core/Http/HttpClientServiceSettings.cs b/src/Facility.Core/Http/HttpClientServiceSettings.cs index b7ca1ee1..cf545eb9 100644 --- a/src/Facility.Core/Http/HttpClientServiceSettings.cs +++ b/src/Facility.Core/Http/HttpClientServiceSettings.cs @@ -31,11 +31,11 @@ public sealed class HttpClientServiceSettings public HttpContentSerializer? TextSerializer { get; set; } /// - /// An optional callback function that determines if a request should be compressed. + /// True to compress HTTP request bodies (default false). /// - /// Request bodies will be compressed with Content-Encoding: gzip. Even when this callback - /// returns true, the request may be sent uncompressed if compressing would make it larger. - public Func? ShouldCompressRequest { get; set; } + /// Request bodies will be compressed with Content-Encoding: gzip. Even when this property + /// has been set to true, the request may be sent uncompressed if compressing would make it larger. + public bool? CompressRequests { get; set; } /// /// True to disable chunked transfer encoding (default false). From 90aec9ab68ad785b3ffb1b42beb646888c868043 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Thu, 27 Mar 2025 13:29:43 -0700 Subject: [PATCH 3/7] Fix build. --- src/Facility.Core/Http/HttpClientServiceSettings.cs | 2 +- .../Http/HttpClientServiceSettingsTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Facility.Core/Http/HttpClientServiceSettings.cs b/src/Facility.Core/Http/HttpClientServiceSettings.cs index cf545eb9..f1f2c640 100644 --- a/src/Facility.Core/Http/HttpClientServiceSettings.cs +++ b/src/Facility.Core/Http/HttpClientServiceSettings.cs @@ -73,7 +73,7 @@ public sealed class HttpClientServiceSettings ContentSerializer = ContentSerializer, BytesSerializer = BytesSerializer, TextSerializer = TextSerializer, - ShouldCompressRequest = ShouldCompressRequest, + CompressRequests = CompressRequests, DisableChunkedTransfer = DisableChunkedTransfer, Aspects = Aspects?.ToList(), Synchronous = Synchronous, diff --git a/tests/Facility.Core.UnitTests/Http/HttpClientServiceSettingsTests.cs b/tests/Facility.Core.UnitTests/Http/HttpClientServiceSettingsTests.cs index 7cd1f12f..517754ef 100644 --- a/tests/Facility.Core.UnitTests/Http/HttpClientServiceSettingsTests.cs +++ b/tests/Facility.Core.UnitTests/Http/HttpClientServiceSettingsTests.cs @@ -17,7 +17,7 @@ public async Task CloneSettings() ContentSerializer = HttpContentSerializer.Create(SystemTextJsonServiceSerializer.Instance), BytesSerializer = BytesHttpContentSerializer.Instance, TextSerializer = TextHttpContentSerializer.Instance, - ShouldCompressRequest = _ => true, + CompressRequests = true, DisableChunkedTransfer = true, Aspects = [], Synchronous = true, From b2aeb3ed2b5a91d9a1cc47a8bb555198218048b2 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Thu, 27 Mar 2025 13:38:49 -0700 Subject: [PATCH 4/7] Stream the compressed request directly without buffering. --- src/Facility.Core/Http/HttpClientService.cs | 116 ++++++++++---------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/src/Facility.Core/Http/HttpClientService.cs b/src/Facility.Core/Http/HttpClientService.cs index 6ffeb70b..99a5abd0 100644 --- a/src/Facility.Core/Http/HttpClientService.cs +++ b/src/Facility.Core/Http/HttpClientService.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO.Compression; +using System.Net; using System.Net.Http.Headers; using System.Net.ServerSentEvents; using System.Runtime.CompilerServices; @@ -104,66 +105,9 @@ protected HttpClientService(HttpClientServiceSettings? settings, Uri? defaultBas var contentType = mapping.RequestBodyContentType ?? requestHeaders?.GetContentType(); httpRequest.Content = GetHttpContentSerializer(requestBody.GetType()).CreateHttpContent(requestBody, contentType); if (m_enableRequestCompression) - httpRequest.Content = await CompressContentAsync(httpRequest.Content, cancellationToken).ConfigureAwait(false); + httpRequest.Content = new CompressingHttpContent(httpRequest.Content); if (m_disableChunkedTransfer) await httpRequest.Content.LoadIntoBufferAsync().ConfigureAwait(false); - - async static Task CompressContentAsync(HttpContent httpContent, CancellationToken cancellationToken) - { - Stream? contentStream = null; - Stream? compressedContentStream = null; - - try - { - // copy the existing HTTP content into a memory stream; we will use this MemoryStream as the request body if compressing fails - contentStream = new MemoryStream(); -#if NET5_0_OR_GREATER - await httpContent.CopyToAsync(contentStream, cancellationToken).ConfigureAwait(false); -#else - await httpContent.CopyToAsync(contentStream).ConfigureAwait(false); -#endif - contentStream.Position = 0; - - // compress that memory stream with gzip - compressedContentStream = new MemoryStream(); - using (var gzipStream = new GZipStream(compressedContentStream, CompressionLevel.Optimal, leaveOpen: true)) - using (var bufferedStream = new BufferedStream(gzipStream, 8192)) - { -#pragma warning disable CA1849 // Deliberately using synchronous copy for CPU-bound operation - contentStream.CopyTo(bufferedStream); -#pragma warning restore CA1849 // Call async methods when in an async method - } - - contentStream.Position = 0; - compressedContentStream.Position = 0; - - // use the compressed result if it's fewer bytes to send - var useCompressedContent = compressedContentStream.Length < contentStream.Length; - - // always construct a new HttpContent object (because we consumed the Stream from the original one) - var newHttpContent = new StreamContent(useCompressedContent ? compressedContentStream : contentStream); - if (useCompressedContent) - compressedContentStream = null; - else - contentStream = null; - - // copy the headers from the original HttpContent object, adding Content-Encoding if compressed - foreach (var header in httpContent.Headers) - newHttpContent.Headers.TryAddWithoutValidation(header.Key, header.Value); - if (useCompressedContent) - newHttpContent.Headers.ContentEncoding.Add("gzip"); - - return newHttpContent; - } - finally - { -#pragma warning disable CA1849 // No need to asynchronously dispose a MemoryStream - contentStream?.Dispose(); - compressedContentStream?.Dispose(); -#pragma warning restore CA1849 // Call async methods when in an async method - httpContent.Dispose(); - } - } } // send the HTTP request and get the HTTP response @@ -529,6 +473,62 @@ private HttpContentSerializer GetHttpContentSerializer(Type objectType) => HttpServiceUtility.UsesTextSerializer(objectType) ? TextSerializer : ContentSerializer; + private sealed class CompressingHttpContent : HttpContent + { + public CompressingHttpContent(HttpContent baseContent) + { + m_baseContent = baseContent; + foreach (var header in baseContent.Headers) + Headers.TryAddWithoutValidation(header.Key, header.Value); + Headers.ContentEncoding.Add("gzip"); + } + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => + DoSerializeToStreamAsync(stream, CancellationToken.None); + +#if NET8_0_OR_GREATER + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => + DoSerializeToStreamAsync(stream, cancellationToken); +#endif + + private async Task DoSerializeToStreamAsync(Stream stream, CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER + using var baseContentStream = await m_baseContent.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); +#else + using var baseContentStream = await m_baseContent.ReadAsStreamAsync().ConfigureAwait(false); +#endif + using var gzipStream = new GZipStream(stream, CompressionLevel.Optimal, leaveOpen: true); + using var bufferedStream = new BufferedStream(gzipStream, 8192); +#if NETCOREAPP2_1_OR_GREATER + await baseContentStream.CopyToAsync(bufferedStream, cancellationToken).ConfigureAwait(false); +#else + await baseContentStream.CopyToAsync(bufferedStream, 8192, cancellationToken).ConfigureAwait(false); +#endif + } + + protected override bool TryComputeLength(out long length) + { + length = -1L; + return false; + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + m_baseContent.Dispose(); + } + finally + { + base.Dispose(disposing); + } + } + + private readonly HttpContent m_baseContent; + } + private static readonly HttpClient s_defaultHttpClient = HttpServiceUtility.CreateHttpClient(); private readonly HttpClient m_httpClient; From 442e02988a53629bdff0b611c96daca5fe59ae54 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Thu, 27 Mar 2025 14:34:44 -0700 Subject: [PATCH 5/7] Add --compress-requests command-line option to fsdgencsharp. --- src/Facility.CodeGen.CSharp/CSharpGenerator.cs | 9 +++++++++ src/Facility.CodeGen.CSharp/CSharpGeneratorSettings.cs | 5 +++++ src/fsdgencsharp/FsdGenCSharpApp.cs | 3 +++ 3 files changed, 17 insertions(+) diff --git a/src/Facility.CodeGen.CSharp/CSharpGenerator.cs b/src/Facility.CodeGen.CSharp/CSharpGenerator.cs index 2e6ffbad..4001835a 100644 --- a/src/Facility.CodeGen.CSharp/CSharpGenerator.cs +++ b/src/Facility.CodeGen.CSharp/CSharpGenerator.cs @@ -44,6 +44,11 @@ public static int GenerateCSharp(CSharpGeneratorSettings settings) => /// public bool UseNullableReferences { get; set; } + /// + /// True if the code should compress HTTP requests. + /// + public bool CompressRequests { get; set; } + /// /// True if C# names should automatically use PascalCase instead of snake case. /// @@ -132,6 +137,7 @@ public override void ApplySettings(FileGeneratorSettings settings) NamespaceName = csharpSettings.NamespaceName; DefaultNamespaceName = csharpSettings.DefaultNamespaceName; UseNullableReferences = csharpSettings.UseNullableReferences; + CompressRequests = csharpSettings.CompressRequests; FixSnakeCase = csharpSettings.FixSnakeCase; SupportMessagePack = csharpSettings.SupportMessagePack; SupportJsonSourceGeneration = csharpSettings.SupportJsonSourceGeneration; @@ -1159,6 +1165,9 @@ private CodeGenFile GenerateHttpClient(HttpServiceInfo httpServiceInfo, Context code.WriteLine($"BaseUri = new Uri({CSharpUtility.CreateString(url)}),"); WriteContentSerializerPropertyInitializer(code, fullServiceName); + + if (CompressRequests) + code.WriteLine("CompressRequests = true,"); } } } diff --git a/src/Facility.CodeGen.CSharp/CSharpGeneratorSettings.cs b/src/Facility.CodeGen.CSharp/CSharpGeneratorSettings.cs index dba66f2a..4aba018a 100644 --- a/src/Facility.CodeGen.CSharp/CSharpGeneratorSettings.cs +++ b/src/Facility.CodeGen.CSharp/CSharpGeneratorSettings.cs @@ -22,6 +22,11 @@ public sealed class CSharpGeneratorSettings : FileGeneratorSettings /// public bool UseNullableReferences { get; set; } + /// + /// True if the code should compress HTTP requests. + /// + public bool CompressRequests { get; set; } + /// /// True if C# names should automatically use PascalCase instead of snake case. /// diff --git a/src/fsdgencsharp/FsdGenCSharpApp.cs b/src/fsdgencsharp/FsdGenCSharpApp.cs index bf885b96..36bb2343 100644 --- a/src/fsdgencsharp/FsdGenCSharpApp.cs +++ b/src/fsdgencsharp/FsdGenCSharpApp.cs @@ -23,6 +23,8 @@ public sealed class FsdGenCSharpApp : CodeGeneratorApp " The namespace used by the generated C# if not specified by FSD.", " --nullable", " Use nullable reference syntax in the generated C#.", + " --compress-requests", + " Compress HTTP requests by default in the generated C#.", " --fix-snake-case", " Replace snake_case with PascalCase.", " --msgpack", @@ -43,6 +45,7 @@ protected override FileGeneratorSettings CreateSettings(ArgsReader args) => NamespaceName = args.ReadOption("namespace"), DefaultNamespaceName = args.ReadOption("default-namespace"), UseNullableReferences = args.ReadFlag("nullable"), + CompressRequests = args.ReadFlag("compress-requests"), FixSnakeCase = args.ReadFlag("fix-snake-case"), SupportMessagePack = args.ReadFlag("msgpack"), SupportJsonSourceGeneration = args.ReadFlag("json-source-gen"), From a2ca60876353d93f769b0394f2efce0aedcf9dd3 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Fri, 28 Mar 2025 09:19:32 -0700 Subject: [PATCH 6/7] Drop Content-Length header. --- src/Facility.Core/Http/HttpClientService.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Facility.Core/Http/HttpClientService.cs b/src/Facility.Core/Http/HttpClientService.cs index 99a5abd0..b32aab1a 100644 --- a/src/Facility.Core/Http/HttpClientService.cs +++ b/src/Facility.Core/Http/HttpClientService.cs @@ -479,7 +479,13 @@ public CompressingHttpContent(HttpContent baseContent) { m_baseContent = baseContent; foreach (var header in baseContent.Headers) - Headers.TryAddWithoutValidation(header.Key, header.Value); + { + // remove Content-Length header, if present, as it will change + if (!header.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase)) + Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + Headers.ContentEncoding.Clear(); Headers.ContentEncoding.Add("gzip"); } From 0ceda073a36feb9630dba73135b453996c756936 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Fri, 28 Mar 2025 09:20:16 -0700 Subject: [PATCH 7/7] Fix comments. --- src/Facility.Core/Http/HttpClientServiceSettings.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Facility.Core/Http/HttpClientServiceSettings.cs b/src/Facility.Core/Http/HttpClientServiceSettings.cs index f1f2c640..5d589e2b 100644 --- a/src/Facility.Core/Http/HttpClientServiceSettings.cs +++ b/src/Facility.Core/Http/HttpClientServiceSettings.cs @@ -31,10 +31,9 @@ public sealed class HttpClientServiceSettings public HttpContentSerializer? TextSerializer { get; set; } /// - /// True to compress HTTP request bodies (default false). + /// True to compress HTTP request bodies; false to not compress. The default is API-specific. /// - /// Request bodies will be compressed with Content-Encoding: gzip. Even when this property - /// has been set to true, the request may be sent uncompressed if compressing would make it larger. + /// Request bodies will be compressed with Content-Encoding: gzip. public bool? CompressRequests { get; set; } ///