Skip to content

Commit 40bd270

Browse files
liveansantonfirsovMihaZupanManickaP
authored
Implement HttpWebRequest AllowWriteStreamBuffering property (#95001)
* Implement HttpWebRequest WriteStreamBuffering * Fix tests * Fix tests * Review feedback * Fix timeouts * fix test build * Hang test * Fix build * fix tests * delete unnecessary line * fix tests * refactor test * Dispose http client * Fix buffer write in RequestStream.Flush() method * Fix same length bug in FlushAsync * Update ContentLength in HttpWebRequestTest.cs * Fix flushing and ending request stream * Fix flushing and ending request stream * Fix FlushAsync method to handle cancellation * Update src/libraries/System.Net.Requests/src/System/Net/HttpClientContentStream.cs Co-authored-by: Anton Firszov <antonfir@gmail.com> * Review feedback * Bound streamBuffer lifecycle to HttpClientContentStream * Review feedback * Review feedback * Change ??= to = * change delay on test * Apply suggestions from code review Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com> * Fix build * Review feedback * Apply suggestions from code review Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com> * Review feedback * Apply suggestions from code review Co-authored-by: Marie Píchová <11718369+ManickaP@users.noreply.github.com> * Add ProtocolViolationException if we're not buffering and we didn't set either SendChunked or ContentLength * Review feedback * Add test for not buffering and sending the content before we call `GetResponse` * Update src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs Co-authored-by: Marie Píchová <11718369+ManickaP@users.noreply.github.com> * Remove exception * Review feedback * Fix string resources * Add RequestStreamContent class for handling request stream serialization * Seperate Buffering and Non-Buffering Streams and Connect non-buffering stream to HttpClient Internal Stream directly * Remove unnecessary code in HttpWebRequestTest * Use random data for testing * Update src/libraries/System.Net.Requests/src/System/Net/RequestBufferingStream.cs Co-authored-by: Anton Firszov <antonfir@gmail.com> * Remove default flushes and add flush on test * Review feedback * Removing unused code --------- Co-authored-by: Anton Firszov <antonfir@gmail.com> Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com> Co-authored-by: Marie Píchová <11718369+ManickaP@users.noreply.github.com>
1 parent 24820b6 commit 40bd270

File tree

6 files changed

+466
-103
lines changed

6 files changed

+466
-103
lines changed

src/libraries/System.Net.Requests/src/System.Net.Requests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<Compile Include="System\Net\IWebRequestCreate.cs" />
3030
<Compile Include="System\Net\ProtocolViolationException.cs" />
3131
<Compile Include="System\Net\RequestStream.cs" />
32+
<Compile Include="System\Net\RequestBufferingStream.cs" />
3233
<Compile Include="System\Net\TaskExtensions.cs" />
3334
<Compile Include="System\Net\WebException.cs" />
3435
<Compile Include="System\Net\WebExceptionStatus.cs" />
@@ -48,6 +49,7 @@
4849
<Compile Include="System\Net\NetRes.cs" />
4950
<Compile Include="System\Net\NetworkStreamWrapper.cs" />
5051
<Compile Include="System\Net\TimerThread.cs" />
52+
<Compile Include="System\Net\RequestStreamContent.cs" />
5153
<Compile Include="System\Net\Cache\HttpCacheAgeControl.cs" />
5254
<Compile Include="System\Net\Cache\HttpRequestCacheLevel.cs" />
5355
<Compile Include="System\Net\Cache\HttpRequestCachePolicy.cs" />

src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs

Lines changed: 112 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Generic;
45
using System.ComponentModel;
56
using System.Diagnostics;
67
using System.Diagnostics.CodeAnalysis;
@@ -12,6 +13,7 @@
1213
using System.Net.Http.Headers;
1314
using System.Net.Security;
1415
using System.Net.Sockets;
16+
using System.Runtime.CompilerServices;
1517
using System.Runtime.Serialization;
1618
using System.Security.Authentication;
1719
using System.Security.Cryptography.X509Certificates;
@@ -40,6 +42,7 @@ public class HttpWebRequest : WebRequest, ISerializable
4042
private IWebProxy? _proxy = WebRequest.DefaultWebProxy;
4143

4244
private Task<HttpResponseMessage>? _sendRequestTask;
45+
private HttpRequestMessage? _sendRequestMessage;
4346

4447
private static int _defaultMaxResponseHeadersLength = HttpHandlerDefaults.DefaultMaxResponseHeadersLength;
4548
private static int _defaultMaximumErrorResponseLength = -1;
@@ -62,7 +65,7 @@ public class HttpWebRequest : WebRequest, ISerializable
6265
private bool _hostHasPort;
6366
private Uri? _hostUri;
6467

65-
private RequestStream? _requestStream;
68+
private Stream? _requestStream;
6669
private TaskCompletionSource<Stream>? _requestStreamOperation;
6770
private TaskCompletionSource<WebResponse>? _responseOperation;
6871
private AsyncCallback? _requestStreamCallback;
@@ -78,6 +81,8 @@ public class HttpWebRequest : WebRequest, ISerializable
7881
private static readonly object s_syncRoot = new object();
7982
private static volatile HttpClient? s_cachedHttpClient;
8083
private static HttpClientParameters? s_cachedHttpClientParameters;
84+
private bool _disposeRequired;
85+
private HttpClient? _httpClient;
8186

8287
//these should be safe.
8388
[Flags]
@@ -1003,17 +1008,17 @@ public override void Abort()
10031008
{
10041009
_responseCallback(_responseOperation.Task);
10051010
}
1006-
1007-
// Cancel the underlying send operation.
1008-
Debug.Assert(_sendRequestCts != null);
1009-
_sendRequestCts.Cancel();
10101011
}
1011-
else if (_requestStreamOperation != null)
1012+
if (_requestStreamOperation != null)
10121013
{
10131014
if (_requestStreamOperation.TrySetCanceled() && _requestStreamCallback != null)
10141015
{
10151016
_requestStreamCallback(_requestStreamOperation.Task);
10161017
}
1018+
1019+
// Cancel the underlying send operation.
1020+
Debug.Assert(_sendRequestCts != null);
1021+
_sendRequestCts.Cancel();
10171022
}
10181023
}
10191024

@@ -1041,8 +1046,7 @@ public override WebResponse GetResponse()
10411046
{
10421047
try
10431048
{
1044-
_sendRequestCts = new CancellationTokenSource();
1045-
return SendRequest(async: false).GetAwaiter().GetResult();
1049+
return HandleResponse(async: false).GetAwaiter().GetResult();
10461050
}
10471051
catch (Exception ex)
10481052
{
@@ -1052,10 +1056,11 @@ public override WebResponse GetResponse()
10521056

10531057
public override Stream GetRequestStream()
10541058
{
1059+
CheckRequestStream();
10551060
return InternalGetRequestStream().Result;
10561061
}
10571062

1058-
private Task<Stream> InternalGetRequestStream()
1063+
private void CheckRequestStream()
10591064
{
10601065
CheckAbort();
10611066

@@ -1073,10 +1078,28 @@ private Task<Stream> InternalGetRequestStream()
10731078
{
10741079
throw new InvalidOperationException(SR.net_reqsubmitted);
10751080
}
1081+
}
10761082

1077-
_requestStream = new RequestStream();
1083+
private async Task<Stream> InternalGetRequestStream()
1084+
{
1085+
// If we aren't buffering we need to open the connection right away.
1086+
// Because we need to send the data as soon as possible when it's available from the RequestStream.
1087+
// Making this allows us to keep the sync send request path for buffering cases.
1088+
if (AllowWriteStreamBuffering is false)
1089+
{
1090+
// We're calling SendRequest with async, because we need to open the connection and send the request
1091+
// Otherwise, sync path will block the current thread until the request is sent.
1092+
TaskCompletionSource<Stream> getStreamTcs = new();
1093+
TaskCompletionSource completeTcs = new();
1094+
_sendRequestTask = SendRequest(async: true, new RequestStreamContent(getStreamTcs, completeTcs));
1095+
_requestStream = new RequestStream(await getStreamTcs.Task.ConfigureAwait(false), completeTcs);
1096+
}
1097+
else
1098+
{
1099+
_requestStream = new RequestBufferingStream();
1100+
}
10781101

1079-
return Task.FromResult((Stream)_requestStream);
1102+
return _requestStream;
10801103
}
10811104

10821105
public Stream EndGetRequestStream(IAsyncResult asyncResult, out TransportContext? context)
@@ -1100,6 +1123,8 @@ public override IAsyncResult BeginGetRequestStream(AsyncCallback? callback, obje
11001123
throw new InvalidOperationException(SR.net_repcall);
11011124
}
11021125

1126+
CheckRequestStream();
1127+
11031128
_requestStreamCallback = callback;
11041129
_requestStreamOperation = InternalGetRequestStream().ToApm(callback, state);
11051130

@@ -1133,78 +1158,95 @@ public override Stream EndGetRequestStream(IAsyncResult asyncResult)
11331158
return stream;
11341159
}
11351160

1136-
private async Task<WebResponse> SendRequest(bool async)
1161+
private Task<HttpResponseMessage> SendRequest(bool async, HttpContent? content = null)
11371162
{
11381163
if (RequestSubmitted)
11391164
{
11401165
throw new InvalidOperationException(SR.net_reqsubmitted);
11411166
}
11421167

1143-
var request = new HttpRequestMessage(HttpMethod.Parse(_originVerb), _requestUri);
1168+
_sendRequestMessage = new HttpRequestMessage(HttpMethod.Parse(_originVerb), _requestUri);
1169+
_sendRequestCts = new CancellationTokenSource();
1170+
_httpClient = GetCachedOrCreateHttpClient(async, out _disposeRequired);
11441171

1145-
bool disposeRequired = false;
1146-
HttpClient? client = null;
1147-
try
1172+
if (content is not null)
1173+
{
1174+
_sendRequestMessage.Content = content;
1175+
}
1176+
1177+
if (_hostUri is not null)
11481178
{
1149-
client = GetCachedOrCreateHttpClient(async, out disposeRequired);
1150-
if (_requestStream != null)
1179+
_sendRequestMessage.Headers.Host = Host;
1180+
}
1181+
1182+
AddCacheControlHeaders(_sendRequestMessage);
1183+
1184+
// Copy the HttpWebRequest request headers from the WebHeaderCollection into HttpRequestMessage.Headers and
1185+
// HttpRequestMessage.Content.Headers.
1186+
foreach (string headerName in _webHeaderCollection)
1187+
{
1188+
// The System.Net.Http APIs require HttpRequestMessage headers to be properly divided between the request headers
1189+
// collection and the request content headers collection for all well-known header names. And custom headers
1190+
// are only allowed in the request headers collection and not in the request content headers collection.
1191+
if (IsWellKnownContentHeader(headerName))
11511192
{
1152-
ArraySegment<byte> bytes = _requestStream.GetBuffer();
1153-
request.Content = new ByteArrayContent(bytes.Array!, bytes.Offset, bytes.Count);
1193+
_sendRequestMessage.Content ??= new ByteArrayContent(Array.Empty<byte>());
1194+
_sendRequestMessage.Content.Headers.TryAddWithoutValidation(headerName, _webHeaderCollection[headerName!]);
11541195
}
1155-
1156-
if (_hostUri != null)
1196+
else
11571197
{
1158-
request.Headers.Host = Host;
1198+
_sendRequestMessage.Headers.TryAddWithoutValidation(headerName, _webHeaderCollection[headerName!]);
11591199
}
1200+
}
11601201

1161-
AddCacheControlHeaders(request);
1202+
if (_servicePoint?.Expect100Continue == true)
1203+
{
1204+
_sendRequestMessage.Headers.ExpectContinue = true;
1205+
}
11621206

1163-
// Copy the HttpWebRequest request headers from the WebHeaderCollection into HttpRequestMessage.Headers and
1164-
// HttpRequestMessage.Content.Headers.
1165-
foreach (string headerName in _webHeaderCollection)
1166-
{
1167-
// The System.Net.Http APIs require HttpRequestMessage headers to be properly divided between the request headers
1168-
// collection and the request content headers collection for all well-known header names. And custom headers
1169-
// are only allowed in the request headers collection and not in the request content headers collection.
1170-
if (IsWellKnownContentHeader(headerName))
1171-
{
1172-
// Create empty content so that we can send the entity-body header.
1173-
request.Content ??= new ByteArrayContent(Array.Empty<byte>());
1207+
_sendRequestMessage.Headers.TransferEncodingChunked = SendChunked;
11741208

1175-
request.Content.Headers.TryAddWithoutValidation(headerName, _webHeaderCollection[headerName!]);
1176-
}
1177-
else
1178-
{
1179-
request.Headers.TryAddWithoutValidation(headerName, _webHeaderCollection[headerName!]);
1180-
}
1181-
}
1209+
if (KeepAlive)
1210+
{
1211+
_sendRequestMessage.Headers.Connection.Add(HttpKnownHeaderNames.KeepAlive);
1212+
}
1213+
else
1214+
{
1215+
_sendRequestMessage.Headers.ConnectionClose = true;
1216+
}
11821217

1183-
request.Headers.TransferEncodingChunked = SendChunked;
1218+
_sendRequestMessage.Version = ProtocolVersion;
1219+
HttpCompletionOption completionOption = _allowReadStreamBuffering ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead;
1220+
// If we're not buffering, there is no way to open the connection and not send the request without async.
1221+
// So we should use Async, if we're not buffering.
1222+
_sendRequestTask = async || !AllowWriteStreamBuffering ?
1223+
_httpClient.SendAsync(_sendRequestMessage, completionOption, _sendRequestCts.Token) :
1224+
Task.FromResult(_httpClient.Send(_sendRequestMessage, completionOption, _sendRequestCts.Token));
11841225

1185-
if (KeepAlive)
1186-
{
1187-
request.Headers.Connection.Add(HttpKnownHeaderNames.KeepAlive);
1188-
}
1189-
else
1190-
{
1191-
request.Headers.ConnectionClose = true;
1192-
}
1226+
return _sendRequestTask!;
1227+
}
11931228

1194-
if (_servicePoint?.Expect100Continue == true)
1195-
{
1196-
request.Headers.ExpectContinue = true;
1197-
}
1229+
private async Task<WebResponse> HandleResponse(bool async)
1230+
{
1231+
// If user code used requestStream and didn't dispose it
1232+
// We're completing it here.
1233+
if (_requestStream is RequestStream requestStream)
1234+
{
1235+
requestStream.Complete();
1236+
}
11981237

1199-
request.Version = ProtocolVersion;
1238+
if (_sendRequestTask is null && _requestStream is RequestBufferingStream requestBufferingStream)
1239+
{
1240+
ArraySegment<byte> buffer = requestBufferingStream.GetBuffer();
1241+
_sendRequestTask = SendRequest(async, new ByteArrayContent(buffer.Array!, buffer.Offset, buffer.Count));
1242+
}
12001243

1201-
_sendRequestTask = async ?
1202-
client.SendAsync(request, _allowReadStreamBuffering ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead, _sendRequestCts!.Token) :
1203-
Task.FromResult(client.Send(request, _allowReadStreamBuffering ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead, _sendRequestCts!.Token));
1244+
_sendRequestTask ??= SendRequest(async);
12041245

1246+
try
1247+
{
12051248
HttpResponseMessage responseMessage = await _sendRequestTask.ConfigureAwait(false);
1206-
1207-
HttpWebResponse response = new HttpWebResponse(responseMessage, _requestUri, _cookieContainer);
1249+
HttpWebResponse response = new(responseMessage, _requestUri, _cookieContainer);
12081250

12091251
int maxSuccessStatusCode = AllowAutoRedirect ? 299 : 399;
12101252
if ((int)response.StatusCode > maxSuccessStatusCode || (int)response.StatusCode < 200)
@@ -1220,9 +1262,15 @@ private async Task<WebResponse> SendRequest(bool async)
12201262
}
12211263
finally
12221264
{
1223-
if (disposeRequired)
1265+
_sendRequestMessage?.Dispose();
1266+
if (_requestStream is RequestBufferingStream bufferStream)
12241267
{
1225-
client?.Dispose();
1268+
bufferStream.GetMemoryStream().Dispose();
1269+
}
1270+
1271+
if (_disposeRequired)
1272+
{
1273+
_httpClient?.Dispose();
12261274
}
12271275
}
12281276
}
@@ -1348,9 +1396,8 @@ public override IAsyncResult BeginGetResponse(AsyncCallback? callback, object? s
13481396
throw new InvalidOperationException(SR.net_repcall);
13491397
}
13501398

1351-
_sendRequestCts = new CancellationTokenSource();
13521399
_responseCallback = callback;
1353-
_responseOperation = SendRequest(async: true).ToApm(callback, state);
1400+
_responseOperation = HandleResponse(async: true).ToApm(callback, state);
13541401

13551402
return _responseOperation.Task;
13561403
}

0 commit comments

Comments
 (0)