diff --git a/src/Hosting/TestHost/src/AsyncStreamWrapper.cs b/src/Hosting/TestHost/src/AsyncStreamWrapper.cs index 32f1548d35..4766f8fcf1 100644 --- a/src/Hosting/TestHost/src/AsyncStreamWrapper.cs +++ b/src/Hosting/TestHost/src/AsyncStreamWrapper.cs @@ -21,13 +21,17 @@ namespace Microsoft.AspNetCore.TestHost public override bool CanRead => _inner.CanRead; - public override bool CanSeek => _inner.CanSeek; + public override bool CanSeek => false; public override bool CanWrite => _inner.CanWrite; - public override long Length => _inner.Length; + public override long Length => throw new NotSupportedException("The stream is not seekable."); - public override long Position { get => _inner.Position; set => _inner.Position = value; } + public override long Position + { + get => throw new NotSupportedException("The stream is not seekable."); + set => throw new NotSupportedException("The stream is not seekable."); + } public override void Flush() { @@ -72,12 +76,12 @@ namespace Microsoft.AspNetCore.TestHost public override long Seek(long offset, SeekOrigin origin) { - return _inner.Seek(offset, origin); + throw new NotSupportedException("The stream is not seekable."); } public override void SetLength(long value) { - _inner.SetLength(value); + throw new NotSupportedException("The stream is not seekable."); } public override void Write(byte[] buffer, int offset, int count) diff --git a/src/Hosting/TestHost/src/TestServer.cs b/src/Hosting/TestHost/src/TestServer.cs index c5668185ac..783184a8de 100644 --- a/src/Hosting/TestHost/src/TestServer.cs +++ b/src/Hosting/TestHost/src/TestServer.cs @@ -81,9 +81,9 @@ namespace Microsoft.AspNetCore.TestHost /// Gets or sets a value that controls whether synchronous IO is allowed for the and /// /// - /// Defaults to true. + /// Defaults to false. /// - public bool AllowSynchronousIO { get; set; } = true; + public bool AllowSynchronousIO { get; set; } = false; private IHttpApplication Application { diff --git a/src/Mvc/Mvc.Formatters.Xml/src/XmlDataContractSerializerOutputFormatter.cs b/src/Mvc/Mvc.Formatters.Xml/src/XmlDataContractSerializerOutputFormatter.cs index c0a9cdab2d..48e7a0d15b 100644 --- a/src/Mvc/Mvc.Formatters.Xml/src/XmlDataContractSerializerOutputFormatter.cs +++ b/src/Mvc/Mvc.Formatters.Xml/src/XmlDataContractSerializerOutputFormatter.cs @@ -254,6 +254,13 @@ namespace Microsoft.AspNetCore.Mvc.Formatters var dataContractSerializer = GetCachedSerializer(wrappingType); + // Opt into sync IO support until we can work out an alternative https://github.com/aspnet/AspNetCore/issues/6397 + var syncIOFeature = context.HttpContext.Features.Get(); + if (syncIOFeature != null) + { + syncIOFeature.AllowSynchronousIO = true; + } + using (var textWriter = context.WriterFactory(context.HttpContext.Response.Body, writerSettings.Encoding)) { using (var xmlWriter = CreateXmlWriter(context, textWriter, writerSettings)) diff --git a/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerOutputFormatter.cs b/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerOutputFormatter.cs index 9960eedbd6..da1812e6e9 100644 --- a/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerOutputFormatter.cs +++ b/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerOutputFormatter.cs @@ -230,6 +230,13 @@ namespace Microsoft.AspNetCore.Mvc.Formatters var xmlSerializer = GetCachedSerializer(wrappingType); + // Opt into sync IO support until we can work out an alternative https://github.com/aspnet/AspNetCore/issues/6397 + var syncIOFeature = context.HttpContext.Features.Get(); + if (syncIOFeature != null) + { + syncIOFeature.AllowSynchronousIO = true; + } + using (var textWriter = context.WriterFactory(context.HttpContext.Response.Body, writerSettings.Encoding)) { using (var xmlWriter = CreateXmlWriter(context, textWriter, writerSettings)) diff --git a/src/Mvc/Mvc.ViewFeatures/src/ViewComponentResultExecutor.cs b/src/Mvc/Mvc.ViewFeatures/src/ViewComponentResultExecutor.cs index 6a4fd56ba4..0d839e7b68 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/ViewComponentResultExecutor.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/ViewComponentResultExecutor.cs @@ -105,6 +105,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures response.StatusCode = result.StatusCode.Value; } + // Opt into sync IO support until we can work out an alternative https://github.com/aspnet/AspNetCore/issues/6397 + var syncIOFeature = context.HttpContext.Features.Get(); + if (syncIOFeature != null) + { + syncIOFeature.AllowSynchronousIO = true; + } + using (var writer = new HttpResponseStreamWriter(response.Body, resolvedContentTypeEncoding)) { var viewContext = new ViewContext( diff --git a/src/Mvc/test/WebSites/BasicWebSite/StartupRequestLimitSize.cs b/src/Mvc/test/WebSites/BasicWebSite/StartupRequestLimitSize.cs index 286169d54d..3e0e9aa16e 100644 --- a/src/Mvc/test/WebSites/BasicWebSite/StartupRequestLimitSize.cs +++ b/src/Mvc/test/WebSites/BasicWebSite/StartupRequestLimitSize.cs @@ -3,6 +3,8 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; @@ -48,6 +50,7 @@ namespace BasicWebSite { private readonly Stream _innerStream; private readonly IHttpMaxRequestBodySizeFeature _maxRequestBodySizeFeature; + private long _totalRead; public RequestBodySizeCheckingStream( Stream innerStream, @@ -78,12 +81,39 @@ namespace BasicWebSite public override int Read(byte[] buffer, int offset, int count) { if (_maxRequestBodySizeFeature.MaxRequestBodySize != null - && _innerStream.Length > _maxRequestBodySizeFeature.MaxRequestBodySize) + && _innerStream.CanSeek && _innerStream.Length > _maxRequestBodySizeFeature.MaxRequestBodySize) { throw new InvalidOperationException("Request content size is greater than the limit size"); } - return _innerStream.Read(buffer, offset, count); + var read = _innerStream.Read(buffer, offset, count); + _totalRead += read; + + if (_maxRequestBodySizeFeature.MaxRequestBodySize != null + && _totalRead > _maxRequestBodySizeFeature.MaxRequestBodySize) + { + throw new InvalidOperationException("Request content size is greater than the limit size"); + } + return read; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_maxRequestBodySizeFeature.MaxRequestBodySize != null + && _innerStream.CanSeek && _innerStream.Length > _maxRequestBodySizeFeature.MaxRequestBodySize) + { + throw new InvalidOperationException("Request content size is greater than the limit size"); + } + + var read = await _innerStream.ReadAsync(buffer, offset, count, cancellationToken); + _totalRead += read; + + if (_maxRequestBodySizeFeature.MaxRequestBodySize != null + && _totalRead > _maxRequestBodySizeFeature.MaxRequestBodySize) + { + throw new InvalidOperationException("Request content size is greater than the limit size"); + } + return read; } public override long Seek(long offset, SeekOrigin origin) diff --git a/src/Mvc/test/WebSites/FormatterWebSite/StringInputFormatter.cs b/src/Mvc/test/WebSites/FormatterWebSite/StringInputFormatter.cs index dc80e3dc62..746b1ea569 100644 --- a/src/Mvc/test/WebSites/FormatterWebSite/StringInputFormatter.cs +++ b/src/Mvc/test/WebSites/FormatterWebSite/StringInputFormatter.cs @@ -20,13 +20,13 @@ namespace FormatterWebSite SupportedEncodings.Add(Encoding.Unicode); } - public override Task ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding) + public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding) { var request = context.HttpContext.Request; using (var reader = new StreamReader(request.Body, effectiveEncoding)) { - var stringContent = reader.ReadToEnd(); - return InputFormatterResult.SuccessAsync(stringContent); + var stringContent = await reader.ReadToEndAsync(); + return await InputFormatterResult.SuccessAsync(stringContent); } } } diff --git a/src/Servers/HttpSys/src/HttpSysOptions.cs b/src/Servers/HttpSys/src/HttpSysOptions.cs index 4500e244b0..82453398a8 100644 --- a/src/Servers/HttpSys/src/HttpSysOptions.cs +++ b/src/Servers/HttpSys/src/HttpSysOptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -136,9 +136,9 @@ namespace Microsoft.AspNetCore.Server.HttpSys /// /// Gets or sets a value that controls whether synchronous IO is allowed for the HttpContext.Request.Body and HttpContext.Response.Body. - /// The default is `true`. + /// The default is `false`. /// - public bool AllowSynchronousIO { get; set; } = true; + public bool AllowSynchronousIO { get; set; } = false; /// /// Gets or sets a value that controls how http.sys reacts when rejecting requests due to throttling conditions - like when the request diff --git a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs index a010203f9b..d3e4fa54ce 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs @@ -51,14 +51,13 @@ namespace Microsoft.AspNetCore.Server.HttpSys [ConditionalFact] public async Task Https_EchoHelloWorld_Success() { - using (Utilities.CreateDynamicHttpsServer(out var address, httpContext => + using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext => { - string input = new StreamReader(httpContext.Request.Body).ReadToEnd(); + var input = await new StreamReader(httpContext.Request.Body).ReadToEndAsync(); Assert.Equal("Hello World", input); - byte[] body = Encoding.UTF8.GetBytes("Hello World"); + var body = Encoding.UTF8.GetBytes("Hello World"); httpContext.Response.ContentLength = body.Length; - httpContext.Response.Body.Write(body, 0, body.Length); - return Task.FromResult(0); + await httpContext.Response.Body.WriteAsync(body, 0, body.Length); })) { string response = await SendRequestAsync(address, "Hello World"); diff --git a/src/Servers/HttpSys/test/FunctionalTests/Listener/RequestBodyTests.cs b/src/Servers/HttpSys/test/FunctionalTests/Listener/RequestBodyTests.cs index c805818931..bdefc3b87f 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Listener/RequestBodyTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/Listener/RequestBodyTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -17,26 +17,26 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener public class RequestBodyTests { [ConditionalFact] - public async Task RequestBody_SyncReadEnabledByDefault_ThrowsWhenDisabled() + public async Task RequestBody_SyncReadDisabledByDefault_WorksWhenEnabled() { string address; using (var server = Utilities.CreateHttpServer(out address)) { Task responseTask = SendRequestAsync(address, "Hello World"); - Assert.True(server.Options.AllowSynchronousIO); + Assert.False(server.Options.AllowSynchronousIO); var context = await server.AcceptAsync(Utilities.DefaultTimeout).Before(responseTask); byte[] input = new byte[100]; + Assert.Throws(() => context.Request.Body.Read(input, 0, input.Length)); + + context.AllowSynchronousIO = true; Assert.True(context.AllowSynchronousIO); var read = context.Request.Body.Read(input, 0, input.Length); context.Response.ContentLength = read; context.Response.Body.Write(input, 0, read); - context.AllowSynchronousIO = false; - Assert.Throws(() => context.Request.Body.Read(input, 0, input.Length)); - string response = await responseTask; Assert.Equal("Hello World", response); } diff --git a/src/Servers/HttpSys/test/FunctionalTests/Listener/ResponseBodyTests.cs b/src/Servers/HttpSys/test/FunctionalTests/Listener/ResponseBodyTests.cs index af044d8297..be87889ee7 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Listener/ResponseBodyTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/Listener/ResponseBodyTests.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener public class ResponseBodyTests { [ConditionalFact] - public async Task ResponseBody_SyncWriteEnabledByDefault_ThrowsWhenDisabled() + public async Task ResponseBody_SyncWriteDisabledByDefault_WorksWhenEnabled() { string address; using (var server = Utilities.CreateHttpServer(out address)) @@ -26,19 +26,17 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener var context = await server.AcceptAsync(Utilities.DefaultTimeout).Before(responseTask); - Assert.True(context.AllowSynchronousIO); - - context.Response.Body.Flush(); - context.Response.Body.Write(new byte[10], 0, 10); - context.Response.Body.Flush(); - - context.AllowSynchronousIO = false; + Assert.False(context.AllowSynchronousIO); Assert.Throws(() => context.Response.Body.Flush()); Assert.Throws(() => context.Response.Body.Write(new byte[10], 0, 10)); Assert.Throws(() => context.Response.Body.Flush()); - await context.Response.Body.WriteAsync(new byte[10], 0, 10); + context.AllowSynchronousIO = true; + + context.Response.Body.Flush(); + context.Response.Body.Write(new byte[10], 0, 10); + context.Response.Body.Flush(); context.Dispose(); var response = await responseTask; @@ -47,7 +45,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener IEnumerable ignored; Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); - Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync()); + Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync()); } } @@ -477,4 +475,4 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener } } } -} \ No newline at end of file +} diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs index b2e39fccd7..1f872b655f 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs @@ -320,7 +320,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core IEnumerator IEnumerable.GetEnumerator() => FastEnumerable().GetEnumerator(); - bool IHttpBodyControlFeature.AllowSynchronousIO { get; set; } = true; + bool IHttpBodyControlFeature.AllowSynchronousIO { get; set; } void IHttpBufferingFeature.DisableRequestBuffering() { diff --git a/src/Servers/IIS/IIS/src/IISServerOptions.cs b/src/Servers/IIS/IIS/src/IISServerOptions.cs index 47d7619f8a..65f86240bd 100644 --- a/src/Servers/IIS/IIS/src/IISServerOptions.cs +++ b/src/Servers/IIS/IIS/src/IISServerOptions.cs @@ -11,9 +11,9 @@ namespace Microsoft.AspNetCore.Builder /// Gets or sets a value that controls whether synchronous IO is allowed for the and /// /// - /// Defaults to true. + /// Defaults to false. /// - public bool AllowSynchronousIO { get; set; } = true; + public bool AllowSynchronousIO { get; set; } = false; /// /// If true the server should set HttpContext.User. If false the server will only provide an diff --git a/src/Servers/IIS/IIS/test/IIS.Tests/ConnectionIdFeatureTests.cs b/src/Servers/IIS/IIS/test/IIS.Tests/ConnectionIdFeatureTests.cs index 37e69b3a32..27688b5e58 100644 --- a/src/Servers/IIS/IIS/test/IIS.Tests/ConnectionIdFeatureTests.cs +++ b/src/Servers/IIS/IIS/test/IIS.Tests/ConnectionIdFeatureTests.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Testing.xunit; @@ -11,44 +10,22 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests { [SkipIfHostableWebCoreNotAvailable] [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] - public class HttpBodyControlFeatureTests : StrictTestServerTests + public class ConnectionIdFeatureTests : StrictTestServerTests { [ConditionalFact] - public async Task ThrowsOnSyncReadOrWrite() + public async Task ProvidesConnectionId() { - Exception writeException = null; - Exception readException = null; - using (var testServer = await TestServer.Create( - ctx => { - var bodyControl = ctx.Features.Get(); - bodyControl.AllowSynchronousIO = false; - - try - { - ctx.Response.Body.Write(new byte[10]); - } - catch (Exception ex) - { - writeException = ex; - } - - try - { - ctx.Request.Body.Read(new byte[10]); - } - catch (Exception ex) - { - readException = ex; - } - - return Task.CompletedTask; - }, LoggerFactory)) + string connectionId = null; + using (var testServer = await TestServer.Create(ctx => { + var connectionIdFeature = ctx.Features.Get(); + connectionId = connectionIdFeature.ConnectionId; + return Task.CompletedTask; + }, LoggerFactory)) { await testServer.HttpClient.GetStringAsync("/"); } - Assert.IsType(readException); - Assert.IsType(writeException); + Assert.NotNull(connectionId); } } } diff --git a/src/Servers/IIS/IIS/test/IIS.Tests/HttpBodyControlFeatureTests.cs b/src/Servers/IIS/IIS/test/IIS.Tests/HttpBodyControlFeatureTests.cs index 3d91c445ca..8ac359a21b 100644 --- a/src/Servers/IIS/IIS/test/IIS.Tests/HttpBodyControlFeatureTests.cs +++ b/src/Servers/IIS/IIS/test/IIS.Tests/HttpBodyControlFeatureTests.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Testing.xunit; @@ -10,22 +11,44 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests { [SkipIfHostableWebCoreNotAvailable] [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] - public class ConnectionIdFeatureTests : StrictTestServerTests + public class HttpBodyControlFeatureTests : StrictTestServerTests { [ConditionalFact] - public async Task ProvidesConnectionId() + public async Task ThrowsOnSyncReadOrWrite() { - string connectionId = null; - using (var testServer = await TestServer.Create(ctx => { - var connectionIdFeature = ctx.Features.Get(); - connectionId = connectionIdFeature.ConnectionId; + Exception writeException = null; + Exception readException = null; + using (var testServer = await TestServer.Create( + ctx => { + var bodyControl = ctx.Features.Get(); + Assert.False(bodyControl.AllowSynchronousIO); + + try + { + ctx.Response.Body.Write(new byte[10]); + } + catch (Exception ex) + { + writeException = ex; + } + + try + { + ctx.Request.Body.Read(new byte[10]); + } + catch (Exception ex) + { + readException = ex; + } + return Task.CompletedTask; }, LoggerFactory)) { await testServer.HttpClient.GetStringAsync("/"); } - Assert.NotNull(connectionId); + Assert.IsType(readException); + Assert.IsType(writeException); } } } diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index 0471b0d1c9..aa36ee7ff1 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -50,9 +50,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// Gets or sets a value that controls whether synchronous IO is allowed for the and /// /// - /// Defaults to true. + /// Defaults to false. /// - public bool AllowSynchronousIO { get; set; } = true; + public bool AllowSynchronousIO { get; set; } = false; /// /// Enables the Listen options callback to resolve and use services registered by the application during startup. diff --git a/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs index 75739a4c7b..02e52db7f0 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs @@ -23,11 +23,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } [Fact] - public void AllowSynchronousIODefaultsToTrue() + public void AllowSynchronousIODefaultsToFalse() { var options = new KestrelServerOptions(); - Assert.True(options.AllowSynchronousIO); + Assert.False(options.AllowSynchronousIO); } [Fact] @@ -65,4 +65,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.False(options.ListenOptions[3].NoDelay); } } -} \ No newline at end of file +} diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs index 0fb509bfea..c482c63b4f 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs @@ -1077,45 +1077,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests } [Fact] - public async Task SynchronousReadsAllowedByDefault() + public async Task SynchronousReadsDisallowedByDefault() { - var firstRequest = true; - using (var server = new TestServer(async context => { var bodyControlFeature = context.Features.Get(); - Assert.True(bodyControlFeature.AllowSynchronousIO); + Assert.False(bodyControlFeature.AllowSynchronousIO); var buffer = new byte[6]; var offset = 0; // The request body is 5 bytes long. The 6th byte (buffer[5]) is only used for writing the response body. - buffer[5] = (byte)(firstRequest ? '1' : '2'); + buffer[5] = (byte)'1'; - if (firstRequest) + // Synchronous reads throw. + var ioEx = Assert.Throws(() => context.Request.Body.Read(new byte[1], 0, 1)); + Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx.Message); + + var ioEx2 = Assert.Throws(() => context.Request.Body.CopyTo(Stream.Null)); + Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx2.Message); + + while (offset < 5) { - while (offset < 5) - { - offset += context.Request.Body.Read(buffer, offset, 5 - offset); - } - - firstRequest = false; - } - else - { - bodyControlFeature.AllowSynchronousIO = false; - - // Synchronous reads now throw. - var ioEx = Assert.Throws(() => context.Request.Body.Read(new byte[1], 0, 1)); - Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx.Message); - - var ioEx2 = Assert.Throws(() => context.Request.Body.CopyTo(Stream.Null)); - Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx2.Message); - - while (offset < 5) - { - offset += await context.Request.Body.ReadAsync(buffer, offset, 5 - offset); - } + offset += await context.Request.Body.ReadAsync(buffer, offset, 5 - offset); } Assert.Equal(0, await context.Request.Body.ReadAsync(new byte[1], 0, 1)); @@ -1132,7 +1116,46 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests "Host:", "Content-Length: 5", "", - "HelloPOST / HTTP/1.1", + "Hello"); + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 6", + "", + "Hello1"); + } + } + } + + [Fact] + public async Task SynchronousReadsAllowedByOptIn() + { + using (var server = new TestServer(async context => + { + var bodyControlFeature = context.Features.Get(); + Assert.False(bodyControlFeature.AllowSynchronousIO); + + var buffer = new byte[5]; + var offset = 0; + + bodyControlFeature.AllowSynchronousIO = true; + + while (offset < 5) + { + offset += context.Request.Body.Read(buffer, offset, 5 - offset); + } + + Assert.Equal(0, await context.Request.Body.ReadAsync(new byte[1], 0, 1)); + Assert.Equal("Hello", Encoding.ASCII.GetString(buffer, 0, 5)); + + context.Response.ContentLength = 5; + await context.Response.Body.WriteAsync(buffer, 0, 5); + }, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", "Host:", "Content-Length: 5", "", @@ -1140,13 +1163,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests await connection.Receive( "HTTP/1.1 200 OK", $"Date: {server.Context.DateHeaderValue}", - "Content-Length: 6", + "Content-Length: 5", "", - "Hello1HTTP/1.1 200 OK", - $"Date: {server.Context.DateHeaderValue}", - "Content-Length: 6", - "", - "Hello2"); + "Hello"); } await server.StopAsync(); } @@ -1197,6 +1216,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests } } + [Fact] + public async Task SynchronousReadsCanBeAllowedGlobally() + { + var testContext = new TestServiceContext(LoggerFactory) + { + ServerOptions = { AllowSynchronousIO = true } + }; + + using (var server = new TestServer(async context => + { + var bodyControlFeature = context.Features.Get(); + Assert.True(bodyControlFeature.AllowSynchronousIO); + + int offset = 0; + var buffer = new byte[5]; + while (offset < 5) + { + offset += context.Request.Body.Read(buffer, offset, 5 - offset); + } + + Assert.Equal(0, await context.Request.Body.ReadAsync(new byte[1], 0, 1)); + Assert.Equal("Hello", Encoding.ASCII.GetString(buffer, 0, 5)); + }, testContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Content-Length: 5", + "", + "Hello"); + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + } + public static TheoryData HostHeaderData => HttpParsingData.HostHeaderData; } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs index b38e97f465..dd7ce8dc16 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs @@ -3042,34 +3042,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests Assert.Equal(2, callOrder.Pop()); } - [Fact] - public async Task SynchronousWritesAllowedByDefault() + public async Task SynchronousWritesDisallowedByDefault() { - var firstRequest = true; - using (var server = new TestServer(async context => { var bodyControlFeature = context.Features.Get(); - Assert.True(bodyControlFeature.AllowSynchronousIO); + Assert.False(bodyControlFeature.AllowSynchronousIO); context.Response.ContentLength = 6; - if (firstRequest) - { - context.Response.Body.Write(Encoding.ASCII.GetBytes("Hello1"), 0, 6); - firstRequest = false; - } - else - { - bodyControlFeature.AllowSynchronousIO = false; + // Synchronous writes now throw. + var ioEx = Assert.Throws(() => context.Response.Body.Write(Encoding.ASCII.GetBytes("What!?"), 0, 6)); + Assert.Equal(CoreStrings.SynchronousWritesDisallowed, ioEx.Message); + await context.Response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello1"), 0, 6); - // Synchronous writes now throw. - var ioEx = Assert.Throws(() => context.Response.Body.Write(Encoding.ASCII.GetBytes("What!?"), 0, 6)); - Assert.Equal(CoreStrings.SynchronousWritesDisallowed, ioEx.Message); - - await context.Response.BodyPipe.WriteAsync(new Memory(Encoding.ASCII.GetBytes("Hello2"), 0, 6)); - } }, new TestServiceContext(LoggerFactory))) { using (var connection = server.CreateConnection()) @@ -3081,14 +3068,67 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests "Content-Length: 6", "", "Hello1"); + } + } + } + [Fact] + public async Task SynchronousWritesAllowedByOptIn() + { + using (var server = new TestServer(context => + { + var bodyControlFeature = context.Features.Get(); + Assert.False(bodyControlFeature.AllowSynchronousIO); + bodyControlFeature.AllowSynchronousIO = true; + context.Response.ContentLength = 6; + context.Response.Body.Write(Encoding.ASCII.GetBytes("Hello1"), 0, 6); + return Task.CompletedTask; + }, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { await connection.SendEmptyGet(); await connection.Receive( "HTTP/1.1 200 OK", $"Date: {server.Context.DateHeaderValue}", "Content-Length: 6", "", - "Hello2"); + "Hello1"); + } + } + } + + [Fact] + public async Task SynchronousWritesCanBeAllowedGlobally() + { + var testContext = new TestServiceContext(LoggerFactory) + { + ServerOptions = { AllowSynchronousIO = true } + }; + + using (var server = new TestServer(context => + { + var bodyControlFeature = context.Features.Get(); + Assert.True(bodyControlFeature.AllowSynchronousIO); + + context.Response.ContentLength = 6; + context.Response.Body.Write(Encoding.ASCII.GetBytes("Hello!"), 0, 6); + return Task.CompletedTask; + }, testContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 6", + "", + "Hello!"); } await server.StopAsync(); } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/UpgradeTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/UpgradeTests.cs index 03e5dab1b5..f36d4e84a1 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/UpgradeTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/UpgradeTests.cs @@ -33,6 +33,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { await writer.WriteLineAsync("New protocol data"); await writer.FlushAsync(); + await writer.DisposeAsync(); } upgrade.TrySetResult(true); @@ -79,6 +80,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests Assert.Equal(send, line); await writer.WriteLineAsync(recv); await writer.FlushAsync(); + await writer.DisposeAsync(); } upgrade.TrySetResult(true);