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 85ad4b1f..91933bd2 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 ArrayBufferWriterRequestBuffer(); +#else + using var buffer = new MemoryStreamRequestBuffer(); +#endif + + 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(builder, request.Headers); + AppendHeaders(request.Headers); if (request.Content != null) { @@ -87,28 +97,31 @@ private string SerializeRequest(HttpRequestMessage request) request.Content.Headers.ContentLength = contentLength.Value; } - AppendHeaders(builder, request.Content.Headers); + AppendHeaders(request.Content.Headers); + if (!contentLength.HasValue) { // Add header for chunked mode. - builder.Append("Transfer-Encoding: chunked\r\n"); + buffer.WriteBytes("Transfer-Encoding: chunked\r\n"u8); } } + // Headers end with an empty line - builder.Append("\r\n"); - return builder.ToString(); - } + buffer.WriteBytes("\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) + 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) { - builder.Append(header.Key); - builder.Append(": "); - builder.Append(string.Join(", ", header.Value)); - builder.Append("\r\n"); + foreach (var header in headers) + { + buffer.WriteString(header.Key); + buffer.WriteBytes(": "u8); + buffer.WriteString(string.Join(", ", header.Value)); + buffer.WriteBytes("\r\n"u8); + } } } 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