Skip to content
This repository was archived by the owner on Mar 19, 2019. It is now read-only.

Commit bbc7620

Browse files
committed
#366 Add flag to disable synchronous IO
1 parent 7febdba commit bbc7620

20 files changed

+155
-44
lines changed

src/Microsoft.AspNetCore.Server.HttpSys/FeatureContext.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ internal class FeatureContext :
2929
IHttpAuthenticationFeature,
3030
IHttpUpgradeFeature,
3131
IHttpRequestIdentifierFeature,
32-
IHttpMaxRequestBodySizeFeature
32+
IHttpMaxRequestBodySizeFeature,
33+
IHttpBodyControlFeature
3334
{
3435
private RequestContext _requestContext;
3536
private IFeatureCollection _features;
@@ -464,6 +465,12 @@ string IHttpRequestIdentifierFeature.TraceIdentifier
464465
}
465466
}
466467

468+
bool IHttpBodyControlFeature.AllowSynchronousIO
469+
{
470+
get => _requestContext.AllowSynchronousIO;
471+
set => _requestContext.AllowSynchronousIO = value;
472+
}
473+
467474
bool IHttpMaxRequestBodySizeFeature.IsReadOnly => Request.HasRequestBodyStarted;
468475

469476
long? IHttpMaxRequestBodySizeFeature.MaxRequestBodySize

src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ public long? MaxRequestBodySize
131131
}
132132
}
133133

134+
/// <summary>
135+
/// Gets or sets a value that controls whether synchronous IO is allowed for the HttpContext.Request.Body and HttpContext.Request.Body.
136+
/// The default is `false`.
137+
/// </summary>
138+
public bool AllowSynchronousIO { get; set; } = false;
139+
134140
internal void Apply(UrlGroup urlGroup, RequestQueue requestQueue)
135141
{
136142
_urlGroup = urlGroup;

src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/Request.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ internal sealed class Request
2323
// private byte[] _referredTokenBindingId;
2424

2525
private BoundaryType _contentBoundaryType;
26+
2627
private long? _contentLength;
2728
private RequestStream _nativeStream;
2829

@@ -149,10 +150,7 @@ private RequestStream EnsureRequestStream()
149150
{
150151
if (_nativeStream == null && HasEntityBody)
151152
{
152-
_nativeStream = new RequestStream(RequestContext)
153-
{
154-
MaxSize = RequestContext.Server.Options.MaxRequestBodySize
155-
};
153+
_nativeStream = new RequestStream(RequestContext);
156154
}
157155
return _nativeStream;
158156
}

src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestContext.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ internal RequestContext(HttpSysListener server, NativeRequestContext memoryBlob)
3030
_memoryBlob = memoryBlob;
3131
Request = new Request(this, _memoryBlob);
3232
Response = new Response(this);
33+
AllowSynchronousIO = server.Options.AllowSynchronousIO;
3334
}
3435

3536
internal HttpSysListener Server { get; }
@@ -88,6 +89,8 @@ public unsafe Guid TraceIdentifier
8889

8990
public bool IsUpgradableRequest => Request.IsUpgradable;
9091

92+
internal bool AllowSynchronousIO { get; set; }
93+
9194
public Task<Stream> UpgradeAsync()
9295
{
9396
if (!IsUpgradableRequest)

src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStream.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ internal class RequestStream : Stream
2525
internal RequestStream(RequestContext httpContext)
2626
{
2727
_requestContext = httpContext;
28+
_maxSize = _requestContext.Server.Options.MaxRequestBodySize;
2829
}
2930

3031
internal RequestContext RequestContext
@@ -111,6 +112,10 @@ private void ValidateReadBuffer(byte[] buffer, int offset, int size)
111112

112113
public override unsafe int Read([In, Out] byte[] buffer, int offset, int size)
113114
{
115+
if (!RequestContext.AllowSynchronousIO)
116+
{
117+
throw new InvalidOperationException("Synchronous IO APIs are disabled, see AllowSynchronousIO.");
118+
}
114119
ValidateReadBuffer(buffer, offset, size);
115120
CheckSizeLimit();
116121
if (_closed)

src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/ResponseBody.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,16 @@ public override long Position
9191
// Send headers
9292
public override void Flush()
9393
{
94+
if (!RequestContext.AllowSynchronousIO)
95+
{
96+
throw new InvalidOperationException("Synchronous IO APIs are disabled, see AllowSynchronousIO.");
97+
}
98+
9499
if (_disposed)
95100
{
96101
return;
97102
}
103+
98104
FlushInternal(endOfRequest: false);
99105
}
100106

@@ -449,9 +455,15 @@ private HttpApi.HTTP_FLAGS ComputeLeftToWrite(long writeCount, bool endOfRequest
449455

450456
public override void Write(byte[] buffer, int offset, int count)
451457
{
458+
if (!RequestContext.AllowSynchronousIO)
459+
{
460+
throw new InvalidOperationException("Synchronous IO APIs are disabled, see AllowSynchronousIO.");
461+
}
462+
452463
// Validates for null and bounds. Allows count == 0.
453464
// TODO: Verbose log parameters
454465
var data = new ArraySegment<byte>(buffer, offset, count);
466+
455467
CheckDisposed();
456468

457469
CheckWriteCount(count);

src/Microsoft.AspNetCore.Server.HttpSys/StandardFeatureCollection.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ internal sealed class StandardFeatureCollection : IFeatureCollection
2727
{ typeof(IHttpRequestIdentifierFeature), _identityFunc },
2828
{ typeof(RequestContext), ctx => ctx.RequestContext },
2929
{ typeof(IHttpMaxRequestBodySizeFeature), _identityFunc },
30+
{ typeof(IHttpBodyControlFeature), _identityFunc },
3031
};
3132

3233
private readonly FeatureContext _featureContext;

test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/HttpsTests.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,9 @@ public async Task Https_EchoHelloWorld_Success()
6060
string input = new StreamReader(context.Request.Body).ReadToEnd();
6161
Assert.Equal("Hello World", input);
6262
context.Response.ContentLength = 11;
63-
using (var writer = new StreamWriter(context.Response.Body))
64-
{
65-
writer.Write("Hello World");
66-
}
63+
var writer = new StreamWriter(context.Response.Body);
64+
await writer.WriteAsync("Hello World");
65+
await writer.FlushAsync();
6766

6867
string response = await responseTask;
6968
Assert.Equal("Hello World", response);

test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/OpaqueUpgradeTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public async Task OpaqueUpgrade_AfterHeadersSent_Throws()
2525

2626
var context = await server.AcceptAsync(Utilities.DefaultTimeout);
2727
byte[] body = Encoding.UTF8.GetBytes("Hello World");
28-
context.Response.Body.Write(body, 0, body.Length);
28+
await context.Response.Body.WriteAsync(body, 0, body.Length);
2929

3030
Assert.Throws<InvalidOperationException>(() => context.Response.Headers["Upgrade"] = "WebSocket"); // Win8.1 blocks anything but WebSocket
3131
await Assert.ThrowsAsync<InvalidOperationException>(async () => await context.UpgradeAsync());

test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/RequestBodyTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,31 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener
1616
{
1717
public class RequestBodyTests
1818
{
19+
[ConditionalFact]
20+
public async Task RequestBody_SyncReadDisabledByDefault_Throws()
21+
{
22+
string address;
23+
using (var server = Utilities.CreateHttpServer(out address))
24+
{
25+
Task<string> responseTask = SendRequestAsync(address, "Hello World");
26+
27+
Assert.False(server.Options.AllowSynchronousIO);
28+
29+
var context = await server.AcceptAsync(Utilities.DefaultTimeout);
30+
byte[] input = new byte[100];
31+
32+
Assert.False(context.AllowSynchronousIO);
33+
Assert.Throws<InvalidOperationException>(() => context.Request.Body.Read(input, 0, input.Length));
34+
context.AllowSynchronousIO = true;
35+
var read = context.Request.Body.Read(input, 0, input.Length);
36+
context.Response.ContentLength = read;
37+
context.Response.Body.Write(input, 0, read);
38+
39+
string response = await responseTask;
40+
Assert.Equal("Hello World", response);
41+
}
42+
}
43+
1944
[ConditionalFact]
2045
public async Task RequestBody_ReadSync_Success()
2146
{
@@ -24,6 +49,7 @@ public async Task RequestBody_ReadSync_Success()
2449
{
2550
Task<string> responseTask = SendRequestAsync(address, "Hello World");
2651

52+
server.Options.AllowSynchronousIO = true;
2753
var context = await server.AcceptAsync(Utilities.DefaultTimeout);
2854
byte[] input = new byte[100];
2955
int read = context.Request.Body.Read(input, 0, input.Length);
@@ -81,6 +107,7 @@ public async Task RequestBody_InvalidBuffer_ArgumentException()
81107
{
82108
Task<string> responseTask = SendRequestAsync(address, "Hello World");
83109

110+
server.Options.AllowSynchronousIO = true;
84111
var context = await server.AcceptAsync(Utilities.DefaultTimeout);
85112
byte[] input = new byte[100];
86113
Assert.Throws<ArgumentNullException>("buffer", () => context.Request.Body.Read(null, 0, 1));
@@ -106,6 +133,7 @@ public async Task RequestBody_ReadSyncPartialBody_Success()
106133
{
107134
Task<string> responseTask = SendRequestAsync(address, content);
108135

136+
server.Options.AllowSynchronousIO = true;
109137
var context = await server.AcceptAsync(Utilities.DefaultTimeout);
110138
byte[] input = new byte[10];
111139
int read = context.Request.Body.Read(input, 0, input.Length);

test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseBodyTests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,37 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener
1616
{
1717
public class ResponseBodyTests
1818
{
19+
[ConditionalFact]
20+
public async Task ResponseBody_SyncWriteDisabledByDefault_Throws()
21+
{
22+
string address;
23+
using (var server = Utilities.CreateHttpServer(out address))
24+
{
25+
var responseTask = SendRequestAsync(address);
26+
27+
var context = await server.AcceptAsync(Utilities.DefaultTimeout);
28+
Assert.Throws<InvalidOperationException>(() => context.Response.Body.Flush());
29+
Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(new byte[10], 0, 10));
30+
Assert.Throws<InvalidOperationException>(() => context.Response.Body.Flush());
31+
32+
context.AllowSynchronousIO = true;
33+
34+
context.Response.Body.Flush();
35+
context.Response.Body.Write(new byte[10], 0, 10);
36+
context.Response.Body.Flush();
37+
await context.Response.Body.WriteAsync(new byte[10], 0, 10);
38+
context.Dispose();
39+
40+
var response = await responseTask;
41+
Assert.Equal(200, (int)response.StatusCode);
42+
Assert.Equal(new Version(1, 1), response.Version);
43+
IEnumerable<string> ignored;
44+
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
45+
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
46+
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
47+
}
48+
}
49+
1950
[ConditionalFact]
2051
public async Task ResponseBody_WriteNoHeaders_DefaultsToChunked()
2152
{
@@ -24,6 +55,7 @@ public async Task ResponseBody_WriteNoHeaders_DefaultsToChunked()
2455
{
2556
var responseTask = SendRequestAsync(address);
2657

58+
server.Options.AllowSynchronousIO = true;
2759
var context = await server.AcceptAsync(Utilities.DefaultTimeout);
2860
context.Response.Body.Write(new byte[10], 0, 10);
2961
await context.Response.Body.WriteAsync(new byte[10], 0, 10);
@@ -45,6 +77,7 @@ public async Task ResponseBody_FlushThenWrite_DefaultsToChunkedAndTerminates()
4577
string address;
4678
using (var server = Utilities.CreateHttpServer(out address))
4779
{
80+
server.Options.AllowSynchronousIO = true;
4881
var responseTask = SendRequestAsync(address);
4982

5083
var context = await server.AcceptAsync(Utilities.DefaultTimeout);
@@ -95,6 +128,7 @@ public async Task ResponseBody_WriteContentLength_PassedThrough()
95128
{
96129
var responseTask = SendRequestAsync(address);
97130

131+
server.Options.AllowSynchronousIO = true;
98132
var context = await server.AcceptAsync(Utilities.DefaultTimeout);
99133
context.Response.Headers["Content-lenGth"] = " 30 ";
100134
var stream = context.Response.Body;
@@ -181,6 +215,7 @@ public async Task ResponseBody_WriteContentLengthExtraWritten_Throws()
181215
{
182216
var responseTask = SendRequestAsync(address);
183217

218+
server.Options.AllowSynchronousIO = true;
184219
var context = await server.AcceptAsync(Utilities.DefaultTimeout);
185220
context.Response.Headers["Content-lenGth"] = " 10 ";
186221
context.Response.Body.Write(new byte[10], 0, 10);
@@ -206,6 +241,7 @@ public async Task ResponseBody_WriteZeroCount_StartsChunkedResponse()
206241
{
207242
var responseTask = SendRequestAsync(address);
208243

244+
server.Options.AllowSynchronousIO = true;
209245
var context = await server.AcceptAsync(Utilities.DefaultTimeout);
210246
context.Response.Body.Write(new byte[10], 0, 0);
211247
Assert.True(context.Response.HasStarted);
@@ -380,6 +416,7 @@ public async Task ResponseBodyWriteExceptions_ClientDisconnectsBeforeFirstWrite_
380416
using (var server = Utilities.CreateHttpServer(out address))
381417
{
382418
server.Options.ThrowWriteExceptions = true;
419+
server.Options.AllowSynchronousIO = true;
383420
var cts = new CancellationTokenSource();
384421
var responseTask = SendRequestAsync(address, cts.Token);
385422

@@ -444,6 +481,7 @@ public async Task ResponseBody_ClientDisconnectsBeforeFirstWrite_WriteCompletesS
444481
var cts = new CancellationTokenSource();
445482
var responseTask = SendRequestAsync(address, cts.Token);
446483

484+
server.Options.AllowSynchronousIO = true;
447485
var context = await server.AcceptAsync(Utilities.DefaultTimeout);
448486
// First write sends headers
449487
cts.Cancel();
@@ -555,6 +593,7 @@ public async Task ResponseBody_ClientDisconnectsBeforeSecondWrite_WriteCompletes
555593
string address;
556594
using (var server = Utilities.CreateHttpServer(out address))
557595
{
596+
server.Options.AllowSynchronousIO = true;
558597
RequestContext context;
559598
using (var client = new HttpClient())
560599
{

test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseCachingTests.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ public async Task Caching_SetTtlAndWriteBody_Cached()
324324
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
325325
context.Response.ContentLength = 10;
326326
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
327-
context.Response.Body.Write(new byte[10], 0, 10);
327+
await context.Response.Body.WriteAsync(new byte[10], 0, 10);
328328
// Http.Sys will add this for us
329329
Assert.Null(context.Response.ContentLength);
330330
context.Dispose();
@@ -381,6 +381,7 @@ public async Task Caching_Flush_NotCached()
381381
{
382382
var responseTask = SendRequestAsync(address);
383383

384+
server.Options.AllowSynchronousIO = true;
384385
var context = await server.AcceptAsync(Utilities.DefaultTimeout);
385386
context.Response.Headers["x-request-count"] = "1";
386387
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
@@ -418,8 +419,8 @@ public async Task Caching_WriteFlush_NotCached()
418419
context.Response.Headers["x-request-count"] = "1";
419420
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
420421
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
421-
context.Response.Body.Write(new byte[10], 0, 10);
422-
context.Response.Body.Flush();
422+
await context.Response.Body.WriteAsync(new byte[10], 0, 10);
423+
await context.Response.Body.FlushAsync();
423424
context.Dispose();
424425

425426
var response = await responseTask;
@@ -453,7 +454,7 @@ public async Task Caching_WriteFullContentLength_Cached()
453454
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
454455
context.Response.ContentLength = 10;
455456
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
456-
context.Response.Body.Write(new byte[10], 0, 10);
457+
await context.Response.Body.WriteAsync(new byte[10], 0, 10);
457458
// Http.Sys will add this for us
458459
Assert.Null(context.Response.ContentLength);
459460
context.Dispose();
@@ -999,7 +1000,7 @@ public async Task Caching_CacheRange_NotCached()
9991000
context.Response.Headers["content-range"] = "bytes 0-10/100";
10001001
context.Response.ContentLength = 11;
10011002
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
1002-
context.Response.Body.Write(new byte[100], 0, 11);
1003+
await context.Response.Body.WriteAsync(new byte[100], 0, 11);
10031004
context.Dispose();
10041005

10051006
var response = await responseTask;
@@ -1016,7 +1017,7 @@ public async Task Caching_CacheRange_NotCached()
10161017
context.Response.Headers["content-range"] = "bytes 0-10/100";
10171018
context.Response.ContentLength = 11;
10181019
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
1019-
context.Response.Body.Write(new byte[100], 0, 11);
1020+
await context.Response.Body.WriteAsync(new byte[100], 0, 11);
10201021
context.Dispose();
10211022

10221023
response = await responseTask;
@@ -1041,7 +1042,7 @@ public async Task Caching_RequestRangeFromCache_RangeServedFromCache()
10411042
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
10421043
context.Response.ContentLength = 100;
10431044
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
1044-
context.Response.Body.Write(new byte[100], 0, 100);
1045+
await context.Response.Body.WriteAsync(new byte[100], 0, 100);
10451046
context.Dispose();
10461047

10471048
var response = await responseTask;
@@ -1071,7 +1072,7 @@ public async Task Caching_RequestMultipleRangesFromCache_RangesServedFromCache()
10711072
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
10721073
context.Response.ContentLength = 100;
10731074
context.Response.CacheTtl = TimeSpan.FromSeconds(10);
1074-
context.Response.Body.Write(new byte[100], 0, 100);
1075+
await context.Response.Body.WriteAsync(new byte[100], 0, 100);
10751076
context.Dispose();
10761077

10771078
var response = await responseTask;

test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseHeaderTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ public async Task Headers_FlushSendsHeaders_Success()
390390
{
391391
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
392392

393+
server.Options.AllowSynchronousIO = true;
393394
var context = await server.AcceptAsync(Utilities.DefaultTimeout);
394395
var responseHeaders = context.Response.Headers;
395396

0 commit comments

Comments
 (0)