From f9977f50b936c9d5d7048e90e6eaaad7ee85ba31 Mon Sep 17 00:00:00 2001 From: campersau Date: Tue, 21 Apr 2026 11:04:34 +0200 Subject: [PATCH 1/6] Perf: HttpConnection write bytes directly into a buffer --- .../HttpConnection.cs | 116 ++++++++++++++---- 1 file changed, 90 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Microsoft.Net.Http.Client/HttpConnection.cs index 85ad4b1f..af4b4620 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnection.cs @@ -16,11 +16,16 @@ public async Task SendAsync(HttpRequestMessage request, Can try { // Serialize headers & send - string rawRequest = SerializeRequest(request); - byte[] requestBytes = Encoding.ASCII.GetBytes(rawRequest); + var requestBytes = SerializeRequest(request); - await Transport.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken) +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + await Transport.WriteAsync(requestBytes, cancellationToken) .ConfigureAwait(false); +#else + var bytes = requestBytes.ToArray(); + await Transport.WriteAsync(bytes, 0, bytes.Length, cancellationToken) + .ConfigureAwait(false); +#endif if (request.Content != null) { @@ -66,17 +71,22 @@ await chunkedStream.EndContentAsync(cancellationToken) } } - private string SerializeRequest(HttpRequestMessage request) + private static ReadOnlyMemory SerializeRequest(HttpRequestMessage request) { - StringBuilder builder = new StringBuilder(); - builder.Append(request.Method); - builder.Append(' '); - builder.Append(request.GetAddressLineProperty()); - builder.Append(" HTTP/"); - builder.Append(request.Version.ToString(2)); - builder.Append("\r\n"); +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + var buffer = new ArrayBufferWriter(); +#else + var buffer = new MemoryStream(); +#endif - AppendHeaders(builder, request.Headers); + WriteString(buffer, request.Method.Method); + WriteBytes(buffer, " "u8); + WriteString(buffer, request.GetAddressLineProperty()); + WriteBytes(buffer, " HTTP/"u8); + WriteString(buffer, request.Version.ToString(2)); + WriteBytes(buffer, "\r\n"u8); + + AppendHeaders(buffer, request.Headers); if (request.Content != null) { @@ -87,29 +97,83 @@ private string SerializeRequest(HttpRequestMessage request) request.Content.Headers.ContentLength = contentLength.Value; } - AppendHeaders(builder, request.Content.Headers); + AppendHeaders(buffer, request.Content.Headers); if (!contentLength.HasValue) { // Add header for chunked mode. - builder.Append("Transfer-Encoding: chunked\r\n"); + WriteBytes(buffer, "Transfer-Encoding: chunked\r\n"u8); } } // Headers end with an empty line - builder.Append("\r\n"); - return builder.ToString(); - } + WriteBytes(buffer, "\r\n"u8); - // HttpHeaders.ToString() uses Environment.NewLine which is \n on macOS/Linux. - // RFC 9112 §2.2 requires \r\n regardless of platform, so we serialize headers explicitly. - private static void AppendHeaders(StringBuilder builder, HttpHeaders headers) - { - foreach (var header in headers) +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + return buffer.WrittenMemory; +#else + if (buffer.TryGetBuffer(out var segment)) + { + return segment; + } + return buffer.ToArray(); +#endif + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + static void AppendHeaders(ArrayBufferWriter buffer, HttpHeaders headers) +#else + static void AppendHeaders(MemoryStream buffer, HttpHeaders headers) +#endif + { + foreach (var header in headers) + { + WriteString(buffer, header.Key); + WriteBytes(buffer, ": "u8); + var first = false; + foreach (var value in header.Value) + { + if (first) + { + WriteBytes(buffer, ", "u8); + } + first = true; + + WriteString(buffer, value); + } + WriteBytes(buffer, "\r\n"u8); + } + } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + static void WriteString(ArrayBufferWriter buffer, string? str) { - builder.Append(header.Key); - builder.Append(": "); - builder.Append(string.Join(", ", header.Value)); - builder.Append("\r\n"); + if (str is null) return; + +#if NET + Encoding.ASCII.GetBytes(str, buffer); +#else + var length = Encoding.ASCII.GetByteCount(str); + var span = buffer.GetSpan(length); + var written = Encoding.ASCII.GetBytes(str, span); + buffer.Advance(written); +#endif } + static void WriteBytes(ArrayBufferWriter buffer, ReadOnlySpan span) + { + buffer.Write(span); + } +#else + static void WriteString(MemoryStream buffer, string? str) + { + if (str is null) return; + + var bytes = Encoding.ASCII.GetBytes(str); + buffer.Write(bytes, 0, bytes.Length); + } + static void WriteBytes(MemoryStream buffer, ReadOnlySpan span) + { + var bytes = span.ToArray(); + buffer.Write(bytes, 0, bytes.Length); + } +#endif } private async Task> ReadResponseLinesAsync(CancellationToken cancellationToken) From 4f8688958a1f4318479384252e4b8e6ca58b19a6 Mon Sep 17 00:00:00 2001 From: campersau Date: Tue, 21 Apr 2026 19:25:50 +0200 Subject: [PATCH 2/6] Using MemoryStream --- src/Microsoft.Net.Http.Client/HttpConnection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Microsoft.Net.Http.Client/HttpConnection.cs index af4b4620..447ad2a3 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnection.cs @@ -76,7 +76,7 @@ private static ReadOnlyMemory SerializeRequest(HttpRequestMessage request) #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER var buffer = new ArrayBufferWriter(); #else - var buffer = new MemoryStream(); + using var buffer = new MemoryStream(); #endif WriteString(buffer, request.Method.Method); From dd4d23406628c9f61096d9dc6ae2d1dd4dfe5fa7 Mon Sep 17 00:00:00 2001 From: campersau Date: Wed, 22 Apr 2026 19:48:40 +0200 Subject: [PATCH 3/6] Use abstract RequestSerializer class to reduce #if --- .../HttpConnection.cs | 113 ++--------------- .../RequestSerializer.cs | 118 ++++++++++++++++++ 2 files changed, 125 insertions(+), 106 deletions(-) create mode 100644 src/Microsoft.Net.Http.Client/RequestSerializer.cs diff --git a/src/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Microsoft.Net.Http.Client/HttpConnection.cs index 447ad2a3..1859ceb7 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnection.cs @@ -4,6 +4,12 @@ internal sealed class HttpConnection : IDisposable { private static readonly ISet DockerStreamHeaders = new HashSet { "application/vnd.docker.raw-stream", "application/vnd.docker.multiplexed-stream" }; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + private static readonly RequestSerializerArrayBufferWriter RequestSerializer = new(); +#else + private static readonly RequestSerializerMemoryStream RequestSerializer = new(); +#endif + public HttpConnection(BufferedReadStream transport) { Transport = transport; @@ -16,7 +22,7 @@ public async Task SendAsync(HttpRequestMessage request, Can try { // Serialize headers & send - var requestBytes = SerializeRequest(request); + var requestBytes = RequestSerializer.SerializeRequest(request); #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER await Transport.WriteAsync(requestBytes, cancellationToken) @@ -71,111 +77,6 @@ await chunkedStream.EndContentAsync(cancellationToken) } } - private static ReadOnlyMemory SerializeRequest(HttpRequestMessage request) - { -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER - var buffer = new ArrayBufferWriter(); -#else - using var buffer = new MemoryStream(); -#endif - - WriteString(buffer, request.Method.Method); - WriteBytes(buffer, " "u8); - WriteString(buffer, request.GetAddressLineProperty()); - WriteBytes(buffer, " HTTP/"u8); - WriteString(buffer, request.Version.ToString(2)); - WriteBytes(buffer, "\r\n"u8); - - AppendHeaders(buffer, request.Headers); - - if (request.Content != null) - { - // Force the content to compute its content length if it has not already. - var contentLength = request.Content.Headers.ContentLength; - if (contentLength.HasValue) - { - request.Content.Headers.ContentLength = contentLength.Value; - } - - AppendHeaders(buffer, request.Content.Headers); - if (!contentLength.HasValue) - { - // Add header for chunked mode. - WriteBytes(buffer, "Transfer-Encoding: chunked\r\n"u8); - } - } - // Headers end with an empty line - WriteBytes(buffer, "\r\n"u8); - -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER - return buffer.WrittenMemory; -#else - if (buffer.TryGetBuffer(out var segment)) - { - return segment; - } - return buffer.ToArray(); -#endif - -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER - static void AppendHeaders(ArrayBufferWriter buffer, HttpHeaders headers) -#else - static void AppendHeaders(MemoryStream buffer, HttpHeaders headers) -#endif - { - foreach (var header in headers) - { - WriteString(buffer, header.Key); - WriteBytes(buffer, ": "u8); - var first = false; - foreach (var value in header.Value) - { - if (first) - { - WriteBytes(buffer, ", "u8); - } - first = true; - - WriteString(buffer, value); - } - WriteBytes(buffer, "\r\n"u8); - } - } - -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER - static void WriteString(ArrayBufferWriter buffer, string? str) - { - if (str is null) return; - -#if NET - Encoding.ASCII.GetBytes(str, buffer); -#else - var length = Encoding.ASCII.GetByteCount(str); - var span = buffer.GetSpan(length); - var written = Encoding.ASCII.GetBytes(str, span); - buffer.Advance(written); -#endif - } - static void WriteBytes(ArrayBufferWriter buffer, ReadOnlySpan span) - { - buffer.Write(span); - } -#else - static void WriteString(MemoryStream buffer, string? str) - { - if (str is null) return; - - var bytes = Encoding.ASCII.GetBytes(str); - buffer.Write(bytes, 0, bytes.Length); - } - static void WriteBytes(MemoryStream buffer, ReadOnlySpan span) - { - var bytes = span.ToArray(); - buffer.Write(bytes, 0, bytes.Length); - } -#endif - } - private async Task> ReadResponseLinesAsync(CancellationToken cancellationToken) { var lines = new List(12); diff --git a/src/Microsoft.Net.Http.Client/RequestSerializer.cs b/src/Microsoft.Net.Http.Client/RequestSerializer.cs new file mode 100644 index 00000000..5ca6161e --- /dev/null +++ b/src/Microsoft.Net.Http.Client/RequestSerializer.cs @@ -0,0 +1,118 @@ +namespace Microsoft.Net.Http.Client; + +internal abstract class RequestSerializer +{ + public abstract ReadOnlyMemory SerializeRequest(HttpRequestMessage request); + + protected void SerializeRequest(T buffer, HttpRequestMessage request) + { + WriteString(buffer, request.Method.Method); + WriteBytes(buffer, " "u8); + WriteString(buffer, request.GetAddressLineProperty()); + WriteBytes(buffer, " HTTP/"u8); + WriteString(buffer, request.Version.ToString(2)); + WriteBytes(buffer, "\r\n"u8); + + AppendHeaders(buffer, request.Headers); + + if (request.Content != null) + { + // Force the content to compute its content length if it has not already. + var contentLength = request.Content.Headers.ContentLength; + if (contentLength.HasValue) + { + request.Content.Headers.ContentLength = contentLength.Value; + } + + AppendHeaders(buffer, request.Content.Headers); + if (!contentLength.HasValue) + { + // Add header for chunked mode. + WriteBytes(buffer, "Transfer-Encoding: chunked\r\n"u8); + } + } + // Headers end with an empty line + WriteBytes(buffer, "\r\n"u8); + } + + protected void AppendHeaders(T buffer, HttpHeaders headers) + { + foreach (var header in headers) + { + WriteString(buffer, header.Key); + WriteBytes(buffer, ": "u8); + var first = false; + foreach (var value in header.Value) + { + if (first) + { + WriteBytes(buffer, ", "u8); + } + first = true; + + WriteString(buffer, value); + } + WriteBytes(buffer, "\r\n"u8); + } + } + + protected abstract void WriteString(T buffer, string? str); + protected abstract void WriteBytes(T buffer, ReadOnlySpan span); +} + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER +internal sealed class RequestSerializerArrayBufferWriter : RequestSerializer> +{ + public override ReadOnlyMemory SerializeRequest(HttpRequestMessage request) + { + var buffer = new ArrayBufferWriter(); + SerializeRequest(buffer, request); + return buffer.WrittenMemory; + } + + protected override void WriteString(ArrayBufferWriter buffer, string? str) + { + if (str is null) return; +#if NET + Encoding.ASCII.GetBytes(str, buffer); +#else + var length = Encoding.ASCII.GetByteCount(str); + var span = buffer.GetSpan(length); + var written = Encoding.ASCII.GetBytes(str, span); + buffer.Advance(written); +#endif + } + + protected override void WriteBytes(ArrayBufferWriter buffer, ReadOnlySpan span) + { + buffer.Write(span); + } +} +#else +internal sealed class RequestSerializerMemoryStream : RequestSerializer +{ + public override ReadOnlyMemory SerializeRequest(HttpRequestMessage request) + { + using var buffer = new MemoryStream(); + SerializeRequest(buffer, request); + if (buffer.TryGetBuffer(out var segment)) + { + return segment; + } + return buffer.ToArray(); + } + + protected override void WriteString(MemoryStream buffer, string? str) + { + if (str is null) return; + var bytes = Encoding.ASCII.GetBytes(str); + buffer.Write(bytes, 0, bytes.Length); + } + + protected override void WriteBytes(MemoryStream buffer, ReadOnlySpan span) + { + var bytes = span.ToArray(); + buffer.Write(bytes, 0, bytes.Length); + } +} +#endif From ab74fa9f54d0a737ac236e365167b8604bab22f2 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:22:55 +0200 Subject: [PATCH 4/6] Revert "Use abstract RequestSerializer class to reduce #if" This reverts commit dd4d23406628c9f61096d9dc6ae2d1dd4dfe5fa7. --- .../HttpConnection.cs | 113 +++++++++++++++-- .../RequestSerializer.cs | 118 ------------------ 2 files changed, 106 insertions(+), 125 deletions(-) delete mode 100644 src/Microsoft.Net.Http.Client/RequestSerializer.cs diff --git a/src/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Microsoft.Net.Http.Client/HttpConnection.cs index 1859ceb7..447ad2a3 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnection.cs @@ -4,12 +4,6 @@ internal sealed class HttpConnection : IDisposable { private static readonly ISet DockerStreamHeaders = new HashSet { "application/vnd.docker.raw-stream", "application/vnd.docker.multiplexed-stream" }; -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER - private static readonly RequestSerializerArrayBufferWriter RequestSerializer = new(); -#else - private static readonly RequestSerializerMemoryStream RequestSerializer = new(); -#endif - public HttpConnection(BufferedReadStream transport) { Transport = transport; @@ -22,7 +16,7 @@ public async Task SendAsync(HttpRequestMessage request, Can try { // Serialize headers & send - var requestBytes = RequestSerializer.SerializeRequest(request); + var requestBytes = SerializeRequest(request); #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER await Transport.WriteAsync(requestBytes, cancellationToken) @@ -77,6 +71,111 @@ await chunkedStream.EndContentAsync(cancellationToken) } } + private static ReadOnlyMemory SerializeRequest(HttpRequestMessage request) + { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + var buffer = new ArrayBufferWriter(); +#else + using var buffer = new MemoryStream(); +#endif + + WriteString(buffer, request.Method.Method); + WriteBytes(buffer, " "u8); + WriteString(buffer, request.GetAddressLineProperty()); + WriteBytes(buffer, " HTTP/"u8); + WriteString(buffer, request.Version.ToString(2)); + WriteBytes(buffer, "\r\n"u8); + + AppendHeaders(buffer, request.Headers); + + if (request.Content != null) + { + // Force the content to compute its content length if it has not already. + var contentLength = request.Content.Headers.ContentLength; + if (contentLength.HasValue) + { + request.Content.Headers.ContentLength = contentLength.Value; + } + + AppendHeaders(buffer, request.Content.Headers); + if (!contentLength.HasValue) + { + // Add header for chunked mode. + WriteBytes(buffer, "Transfer-Encoding: chunked\r\n"u8); + } + } + // Headers end with an empty line + WriteBytes(buffer, "\r\n"u8); + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + return buffer.WrittenMemory; +#else + if (buffer.TryGetBuffer(out var segment)) + { + return segment; + } + return buffer.ToArray(); +#endif + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + static void AppendHeaders(ArrayBufferWriter buffer, HttpHeaders headers) +#else + static void AppendHeaders(MemoryStream buffer, HttpHeaders headers) +#endif + { + foreach (var header in headers) + { + WriteString(buffer, header.Key); + WriteBytes(buffer, ": "u8); + var first = false; + foreach (var value in header.Value) + { + if (first) + { + WriteBytes(buffer, ", "u8); + } + first = true; + + WriteString(buffer, value); + } + WriteBytes(buffer, "\r\n"u8); + } + } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + static void WriteString(ArrayBufferWriter buffer, string? str) + { + if (str is null) return; + +#if NET + Encoding.ASCII.GetBytes(str, buffer); +#else + var length = Encoding.ASCII.GetByteCount(str); + var span = buffer.GetSpan(length); + var written = Encoding.ASCII.GetBytes(str, span); + buffer.Advance(written); +#endif + } + static void WriteBytes(ArrayBufferWriter buffer, ReadOnlySpan span) + { + buffer.Write(span); + } +#else + static void WriteString(MemoryStream buffer, string? str) + { + if (str is null) return; + + var bytes = Encoding.ASCII.GetBytes(str); + buffer.Write(bytes, 0, bytes.Length); + } + static void WriteBytes(MemoryStream buffer, ReadOnlySpan span) + { + var bytes = span.ToArray(); + buffer.Write(bytes, 0, bytes.Length); + } +#endif + } + private async Task> ReadResponseLinesAsync(CancellationToken cancellationToken) { var lines = new List(12); diff --git a/src/Microsoft.Net.Http.Client/RequestSerializer.cs b/src/Microsoft.Net.Http.Client/RequestSerializer.cs deleted file mode 100644 index 5ca6161e..00000000 --- a/src/Microsoft.Net.Http.Client/RequestSerializer.cs +++ /dev/null @@ -1,118 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -internal abstract class RequestSerializer -{ - public abstract ReadOnlyMemory SerializeRequest(HttpRequestMessage request); - - protected void SerializeRequest(T buffer, HttpRequestMessage request) - { - WriteString(buffer, request.Method.Method); - WriteBytes(buffer, " "u8); - WriteString(buffer, request.GetAddressLineProperty()); - WriteBytes(buffer, " HTTP/"u8); - WriteString(buffer, request.Version.ToString(2)); - WriteBytes(buffer, "\r\n"u8); - - AppendHeaders(buffer, request.Headers); - - if (request.Content != null) - { - // Force the content to compute its content length if it has not already. - var contentLength = request.Content.Headers.ContentLength; - if (contentLength.HasValue) - { - request.Content.Headers.ContentLength = contentLength.Value; - } - - AppendHeaders(buffer, request.Content.Headers); - if (!contentLength.HasValue) - { - // Add header for chunked mode. - WriteBytes(buffer, "Transfer-Encoding: chunked\r\n"u8); - } - } - // Headers end with an empty line - WriteBytes(buffer, "\r\n"u8); - } - - protected void AppendHeaders(T buffer, HttpHeaders headers) - { - foreach (var header in headers) - { - WriteString(buffer, header.Key); - WriteBytes(buffer, ": "u8); - var first = false; - foreach (var value in header.Value) - { - if (first) - { - WriteBytes(buffer, ", "u8); - } - first = true; - - WriteString(buffer, value); - } - WriteBytes(buffer, "\r\n"u8); - } - } - - protected abstract void WriteString(T buffer, string? str); - protected abstract void WriteBytes(T buffer, ReadOnlySpan span); -} - -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER -internal sealed class RequestSerializerArrayBufferWriter : RequestSerializer> -{ - public override ReadOnlyMemory SerializeRequest(HttpRequestMessage request) - { - var buffer = new ArrayBufferWriter(); - SerializeRequest(buffer, request); - return buffer.WrittenMemory; - } - - protected override void WriteString(ArrayBufferWriter buffer, string? str) - { - if (str is null) return; -#if NET - Encoding.ASCII.GetBytes(str, buffer); -#else - var length = Encoding.ASCII.GetByteCount(str); - var span = buffer.GetSpan(length); - var written = Encoding.ASCII.GetBytes(str, span); - buffer.Advance(written); -#endif - } - - protected override void WriteBytes(ArrayBufferWriter buffer, ReadOnlySpan span) - { - buffer.Write(span); - } -} -#else -internal sealed class RequestSerializerMemoryStream : RequestSerializer -{ - public override ReadOnlyMemory SerializeRequest(HttpRequestMessage request) - { - using var buffer = new MemoryStream(); - SerializeRequest(buffer, request); - if (buffer.TryGetBuffer(out var segment)) - { - return segment; - } - return buffer.ToArray(); - } - - protected override void WriteString(MemoryStream buffer, string? str) - { - if (str is null) return; - var bytes = Encoding.ASCII.GetBytes(str); - buffer.Write(bytes, 0, bytes.Length); - } - - protected override void WriteBytes(MemoryStream buffer, ReadOnlySpan span) - { - var bytes = span.ToArray(); - buffer.Write(bytes, 0, bytes.Length); - } -} -#endif From a26d05b2382193e4b447d677941fcc4aa7843185 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Wed, 22 Apr 2026 21:06:07 +0200 Subject: [PATCH 5/6] chore: move write to seperate classes --- .../ArrayBufferWriterRequestBuffer.cs | 35 ++++++++ .../HttpConnection.cs | 89 ++++++------------- .../MemoryStreamRequestBuffer.cs | 45 ++++++++++ 3 files changed, 105 insertions(+), 64 deletions(-) create mode 100644 src/Microsoft.Net.Http.Client/ArrayBufferWriterRequestBuffer.cs create mode 100644 src/Microsoft.Net.Http.Client/MemoryStreamRequestBuffer.cs diff --git a/src/Microsoft.Net.Http.Client/ArrayBufferWriterRequestBuffer.cs b/src/Microsoft.Net.Http.Client/ArrayBufferWriterRequestBuffer.cs new file mode 100644 index 00000000..e7b2b3b6 --- /dev/null +++ b/src/Microsoft.Net.Http.Client/ArrayBufferWriterRequestBuffer.cs @@ -0,0 +1,35 @@ +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER +namespace Microsoft.Net.Http.Client; + +internal sealed class ArrayBufferWriterRequestBuffer +{ + private readonly ArrayBufferWriter _buffer = new(); + + public ReadOnlyMemory GetWrittenMemory() + { + return _buffer.WrittenMemory; + } + + public void WriteString(string? value) + { + if (string.IsNullOrEmpty(value)) + { + return; + } + +#if NET + Encoding.ASCII.GetBytes(value, _buffer); +#else + var length = Encoding.ASCII.GetByteCount(value); + var bytes = _buffer.GetSpan(length); + var written = Encoding.ASCII.GetBytes(value, bytes); + _buffer.Advance(written); +#endif + } + + public void WriteBytes(ReadOnlySpan value) + { + _buffer.Write(value); + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Microsoft.Net.Http.Client/HttpConnection.cs index 447ad2a3..43ce9875 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnection.cs @@ -74,19 +74,19 @@ await chunkedStream.EndContentAsync(cancellationToken) private static ReadOnlyMemory SerializeRequest(HttpRequestMessage request) { #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER - var buffer = new ArrayBufferWriter(); + var buffer = new ArrayBufferWriterRequestBuffer(); #else - using var buffer = new MemoryStream(); + using var buffer = new MemoryStreamRequestBuffer(); #endif - WriteString(buffer, request.Method.Method); - WriteBytes(buffer, " "u8); - WriteString(buffer, request.GetAddressLineProperty()); - WriteBytes(buffer, " HTTP/"u8); - WriteString(buffer, request.Version.ToString(2)); - WriteBytes(buffer, "\r\n"u8); + buffer.WriteString(request.Method.Method); + buffer.WriteBytes(" "u8); + buffer.WriteString(request.GetAddressLineProperty()); + buffer.WriteBytes(" HTTP/"u8); + buffer.WriteString(request.Version.ToString(2)); + buffer.WriteBytes("\r\n"u8); - AppendHeaders(buffer, request.Headers); + AppendHeaders(request.Headers); if (request.Content != null) { @@ -97,83 +97,44 @@ private static ReadOnlyMemory SerializeRequest(HttpRequestMessage request) request.Content.Headers.ContentLength = contentLength.Value; } - AppendHeaders(buffer, request.Content.Headers); + AppendHeaders(request.Content.Headers); + if (!contentLength.HasValue) { // Add header for chunked mode. - WriteBytes(buffer, "Transfer-Encoding: chunked\r\n"u8); + buffer.WriteBytes("Transfer-Encoding: chunked\r\n"u8); } } + // Headers end with an empty line - WriteBytes(buffer, "\r\n"u8); + buffer.WriteBytes("\r\n"u8); -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER - return buffer.WrittenMemory; -#else - if (buffer.TryGetBuffer(out var segment)) - { - return segment; - } - return buffer.ToArray(); -#endif + return buffer.GetWrittenMemory(); -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER - static void AppendHeaders(ArrayBufferWriter buffer, HttpHeaders headers) -#else - static void AppendHeaders(MemoryStream buffer, HttpHeaders headers) -#endif + void AppendHeaders(HttpHeaders headers) { foreach (var header in headers) { - WriteString(buffer, header.Key); - WriteBytes(buffer, ": "u8); var first = false; + + buffer.WriteString(header.Key); + buffer.WriteBytes(": "u8); + foreach (var value in header.Value) { if (first) { - WriteBytes(buffer, ", "u8); + buffer.WriteBytes(", "u8); } + first = true; - WriteString(buffer, value); + buffer.WriteString(value); } - WriteBytes(buffer, "\r\n"u8); - } - } -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER - static void WriteString(ArrayBufferWriter buffer, string? str) - { - if (str is null) return; - -#if NET - Encoding.ASCII.GetBytes(str, buffer); -#else - var length = Encoding.ASCII.GetByteCount(str); - var span = buffer.GetSpan(length); - var written = Encoding.ASCII.GetBytes(str, span); - buffer.Advance(written); -#endif - } - static void WriteBytes(ArrayBufferWriter buffer, ReadOnlySpan span) - { - buffer.Write(span); - } -#else - static void WriteString(MemoryStream buffer, string? str) - { - if (str is null) return; - - var bytes = Encoding.ASCII.GetBytes(str); - buffer.Write(bytes, 0, bytes.Length); - } - static void WriteBytes(MemoryStream buffer, ReadOnlySpan span) - { - var bytes = span.ToArray(); - buffer.Write(bytes, 0, bytes.Length); + buffer.WriteBytes("\r\n"u8); + } } -#endif } private async Task> ReadResponseLinesAsync(CancellationToken cancellationToken) diff --git a/src/Microsoft.Net.Http.Client/MemoryStreamRequestBuffer.cs b/src/Microsoft.Net.Http.Client/MemoryStreamRequestBuffer.cs new file mode 100644 index 00000000..c3843917 --- /dev/null +++ b/src/Microsoft.Net.Http.Client/MemoryStreamRequestBuffer.cs @@ -0,0 +1,45 @@ +#if !NETSTANDARD2_1_OR_GREATER && !NETCOREAPP2_1_OR_GREATER +namespace Microsoft.Net.Http.Client; + +internal sealed class MemoryStreamRequestBuffer : IDisposable +{ + private readonly MemoryStream _buffer = new(); + + public void Dispose() + { + _buffer.Dispose(); + } + + public ReadOnlyMemory GetWrittenMemory() + { + if (_buffer.TryGetBuffer(out var buffer)) + { + return buffer; + } + + return _buffer.ToArray(); + } + + public void WriteString(string? value) + { + if (string.IsNullOrEmpty(value)) + { + return; + } + + var bytes = Encoding.ASCII.GetBytes(value); + _buffer.Write(bytes, 0, bytes.Length); + } + + public void WriteBytes(ReadOnlySpan value) + { + if (value.Length == 0) + { + return; + } + + var buffer = value.ToArray(); + _buffer.Write(buffer, 0, buffer.Length); + } +} +#endif \ No newline at end of file From e946af6540594c99989c7d0d58eb9e101210ed34 Mon Sep 17 00:00:00 2001 From: campersau Date: Thu, 23 Apr 2026 09:08:29 +0200 Subject: [PATCH 6/6] prefer readable code over optimized one --- .../HttpConnection.cs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Microsoft.Net.Http.Client/HttpConnection.cs index 43ce9875..91933bd2 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnection.cs @@ -111,27 +111,15 @@ private static ReadOnlyMemory SerializeRequest(HttpRequestMessage request) return buffer.GetWrittenMemory(); + // HttpHeaders.ToString() uses Environment.NewLine which is \n on macOS/Linux. + // RFC 9112 §2.2 requires \r\n regardless of platform, so we serialize headers explicitly. void AppendHeaders(HttpHeaders headers) { foreach (var header in headers) { - var first = false; - buffer.WriteString(header.Key); buffer.WriteBytes(": "u8); - - foreach (var value in header.Value) - { - if (first) - { - buffer.WriteBytes(", "u8); - } - - first = true; - - buffer.WriteString(value); - } - + buffer.WriteString(string.Join(", ", header.Value)); buffer.WriteBytes("\r\n"u8); } }