From fe6ecfde65b6b9a1c8b1a1db077e2fc85654082c Mon Sep 17 00:00:00 2001 From: Chris R Date: Wed, 3 Aug 2016 15:55:57 -0700 Subject: [PATCH] #160 Remove response body buffering, fix layering of related features --- .../FeatureContext.cs | 114 ++++++++++--- .../Helpers.cs | 5 +- .../MessagePump.cs | 22 ++- .../ResponseStream.cs | 143 ++++++++++++++++ .../NativeInterop/TokenBindingUtil.cs | 2 +- .../RequestProcessing/BufferBuilder.cs | 50 ------ .../RequestProcessing/Response.cs | 149 ++--------------- .../RequestProcessing/ResponseStream.cs | 156 +++++++----------- .../ResponseStreamAsyncResult.cs | 25 +-- src/Microsoft.Net.Http.Server/WebListener.cs | 8 - .../OpaqueUpgradeTests.cs | 34 ++++ .../ResponseBodyTests.cs | 107 ++++++++++-- .../ResponseSendFileTests.cs | 66 ++++++-- .../ResponseTests.cs | 96 ++++++++++- .../WebSocketTests.cs | 33 ++++ .../ResponseBodyTests.cs | 114 +------------ .../ResponseCachingTests.cs | 1 - .../ResponseSendFileTests.cs | 2 +- 18 files changed, 646 insertions(+), 481 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Server.WebListener/ResponseStream.cs delete mode 100644 src/Microsoft.Net.Http.Server/RequestProcessing/BufferBuilder.cs diff --git a/src/Microsoft.AspNetCore.Server.WebListener/FeatureContext.cs b/src/Microsoft.AspNetCore.Server.WebListener/FeatureContext.cs index df448e2b72..2d9fe77c77 100644 --- a/src/Microsoft.AspNetCore.Server.WebListener/FeatureContext.cs +++ b/src/Microsoft.AspNetCore.Server.WebListener/FeatureContext.cs @@ -16,8 +16,10 @@ // permissions and limitations under the License. using System; +using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Net; using System.Net.WebSockets; using System.Security.Claims; @@ -46,8 +48,6 @@ namespace Microsoft.AspNetCore.Server.WebListener IHttpUpgradeFeature, IHttpRequestIdentifierFeature { - private static Func OnStartDelegate = OnStart; - private RequestContext _requestContext; private IFeatureCollection _features; private bool _enableResponseCaching; @@ -74,13 +74,18 @@ namespace Microsoft.AspNetCore.Server.WebListener private Stream _responseStream; private IHeaderDictionary _responseHeaders; + private List, object>> _onStartingActions = new List, object>>(); + private List, object>> _onCompletedActions = new List, object>>(); + private bool _responseStarted; + private bool _completed; + internal FeatureContext(RequestContext requestContext, bool enableResponseCaching) { _requestContext = requestContext; _features = new FeatureCollection(new StandardFeatureCollection(this)); _authHandler = new AuthenticationHandler(requestContext); _enableResponseCaching = enableResponseCaching; - requestContext.Response.OnStarting(OnStartDelegate, this); + _responseStream = new ResponseStream(requestContext.Response.Body, OnStart); } internal IFeatureCollection Features @@ -346,7 +351,7 @@ namespace Microsoft.AspNetCore.Server.WebListener void IHttpBufferingFeature.DisableResponseBuffering() { - Response.ShouldBuffer = false; + // TODO: What about native buffering? } Stream IHttpResponseFeature.Body @@ -382,12 +387,30 @@ namespace Microsoft.AspNetCore.Server.WebListener void IHttpResponseFeature.OnStarting(Func callback, object state) { - Response.OnStarting(callback, state); + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + if (_onStartingActions == null) + { + throw new InvalidOperationException("Cannot register new callbacks, the response has already started."); + } + + _onStartingActions.Add(new Tuple, object>(callback, state)); } void IHttpResponseFeature.OnCompleted(Func callback, object state) { - Response.OnCompleted(callback, state); + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + if (_onCompletedActions == null) + { + throw new InvalidOperationException("Cannot register new callbacks, the response has already completed."); + } + + _onCompletedActions.Add(new Tuple, object>(callback, state)); } string IHttpResponseFeature.ReasonPhrase @@ -402,9 +425,10 @@ namespace Microsoft.AspNetCore.Server.WebListener set { Response.StatusCode = value; } } - Task IHttpSendFileFeature.SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) + async Task IHttpSendFileFeature.SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) { - return Response.SendFileAsync(path, offset, length, cancellation); + await OnStart(); + await Response.SendFileAsync(path, offset, length, cancellation); } CancellationToken IHttpRequestLifetimeFeature.RequestAborted @@ -430,14 +454,15 @@ namespace Microsoft.AspNetCore.Server.WebListener get { return _requestContext.IsUpgradableRequest; } } - Task IHttpUpgradeFeature.UpgradeAsync() + async Task IHttpUpgradeFeature.UpgradeAsync() { - return _requestContext.UpgradeAsync(); + await OnStart(); + return await _requestContext.UpgradeAsync(); } bool IHttpWebSocketFeature.IsWebSocketRequest => _requestContext.IsWebSocketRequest; - Task IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context) + async Task IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context) { // TODO: Advanced params string subProtocol = null; @@ -445,7 +470,9 @@ namespace Microsoft.AspNetCore.Server.WebListener { subProtocol = context.SubProtocol; } - return _requestContext.AcceptWebSocketAsync(subProtocol); + + await OnStart(); + return await _requestContext.AcceptWebSocketAsync(subProtocol); } ClaimsPrincipal IHttpAuthenticationFeature.User @@ -480,22 +507,42 @@ namespace Microsoft.AspNetCore.Server.WebListener set { _requestId = value; } } - private static Task OnStart(object obj) + internal async Task OnStart() { - var featureContext = (FeatureContext)obj; - - ConsiderEnablingResponseCache(featureContext); - return Task.FromResult(0); + if (_responseStarted) + { + return; + } + _responseStarted = true; + await NotifiyOnStartingAsync(); + ConsiderEnablingResponseCache(); } - private static void ConsiderEnablingResponseCache(FeatureContext featureContext) + private async Task NotifiyOnStartingAsync() { - if (featureContext._enableResponseCaching) + var actions = _onStartingActions; + _onStartingActions = null; + if (actions == null) + { + return; + } + + actions.Reverse(); + // Execute last to first. This mimics a stack unwind. + foreach (var actionPair in actions) + { + await actionPair.Item1(actionPair.Item2); + } + } + + private void ConsiderEnablingResponseCache() + { + if (_enableResponseCaching) { // We don't have to worry too much about what Http.Sys supports, caching is a best-effort feature. // If there's something about the request or response that prevents it from caching then the response // will complete normally without caching. - featureContext._requestContext.Response.CacheTtl = GetCacheTtl(featureContext._requestContext); + _requestContext.Response.CacheTtl = GetCacheTtl(_requestContext); } } @@ -544,5 +591,32 @@ namespace Microsoft.AspNetCore.Server.WebListener return null; } + + internal Task OnCompleted() + { + if (_completed) + { + return Helpers.CompletedTask; + } + _completed = true; + return NotifyOnCompletedAsync(); + } + + private async Task NotifyOnCompletedAsync() + { + var actions = _onCompletedActions; + _onCompletedActions = null; + if (actions == null) + { + return; + } + + actions.Reverse(); + // Execute last to first. This mimics a stack unwind. + foreach (var actionPair in actions) + { + await actionPair.Item1(actionPair.Item2); + } + } } } diff --git a/src/Microsoft.AspNetCore.Server.WebListener/Helpers.cs b/src/Microsoft.AspNetCore.Server.WebListener/Helpers.cs index f9ff181782..1b7efd9698 100644 --- a/src/Microsoft.AspNetCore.Server.WebListener/Helpers.cs +++ b/src/Microsoft.AspNetCore.Server.WebListener/Helpers.cs @@ -28,10 +28,7 @@ namespace Microsoft.AspNetCore.Server.WebListener { internal static class Helpers { - internal static Task CompletedTask() - { - return Task.FromResult(null); - } + internal static Task CompletedTask { get; } = Task.FromResult(0); internal static ConfiguredTaskAwaitable SupressContext(this Task task) { diff --git a/src/Microsoft.AspNetCore.Server.WebListener/MessagePump.cs b/src/Microsoft.AspNetCore.Server.WebListener/MessagePump.cs index a686dc782e..591b060900 100644 --- a/src/Microsoft.AspNetCore.Server.WebListener/MessagePump.cs +++ b/src/Microsoft.AspNetCore.Server.WebListener/MessagePump.cs @@ -166,26 +166,34 @@ namespace Microsoft.AspNetCore.Server.WebListener } object context = null; + Interlocked.Increment(ref _outstandingRequests); try { - Interlocked.Increment(ref _outstandingRequests); - FeatureContext featureContext = new FeatureContext(requestContext, EnableResponseCaching); + var featureContext = new FeatureContext(requestContext, EnableResponseCaching); context = _application.CreateContext(featureContext.Features); - await _application.ProcessRequestAsync(context).SupressContext(); - requestContext.Dispose(); - _application.DisposeContext(context, null); + try + { + await _application.ProcessRequestAsync(context).SupressContext(); + await featureContext.OnStart(); + requestContext.Dispose(); + _application.DisposeContext(context, null); + } + finally + { + await featureContext.OnCompleted(); + } } catch (Exception ex) { LogHelper.LogException(_logger, "ProcessRequestAsync", ex); - if (requestContext.Response.HasStartedSending) + if (requestContext.Response.HasStarted) { requestContext.Abort(); } else { // We haven't sent a response yet, try to send a 500 Internal Server Error - requestContext.Response.Reset(); + requestContext.Response.Headers.Clear(); SetFatalResponse(requestContext, 500); } _application.DisposeContext(context, ex); diff --git a/src/Microsoft.AspNetCore.Server.WebListener/ResponseStream.cs b/src/Microsoft.AspNetCore.Server.WebListener/ResponseStream.cs new file mode 100644 index 0000000000..d641124c42 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.WebListener/ResponseStream.cs @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// ----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.WebListener +{ + internal class ResponseStream : Stream + { + private readonly Stream _innerStream; + private readonly Func _onStart; + + internal ResponseStream(Stream innerStream, Func onStart) + { + _innerStream = innerStream; + _onStart = onStart; + } + + public override bool CanRead => _innerStream.CanRead; + + public override bool CanSeek => _innerStream.CanSeek; + + public override bool CanWrite => _innerStream.CanWrite; + + public override long Length => _innerStream.Length; + + public override long Position + { + get { return _innerStream.Position; } + set { _innerStream.Position = value; } + } + + public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin); + + public override void SetLength(long value) => _innerStream.SetLength(value); + + public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); + +#if !NETSTANDARD1_3 + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return _innerStream.BeginRead(buffer, offset, count, callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + return _innerStream.EndRead(asyncResult); + } +#endif + public override void Flush() + { + _onStart().GetAwaiter().GetResult(); + _innerStream.Flush(); + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + await _onStart(); + await _innerStream.FlushAsync(cancellationToken); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _onStart().GetAwaiter().GetResult(); + _innerStream.Write(buffer, offset, count); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await _onStart(); + await _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + } +#if NETSTANDARD1_3 + public IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) +#else + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) +#endif + { + return ToIAsyncResult(WriteAsync(buffer, offset, count), callback, state); + } +#if NETSTANDARD1_3 + public void EndWrite(IAsyncResult asyncResult) +#else + public override void EndWrite(IAsyncResult asyncResult) +#endif + { + if (asyncResult == null) + { + throw new ArgumentNullException(nameof(asyncResult)); + } + ((Task)asyncResult).GetAwaiter().GetResult(); + } + + private static IAsyncResult ToIAsyncResult(Task task, AsyncCallback callback, object state) + { + var tcs = new TaskCompletionSource(state); + task.ContinueWith(t => + { + if (t.IsFaulted) + { + tcs.TrySetException(t.Exception.InnerExceptions); + } + else if (t.IsCanceled) + { + tcs.TrySetCanceled(); + } + else + { + tcs.TrySetResult(0); + } + + if (callback != null) + { + callback(tcs.Task); + } + }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); + return tcs.Task; + } + } +} diff --git a/src/Microsoft.Net.Http.Server/NativeInterop/TokenBindingUtil.cs b/src/Microsoft.Net.Http.Server/NativeInterop/TokenBindingUtil.cs index eeeb1ed395..2b6fe417be 100644 --- a/src/Microsoft.Net.Http.Server/NativeInterop/TokenBindingUtil.cs +++ b/src/Microsoft.Net.Http.Server/NativeInterop/TokenBindingUtil.cs @@ -30,7 +30,7 @@ namespace Microsoft.Net.Http.Server private static byte[] ExtractIdentifierBlob(TOKENBINDING_RESULT_DATA* pTokenBindingResultData) { // Per http://tools.ietf.org/html/draft-ietf-tokbind-protocol-00, Sec. 4, - // the identifer is a tuple which contains (token binding type, hash algorithm + // the identifier is a tuple which contains (token binding type, hash algorithm // signature algorithm, key data). We'll strip off the token binding type and // return the remainder (starting with the hash algorithm) as an opaque blob. byte[] retVal = new byte[checked(pTokenBindingResultData->identifierSize - 1)]; diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/BufferBuilder.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/BufferBuilder.cs deleted file mode 100644 index 5cb08d271d..0000000000 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/BufferBuilder.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.Net.Http.Server -{ - internal class BufferBuilder - { - private List> _buffers = new List>(); - - internal IEnumerable> Buffers - { - get { return _buffers; } - } - - internal int BufferCount - { - get { return _buffers.Count; } - } - - internal int TotalBytes { get; private set; } - - internal void Add(ArraySegment data) - { - _buffers.Add(data); - TotalBytes += data.Count; - } - - public void CopyAndAdd(ArraySegment data) - { - if (data.Count > 0) - { - var temp = new byte[data.Count]; - Buffer.BlockCopy(data.Array, data.Offset, temp, 0, data.Count); - _buffers.Add(new ArraySegment(temp)); - TotalBytes += data.Count; - } - } - - public void Clear() - { - _buffers.Clear(); - TotalBytes = 0; - } - } -} diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs index 1203105e3f..023d4edab8 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs @@ -26,11 +26,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; -using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using static Microsoft.Net.Http.Server.UnsafeNclNativeMethods; @@ -46,25 +44,12 @@ namespace Microsoft.Net.Http.Server private long _expectedBodyLength; private BoundaryType _boundaryType; private HttpApi.HTTP_RESPONSE_V2 _nativeResponse; - private IList, object>> _onStartingActions; - private IList, object>> _onCompletedActions; - - private bool _bufferingEnabled; internal Response(RequestContext requestContext) { // TODO: Verbose log RequestContext = requestContext; Headers = new HeaderCollection(); - Reset(); - } - - public void Reset() - { - if (_responseState >= ResponseState.StartedSending) - { - throw new InvalidOperationException("The response has already been sent. Request Aborted."); - } // We haven't started yet, or we're just buffered, we can clear any data, headers, and state so // that we can start over (e.g. to write an error message). _nativeResponse = new HttpApi.HTTP_RESPONSE_V2(); @@ -76,9 +61,6 @@ namespace Microsoft.Net.Http.Server _nativeResponse.Response_V1.Version.MajorVersion = 1; _nativeResponse.Response_V1.Version.MinorVersion = 1; _responseState = ResponseState.Created; - _onStartingActions = new List, object>>(); - _onCompletedActions = new List, object>>(); - _bufferingEnabled = RequestContext.Server.BufferResponses; _expectedBodyLength = 0; _nativeStream = null; _cacheTtl = null; @@ -88,9 +70,8 @@ namespace Microsoft.Net.Http.Server private enum ResponseState { Created, - Started, ComputedHeaders, - StartedSending, + Started, Closed, } @@ -124,16 +105,6 @@ namespace Microsoft.Net.Http.Server } } - public bool ShouldBuffer - { - get { return _bufferingEnabled; } - set - { - CheckResponseStarted(); - _bufferingEnabled = value; - } - } - public Stream Body { get @@ -279,8 +250,6 @@ namespace Microsoft.Net.Http.Server { return; } - Start(); - NotifyOnResponseCompleted(); // TODO: Verbose log EnsureResponseStream(); _nativeStream.Dispose(); @@ -292,11 +261,14 @@ namespace Microsoft.Net.Http.Server get { return _boundaryType; } } + internal bool HasComputedHeaders + { + get { return _responseState >= ResponseState.ComputedHeaders; } + } + /// /// Indicates if the response status, reason, and headers are prepared to send and can - /// no longer be modified. This is caused by the first write to the response body. However, - /// the response may not have been flushed to the network yet if the body is buffered. - /// See HasStartedSending. + /// no longer be modified. This is caused by the first write or flush to the response body. /// public bool HasStarted { @@ -311,19 +283,6 @@ namespace Microsoft.Net.Http.Server } } - internal bool ComputedHeaders - { - get { return _responseState >= ResponseState.ComputedHeaders; } - } - - /// - /// Indicates the initial response has been flushed to the network and can no longer be modified or Reset. - /// - public bool HasStartedSending - { - get { return _responseState >= ResponseState.StartedSending; } - } - private void EnsureResponseStream() { if (_nativeStream == null) @@ -367,9 +326,9 @@ namespace Microsoft.Net.Http.Server HttpApi.HTTP_FLAGS flags, bool isOpaqueUpgrade) { - Debug.Assert(!HasStartedSending, "HttpListenerResponse::SendHeaders()|SentHeaders is true."); + Debug.Assert(!HasStarted, "HttpListenerResponse::SendHeaders()|SentHeaders is true."); - _responseState = ResponseState.StartedSending; + _responseState = ResponseState.Started; var reasonPhrase = GetReasonPhrase(StatusCode); /* @@ -448,21 +407,8 @@ namespace Microsoft.Net.Http.Server return statusCode; } - internal void Start() + internal HttpApi.HTTP_FLAGS ComputeHeaders(bool endOfRequest = false) { - if (!HasStarted) - { - // Notify that this is absolutely the last chance to make changes. - NotifyOnSendingHeaders(); - Headers.IsReadOnly = true; // Prohibit further modifications. - _responseState = ResponseState.Started; - } - } - - internal HttpApi.HTTP_FLAGS ComputeHeaders(bool endOfRequest = false, int bufferedBytes = 0) - { - Headers.IsReadOnly = false; // Temporarily allow modification. - // 401 if (StatusCode == (ushort)HttpStatusCode.Unauthorized) { @@ -470,7 +416,7 @@ namespace Microsoft.Net.Http.Server } var flags = HttpApi.HTTP_FLAGS.NONE; - Debug.Assert(!ComputedHeaders, "HttpListenerResponse::ComputeHeaders()|ComputedHeaders is true."); + Debug.Assert(!HasComputedHeaders, nameof(HasComputedHeaders) + " is true."); _responseState = ResponseState.ComputedHeaders; // Gather everything from the request that affects the response: @@ -511,16 +457,12 @@ namespace Microsoft.Net.Http.Server } else if (endOfRequest && !(isHeadRequest && statusCanHaveBody)) // HEAD requests should always end without a body. Assume a GET response would have a body. { - if (bufferedBytes > 0) - { - Headers[HttpKnownHeaderNames.ContentLength] = bufferedBytes.ToString(CultureInfo.InvariantCulture); - } - else if (statusCanHaveBody) + if (statusCanHaveBody) { Headers[HttpKnownHeaderNames.ContentLength] = Constants.Zero; } _boundaryType = BoundaryType.ContentLength; - _expectedBodyLength = bufferedBytes; + _expectedBodyLength = 0; } else if (keepConnectionAlive && requestVersion == Constants.V1_1) { @@ -726,8 +668,6 @@ namespace Microsoft.Net.Http.Server // Subset of ComputeHeaders internal void SendOpaqueUpgrade() { - // Notify that this is absolutely the last chance to make changes. - Start(); _boundaryType = BoundaryType.Close; // TODO: Send headers async? @@ -765,70 +705,7 @@ namespace Microsoft.Net.Http.Server internal void SwitchToOpaqueMode() { EnsureResponseStream(); - _bufferingEnabled = false; _nativeStream.SwitchToOpaqueMode(); } - - public void OnStarting(Func callback, object state) - { - var actions = _onStartingActions; - if (actions == null) - { - throw new InvalidOperationException("Response already started"); - } - - actions.Add(new Tuple, object>(callback, state)); - } - - public void OnCompleted(Func callback, object state) - { - var actions = _onCompletedActions; - if (actions == null) - { - throw new InvalidOperationException("Response already completed"); - } - - actions.Add(new Tuple, object>(callback, state)); - } - - private void NotifyOnSendingHeaders() - { - var actions = Interlocked.Exchange(ref _onStartingActions, null); - if (actions == null) - { - // Something threw the first time, do not try again. - return; - } - - // Execute last to first. This mimics a stack unwind. - foreach (var actionPair in actions.Reverse()) - { - actionPair.Item1(actionPair.Item2); - } - } - - private void NotifyOnResponseCompleted() - { - var actions = Interlocked.Exchange(ref _onCompletedActions, null); - if (actions == null) - { - // Something threw the first time, do not try again. - return; - } - - foreach (var actionPair in actions) - { - try - { - actionPair.Item1(actionPair.Item2); - } - catch (Exception ex) - { - RequestContext.Logger.LogWarning( - String.Format(Resources.Warning_ExceptionInOnResponseCompletedAction, nameof(OnCompleted)), - ex); - } - } - } } } diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs index 1b3c94883a..04e8f16e64 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs @@ -23,8 +23,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.IO; using System.Runtime.InteropServices; using System.Threading; @@ -36,13 +36,10 @@ namespace Microsoft.Net.Http.Server { internal class ResponseStream : Stream { - private const int MaxBufferSize = 4 * 1024; - private RequestContext _requestContext; private long _leftToWrite = long.MinValue; private bool _closed; private bool _inOpaqueMode; - private BufferBuilder _buffer = new BufferBuilder(); // The last write needs special handling to cancel. private ResponseStreamAsyncResult _lastWrite; @@ -117,18 +114,20 @@ namespace Microsoft.Net.Http.Server FlushInternal(endOfRequest: false); } - private unsafe void FlushInternal(bool endOfRequest) + // We never expect endOfRequest and data at the same time + private unsafe void FlushInternal(bool endOfRequest, ArraySegment data = new ArraySegment()) { - bool startedSending = _requestContext.Response.HasStartedSending; - var byteCount = _buffer.TotalBytes; - if (byteCount == 0 && startedSending && !endOfRequest) + Debug.Assert(!(endOfRequest && data.Count > 0), "Data is not supported at the end of the request."); + + var started = _requestContext.Response.HasStarted; + if (data.Count == 0 && started && !endOfRequest) { // Empty flush return; } var flags = ComputeLeftToWrite(endOfRequest); - if (!_inOpaqueMode && endOfRequest && _leftToWrite > byteCount) + if (!_inOpaqueMode && endOfRequest && _leftToWrite > data.Count) { _requestContext.Abort(); // This is logged rather than thrown because it is too late for an exception to be visible in user code. @@ -140,18 +139,18 @@ namespace Microsoft.Net.Http.Server { flags |= HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT; } - else if (!endOfRequest && _leftToWrite != byteCount) + else if (!endOfRequest && _leftToWrite != data.Count) { flags |= HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA; } - UpdateWritenCount((uint)byteCount); + UpdateWritenCount((uint)data.Count); uint statusCode = 0; HttpApi.HTTP_DATA_CHUNK[] dataChunks; - var pinnedBuffers = PinDataBuffers(endOfRequest, out dataChunks); + var pinnedBuffers = PinDataBuffers(endOfRequest, data, out dataChunks); try { - if (!startedSending) + if (!started) { statusCode = _requestContext.Response.SendHeaders(dataChunks, null, flags, false); } @@ -181,7 +180,6 @@ namespace Microsoft.Net.Http.Server finally { FreeDataBuffers(pinnedBuffers); - _buffer.Clear(); } if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_HANDLE_EOF @@ -195,27 +193,27 @@ namespace Microsoft.Net.Http.Server } } - private List PinDataBuffers(bool endOfRequest, out HttpApi.HTTP_DATA_CHUNK[] dataChunks) + private List PinDataBuffers(bool endOfRequest, ArraySegment data, out HttpApi.HTTP_DATA_CHUNK[] dataChunks) { var pins = new List(); var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked; var currentChunk = 0; // Figure out how many data chunks - if (chunked && _buffer.TotalBytes == 0 && endOfRequest) + if (chunked && data.Count == 0 && endOfRequest) { dataChunks = new HttpApi.HTTP_DATA_CHUNK[1]; SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment(Helpers.ChunkTerminator)); return pins; } - else if (_buffer.TotalBytes == 0) + else if (data.Count == 0) { // No data dataChunks = new HttpApi.HTTP_DATA_CHUNK[0]; return pins; } - var chunkCount = _buffer.BufferCount; + var chunkCount = 1; if (chunked) { // Chunk framing @@ -231,14 +229,11 @@ namespace Microsoft.Net.Http.Server if (chunked) { - var chunkHeaderBuffer = Helpers.GetChunkHeader(_buffer.TotalBytes); + var chunkHeaderBuffer = Helpers.GetChunkHeader(data.Count); SetDataChunk(dataChunks, ref currentChunk, pins, chunkHeaderBuffer); } - foreach (var buffer in _buffer.Buffers) - { - SetDataChunk(dataChunks, ref currentChunk, pins, buffer); - } + SetDataChunk(dataChunks, ref currentChunk, pins, data); if (chunked) { @@ -274,18 +269,20 @@ namespace Microsoft.Net.Http.Server } } - - // Simpler than Flush because it will never be called at the end of the request from Dispose. - public unsafe override Task FlushAsync(CancellationToken cancellationToken) + public override Task FlushAsync(CancellationToken cancellationToken) { if (_closed) { return Helpers.CompletedTask(); } + return FlushInternalAsync(new ArraySegment(), cancellationToken); + } - bool startedSending = _requestContext.Response.HasStartedSending; - var byteCount = _buffer.TotalBytes; - if (byteCount == 0 && startedSending) + // Simpler than Flush because it will never be called at the end of the request from Dispose. + private unsafe Task FlushInternalAsync(ArraySegment data, CancellationToken cancellationToken) + { + var started = _requestContext.Response.HasStarted; + if (data.Count == 0 && started) { // Empty flush return Helpers.CompletedTask(); @@ -298,19 +295,19 @@ namespace Microsoft.Net.Http.Server } var flags = ComputeLeftToWrite(); - if (_leftToWrite != byteCount) + if (_leftToWrite != data.Count) { flags |= HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA; } - UpdateWritenCount((uint)byteCount); + UpdateWritenCount((uint)data.Count); uint statusCode = 0; var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked; - var asyncResult = new ResponseStreamAsyncResult(this, _buffer, chunked, cancellationRegistration); + var asyncResult = new ResponseStreamAsyncResult(this, data, chunked, cancellationRegistration); uint bytesSent = 0; try { - if (!startedSending) + if (!started) { statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false); bytesSent = asyncResult.BytesSent; @@ -341,7 +338,7 @@ namespace Microsoft.Net.Http.Server if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_IO_PENDING) { asyncResult.Dispose(); - if (_requestContext.Server.IgnoreWriteExceptions && startedSending) + if (_requestContext.Server.IgnoreWriteExceptions && started) { asyncResult.Complete(); } @@ -408,10 +405,10 @@ namespace Microsoft.Net.Http.Server private HttpApi.HTTP_FLAGS ComputeLeftToWrite(bool endOfRequest = false) { - HttpApi.HTTP_FLAGS flags = HttpApi.HTTP_FLAGS.NONE; - if (!_requestContext.Response.ComputedHeaders) + var flags = HttpApi.HTTP_FLAGS.NONE; + if (!_requestContext.Response.HasComputedHeaders) { - flags = _requestContext.Response.ComputeHeaders(endOfRequest, _buffer.TotalBytes); + flags = _requestContext.Response.ComputeHeaders(endOfRequest); } if (_leftToWrite == long.MinValue) { @@ -437,47 +434,31 @@ namespace Microsoft.Net.Http.Server var data = new ArraySegment(buffer, offset, count); CheckDisposed(); // TODO: Verbose log parameters - // Officially starts the response and fires OnSendingHeaders - _requestContext.Response.Start(); - var currentBytes = _buffer.TotalBytes + data.Count; var contentLength = _requestContext.Response.ContentLength; - if (contentLength.HasValue && !_requestContext.Response.ComputedHeaders && contentLength.Value <= currentBytes) + if (contentLength.HasValue && !_requestContext.Response.HasComputedHeaders && contentLength.Value <= data.Count) { - if (contentLength.Value < currentBytes) + if (contentLength.Value < data.Count) { throw new InvalidOperationException("More bytes written than specified in the Content-Length header."); } - // or the last write in a response that hasn't started yet, flush immideately - _buffer.Add(data); - Flush(); } - // The last write in a response that has already started, flush immidately - else if (_requestContext.Response.ComputedHeaders && _leftToWrite >= 0 && _leftToWrite <= currentBytes) + // The last write in a response that has already started, flush immediately + else if (_requestContext.Response.HasComputedHeaders && _leftToWrite >= 0 && _leftToWrite <= data.Count) { - if (_leftToWrite < currentBytes) + if (_leftToWrite < data.Count) { throw new InvalidOperationException("More bytes written than specified in the Content-Length header."); } - _buffer.Add(data); - Flush(); - } - else if (_requestContext.Response.ShouldBuffer && currentBytes < MaxBufferSize) - { - _buffer.CopyAndAdd(data); - } - else - { - // Append to existing data without a copy, and then flush immidately - _buffer.Add(data); - Flush(); } + + FlushInternal(endOfRequest: false, data: data); } #if NETSTANDARD1_3 - public unsafe IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + public IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) #else - public override unsafe IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) #endif { return WriteAsync(buffer, offset, count).ToIAsyncResult(callback, state); @@ -490,7 +471,7 @@ namespace Microsoft.Net.Http.Server { if (asyncResult == null) { - throw new ArgumentNullException("asyncResult"); + throw new ArgumentNullException(nameof(asyncResult)); } ((Task)asyncResult).GetAwaiter().GetResult(); } @@ -505,42 +486,25 @@ namespace Microsoft.Net.Http.Server } CheckDisposed(); // TODO: Verbose log parameters - // Officially starts the response and fires OnSendingHeaders - _requestContext.Response.Start(); - var currentBytes = _buffer.TotalBytes + data.Count; var contentLength = _requestContext.Response.ContentLength; - if (contentLength.HasValue && !_requestContext.Response.ComputedHeaders && contentLength.Value <= currentBytes) + if (contentLength.HasValue && !_requestContext.Response.HasComputedHeaders && contentLength.Value <= data.Count) { - if (contentLength.Value < currentBytes) + if (contentLength.Value < data.Count) { throw new InvalidOperationException("More bytes written than specified in the Content-Length header."); } - // The last write in a response that hasn't started yet, flush immideately - _buffer.Add(data); - return FlushAsync(cancellationToken); } - // The last write in a response that has already started, flush immidately - else if (_requestContext.Response.ComputedHeaders && _leftToWrite > 0 && _leftToWrite <= currentBytes) + // The last write in a response that has already started, flush immediately + else if (_requestContext.Response.HasComputedHeaders && _leftToWrite > 0 && _leftToWrite <= data.Count) { - if (_leftToWrite < currentBytes) + if (_leftToWrite < data.Count) { throw new InvalidOperationException("More bytes written than specified in the Content-Length header."); } - _buffer.Add(data); - return FlushAsync(cancellationToken); - } - else if (_requestContext.Response.ShouldBuffer && currentBytes < MaxBufferSize) - { - _buffer.CopyAndAdd(data); - return Helpers.CompletedTask(); - } - else - { - // Append to existing data without a copy, and then flush immidately - _buffer.Add(data); - return FlushAsync(cancellationToken); } + + return FlushInternalAsync(data, cancellationToken); } internal async Task SendFileAsync(string fileName, long offset, long? count, CancellationToken cancellationToken) @@ -552,20 +516,14 @@ namespace Microsoft.Net.Http.Server throw new ArgumentNullException("fileName"); } CheckDisposed(); - if (_buffer.TotalBytes > 0) - { - // SendFileAsync is primarly used for full responses so we don't optimize this partialy buffered scenario. - // In theory we could merge SendFileAsyncCore into FlushAsync[Internal] and send the buffered data in the same call as the file. - await FlushAsync(cancellationToken); - } - // We can't mix await and unsafe so seperate the unsafe code into another method. + + // We can't mix await and unsafe so separate the unsafe code into another method. await SendFileAsyncCore(fileName, offset, count, cancellationToken); } internal unsafe Task SendFileAsyncCore(string fileName, long offset, long? count, CancellationToken cancellationToken) { - _requestContext.Response.Start(); - HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite(); + var flags = ComputeLeftToWrite(); if (count == 0 && _leftToWrite != 0) { return Helpers.CompletedTask(); @@ -589,7 +547,7 @@ namespace Microsoft.Net.Http.Server uint statusCode; uint bytesSent = 0; - bool startedSending = _requestContext.Response.HasStartedSending; + var started = _requestContext.Response.HasStarted; var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked; ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, fileName, offset, count, chunked, cancellationRegistration); @@ -612,7 +570,7 @@ namespace Microsoft.Net.Http.Server try { - if (!startedSending) + if (!started) { statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false); bytesSent = asyncResult.BytesSent; @@ -644,7 +602,7 @@ namespace Microsoft.Net.Http.Server if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING) { asyncResult.Dispose(); - if (_requestContext.Server.IgnoreWriteExceptions && startedSending) + if (_requestContext.Server.IgnoreWriteExceptions && started) { asyncResult.Complete(); } diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs index 12af57f093..73fa62ff28 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs @@ -50,14 +50,14 @@ namespace Microsoft.Net.Http.Server _cancellationRegistration = cancellationRegistration; } - internal ResponseStreamAsyncResult(ResponseStream responseStream, BufferBuilder buffer, bool chunked, + internal ResponseStreamAsyncResult(ResponseStream responseStream, ArraySegment data, bool chunked, CancellationTokenRegistration cancellationRegistration) : this(responseStream, cancellationRegistration) { var boundHandle = _responseStream.RequestContext.Server.RequestQueue.BoundHandle; object[] objectsToPin; - if (buffer.TotalBytes == 0) + if (data.Count == 0) { _dataChunks = null; _overlapped = new SafeNativeOverlapped(boundHandle, @@ -65,7 +65,7 @@ namespace Microsoft.Net.Http.Server return; } - _dataChunks = new HttpApi.HTTP_DATA_CHUNK[buffer.BufferCount + (chunked ? 2 : 0)]; + _dataChunks = new HttpApi.HTTP_DATA_CHUNK[1 + (chunked ? 2 : 0)]; objectsToPin = new object[_dataChunks.Length + 1]; objectsToPin[0] = _dataChunks; var currentChunk = 0; @@ -74,14 +74,11 @@ namespace Microsoft.Net.Http.Server var chunkHeaderBuffer = new ArraySegment(); if (chunked) { - chunkHeaderBuffer = Helpers.GetChunkHeader(buffer.TotalBytes); + chunkHeaderBuffer = Helpers.GetChunkHeader(data.Count); SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, chunkHeaderBuffer); } - foreach (var segment in buffer.Buffers) - { - SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, segment); - } + SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, data); if (chunked) { @@ -98,19 +95,15 @@ namespace Microsoft.Net.Http.Server _dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer.Array, chunkHeaderBuffer.Offset); currentChunk++; } - foreach (var segment in buffer.Buffers) - { - _dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(segment.Array, segment.Offset); - currentChunk++; - } + + _dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(data.Array, data.Offset); + currentChunk++; + if (chunked) { _dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(Helpers.CRLF, 0); currentChunk++; } - - // We've captured a reference to all the buffers, clear the buffer so that it can be used to queue overlapped writes. - buffer.Clear(); } internal ResponseStreamAsyncResult(ResponseStream responseStream, string fileName, long offset, diff --git a/src/Microsoft.Net.Http.Server/WebListener.cs b/src/Microsoft.Net.Http.Server/WebListener.cs index be2e30dbb8..5f92de5fae 100644 --- a/src/Microsoft.Net.Http.Server/WebListener.cs +++ b/src/Microsoft.Net.Http.Server/WebListener.cs @@ -72,8 +72,6 @@ namespace Microsoft.Net.Http.Server // The native request queue private long? _requestQueueLength; - private bool _bufferResponses = true; - public WebListener() : this(null) { @@ -169,12 +167,6 @@ namespace Microsoft.Net.Http.Server get { return _urlPrefixes; } } - public bool BufferResponses - { - get { return _bufferResponses; } - set { _bufferResponses = value; } - } - /// /// Exposes the Http.Sys timeout configurations. These may also be configured in the registry. /// diff --git a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/OpaqueUpgradeTests.cs b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/OpaqueUpgradeTests.cs index 191208da1e..a311b6b770 100644 --- a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/OpaqueUpgradeTests.cs +++ b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/OpaqueUpgradeTests.cs @@ -115,6 +115,40 @@ namespace Microsoft.AspNetCore.Server.WebListener } } + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)] + public async Task OpaqueUpgrade_WithOnStarting_CallbackCalled() + { + var callbackCalled = false; + var waitHandle = new ManualResetEvent(false); + bool? upgraded = null; + string address; + using (Utilities.CreateHttpServer(out address, async httpContext => + { + httpContext.Response.OnStarting(_ => + { + callbackCalled = true; + return Task.FromResult(0); + }, null); + httpContext.Response.Headers["Upgrade"] = "websocket"; // Win8.1 blocks anything but WebSockets + var opaqueFeature = httpContext.Features.Get(); + Assert.NotNull(opaqueFeature); + Assert.True(opaqueFeature.IsUpgradableRequest); + await opaqueFeature.UpgradeAsync(); + upgraded = true; + waitHandle.Set(); + })) + { + using (Stream stream = await SendOpaqueRequestAsync("GET", address)) + { + Assert.True(waitHandle.WaitOne(TimeSpan.FromSeconds(1)), "Timed out"); + Assert.True(upgraded.HasValue, "Upgraded not set"); + Assert.True(upgraded.Value, "Upgrade failed"); + Assert.True(callbackCalled, "Callback not called"); + } + } + } + [ConditionalTheory] [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)] // See HTTP_VERB for known verbs diff --git a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ResponseBodyTests.cs b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ResponseBodyTests.cs index 7f6122dccc..9df0c89089 100644 --- a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ResponseBodyTests.cs +++ b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ResponseBodyTests.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.WebListener public class ResponseBodyTests { [Fact] - public async Task ResponseBody_WriteNoHeaders_BuffersAndSetsContentLength() + public async Task ResponseBody_WriteNoHeaders_SetsChunked() { string address; using (Utilities.CreateHttpServer(out address, httpContext => @@ -39,12 +39,12 @@ namespace Microsoft.AspNetCore.Server.WebListener return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10); })) { - HttpResponseMessage response = await SendRequestAsync(address); + var response = await SendRequestAsync(address); Assert.Equal(200, (int)response.StatusCode); Assert.Equal(new Version(1, 1), response.Version); IEnumerable ignored; - Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); - Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked"); + Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); + Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked"); Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync()); } } @@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Server.WebListener await httpContext.Response.Body.FlushAsync(); })) { - HttpResponseMessage response = await SendRequestAsync(address); + var response = await SendRequestAsync(address); Assert.Equal(200, (int)response.StatusCode); Assert.Equal(new Version(1, 1), response.Version); IEnumerable ignored; @@ -82,7 +82,7 @@ namespace Microsoft.AspNetCore.Server.WebListener await stream.WriteAsync(responseBytes, 0, responseBytes.Length); })) { - HttpResponseMessage response = await SendRequestAsync(address); + var response = await SendRequestAsync(address); Assert.Equal(200, (int)response.StatusCode); Assert.Equal(new Version(1, 1), response.Version); IEnumerable ignored; @@ -109,7 +109,7 @@ namespace Microsoft.AspNetCore.Server.WebListener await stream.WriteAsync(new byte[10], 0, 10); })) { - HttpResponseMessage response = await SendRequestAsync(address); + var response = await SendRequestAsync(address); Assert.Equal(200, (int)response.StatusCode); Assert.Equal(new Version(1, 1), response.Version); IEnumerable contentLength; @@ -152,24 +152,26 @@ namespace Microsoft.AspNetCore.Server.WebListener [Fact] public async Task ResponseBody_WriteContentLengthTooMuchWritten_Throws() { + var completed = false; string address; using (Utilities.CreateHttpServer(out address, httpContext => { httpContext.Response.Headers["Content-lenGth"] = " 10 "; httpContext.Response.Body.Write(new byte[5], 0, 5); - httpContext.Response.Body.Write(new byte[6], 0, 6); + Assert.Throws(() => httpContext.Response.Body.Write(new byte[6], 0, 6)); + completed = true; return Task.FromResult(0); })) { - var response = await SendRequestAsync(address); - Assert.Equal(500, (int)response.StatusCode); + await Assert.ThrowsAsync(() => SendRequestAsync(address)); + Assert.True(completed); } } [Fact] public async Task ResponseBody_WriteContentLengthExtraWritten_Throws() { - ManualResetEvent waitHandle = new ManualResetEvent(false); + var waitHandle = new ManualResetEvent(false); bool? appThrew = null; string address; using (Utilities.CreateHttpServer(out address, httpContext => @@ -205,6 +207,89 @@ namespace Microsoft.AspNetCore.Server.WebListener } } + [Fact] + public async Task ResponseBody_Write_TriggersOnStarting() + { + var onStartingCalled = false; + string address; + using (Utilities.CreateHttpServer(out address, httpContext => + { + httpContext.Response.OnStarting(state => + { + onStartingCalled = true; + Assert.Same(state, httpContext); + return Task.FromResult(0); + }, httpContext); + httpContext.Response.Body.Write(new byte[10], 0, 10); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(address); + Assert.Equal(200, (int)response.StatusCode); + Assert.Equal(new Version(1, 1), response.Version); + Assert.True(onStartingCalled); + IEnumerable ignored; + Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); + Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked"); + Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync()); + } + } +#if NET451 + [Fact] + public async Task ResponseBody_BeginWrite_TriggersOnStarting() + { + var onStartingCalled = false; + string address; + using (Utilities.CreateHttpServer(out address, httpContext => + { + httpContext.Response.OnStarting(state => + { + onStartingCalled = true; + Assert.Same(state, httpContext); + return Task.FromResult(0); + }, httpContext); + httpContext.Response.Body.EndWrite(httpContext.Response.Body.BeginWrite(new byte[10], 0, 10, null, null)); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(address); + Assert.Equal(200, (int)response.StatusCode); + Assert.Equal(new Version(1, 1), response.Version); + Assert.True(onStartingCalled); + IEnumerable ignored; + Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); + Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked"); + Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync()); + } + } +#endif + [Fact] + public async Task ResponseBody_WriteAsync_TriggersOnStarting() + { + var onStartingCalled = false; + string address; + using (Utilities.CreateHttpServer(out address, httpContext => + { + httpContext.Response.OnStarting(state => + { + onStartingCalled = true; + Assert.Same(state, httpContext); + return Task.FromResult(0); + }, httpContext); + return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10); + })) + { + var response = await SendRequestAsync(address); + Assert.Equal(200, (int)response.StatusCode); + Assert.Equal(new Version(1, 1), response.Version); + Assert.True(onStartingCalled); + IEnumerable ignored; + Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); + Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked"); + Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync()); + } + } + private async Task SendRequestAsync(string uri) { using (HttpClient client = new HttpClient()) diff --git a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ResponseSendFileTests.cs b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ResponseSendFileTests.cs index 3c39c77764..7cc9269f12 100644 --- a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ResponseSendFileTests.cs +++ b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ResponseSendFileTests.cs @@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.Server.WebListener return Task.FromResult(0); })) { - HttpResponseMessage response = await SendRequestAsync(address); + var response = await SendRequestAsync(address); Assert.Equal(200, (int)response.StatusCode); IEnumerable ignored; Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); @@ -86,7 +86,7 @@ namespace Microsoft.AspNetCore.Server.WebListener [Fact] public async Task ResponseSendFile_MissingFile_Throws() { - ManualResetEvent waitHandle = new ManualResetEvent(false); + var waitHandle = new ManualResetEvent(false); bool? appThrew = null; string address; using (Utilities.CreateHttpServer(out address, httpContext => @@ -165,7 +165,7 @@ namespace Microsoft.AspNetCore.Server.WebListener return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); })) { - HttpResponseMessage response = await SendRequestAsync(address); + var response = await SendRequestAsync(address); Assert.Equal(200, (int)response.StatusCode); IEnumerable contentLength; Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); @@ -185,7 +185,7 @@ namespace Microsoft.AspNetCore.Server.WebListener return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); })) { - HttpResponseMessage response = await SendRequestAsync(address); + var response = await SendRequestAsync(address); Assert.Equal(200, (int)response.StatusCode); IEnumerable contentLength; Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); @@ -204,7 +204,7 @@ namespace Microsoft.AspNetCore.Server.WebListener return sendFile.SendFileAsync(AbsoluteFilePath, 0, FileLength / 2, CancellationToken.None); })) { - HttpResponseMessage response = await SendRequestAsync(address); + var response = await SendRequestAsync(address); Assert.Equal(200, (int)response.StatusCode); IEnumerable contentLength; Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); @@ -216,30 +216,34 @@ namespace Microsoft.AspNetCore.Server.WebListener [Fact] public async Task ResponseSendFile_OffsetOutOfRange_Throws() { + var completed = false; string address; - using (Utilities.CreateHttpServer(out address, httpContext => + using (Utilities.CreateHttpServer(out address, async httpContext => { var sendFile = httpContext.Features.Get(); - return sendFile.SendFileAsync(AbsoluteFilePath, 1234567, null, CancellationToken.None); + await sendFile.SendFileAsync(AbsoluteFilePath, 1234567, null, CancellationToken.None); + completed = true; })) { - HttpResponseMessage response = await SendRequestAsync(address); - Assert.Equal(500, (int)response.StatusCode); + await Assert.ThrowsAsync(() => SendRequestAsync(address)); + Assert.False(completed); } } [Fact] public async Task ResponseSendFile_CountOutOfRange_Throws() { + var completed = false; string address; - using (Utilities.CreateHttpServer(out address, httpContext => + using (Utilities.CreateHttpServer(out address, async httpContext => { var sendFile = httpContext.Features.Get(); - return sendFile.SendFileAsync(AbsoluteFilePath, 0, 1234567, CancellationToken.None); + await sendFile.SendFileAsync(AbsoluteFilePath, 0, 1234567, CancellationToken.None); + completed = true; })) { - HttpResponseMessage response = await SendRequestAsync(address); - Assert.Equal(500, (int)response.StatusCode); + await Assert.ThrowsAsync(() => SendRequestAsync(address)); + Assert.False(completed); } } @@ -253,7 +257,7 @@ namespace Microsoft.AspNetCore.Server.WebListener return sendFile.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None); })) { - HttpResponseMessage response = await SendRequestAsync(address); + var response = await SendRequestAsync(address); Assert.Equal(200, (int)response.StatusCode); IEnumerable contentLength; Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); @@ -273,7 +277,7 @@ namespace Microsoft.AspNetCore.Server.WebListener return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); })) { - HttpResponseMessage response = await SendRequestAsync(address); + var response = await SendRequestAsync(address); Assert.Equal(200, (int)response.StatusCode); IEnumerable contentLength; Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); @@ -294,7 +298,7 @@ namespace Microsoft.AspNetCore.Server.WebListener return sendFile.SendFileAsync(AbsoluteFilePath, 0, 10, CancellationToken.None); })) { - HttpResponseMessage response = await SendRequestAsync(address); + var response = await SendRequestAsync(address); Assert.Equal(200, (int)response.StatusCode); IEnumerable contentLength; Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); @@ -315,7 +319,7 @@ namespace Microsoft.AspNetCore.Server.WebListener return sendFile.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None); })) { - HttpResponseMessage response = await SendRequestAsync(address); + var response = await SendRequestAsync(address); Assert.Equal(200, (int)response.StatusCode); IEnumerable contentLength; Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); @@ -325,6 +329,34 @@ namespace Microsoft.AspNetCore.Server.WebListener } } + [Fact] + public async Task ResponseSendFile_TriggersOnStarting() + { + var onStartingCalled = false; + string address; + using (Utilities.CreateHttpServer(out address, httpContext => + { + httpContext.Response.OnStarting(state => + { + onStartingCalled = true; + Assert.Same(state, httpContext); + return Task.FromResult(0); + }, httpContext); + var sendFile = httpContext.Features.Get(); + return sendFile.SendFileAsync(AbsoluteFilePath, 0, 10, CancellationToken.None); + })) + { + var response = await SendRequestAsync(address); + Assert.Equal(200, (int)response.StatusCode); + Assert.Equal(new Version(1, 1), response.Version); + Assert.True(onStartingCalled); + IEnumerable ignored; + Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); + Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked"); + Assert.Equal(10, (await response.Content.ReadAsByteArrayAsync()).Length); + } + } + private async Task SendRequestAsync(string uri) { using (HttpClient client = new HttpClient()) diff --git a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ResponseTests.cs b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ResponseTests.cs index 51aeb4a053..8fcbe69d4e 100644 --- a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ResponseTests.cs +++ b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ResponseTests.cs @@ -16,6 +16,7 @@ // permissions and limitations under the License. using System; +using System.IO; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -102,7 +103,7 @@ namespace Microsoft.AspNetCore.Server.WebListener } [Fact] - public async Task Response_100_Throws() + public async Task Response_StatusCode100_Throws() { string address; using (Utilities.CreateHttpServer(out address, httpContext => @@ -117,7 +118,7 @@ namespace Microsoft.AspNetCore.Server.WebListener } [Fact] - public async Task Response_0_Throws() + public async Task Response_StatusCode0_Throws() { string address; using (Utilities.CreateHttpServer(out address, httpContext => @@ -131,9 +132,98 @@ namespace Microsoft.AspNetCore.Server.WebListener } } + [Fact] + public async Task Response_Empty_CallsOnStartingAndOnCompleted() + { + var onStartingCalled = false; + var onCompletedCalled = false; + string address; + using (Utilities.CreateHttpServer(out address, httpContext => + { + httpContext.Response.OnStarting(state => + { + onStartingCalled = true; + Assert.Same(state, httpContext); + return Task.FromResult(0); + }, httpContext); + httpContext.Response.OnCompleted(state => + { + onCompletedCalled = true; + Assert.Same(state, httpContext); + return Task.FromResult(0); + }, httpContext); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(address); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(onStartingCalled); + Assert.True(onCompletedCalled); + } + } + + [Fact] + public async Task Response_OnStartingThrows_StillCallsOnCompleted() + { + var onStartingCalled = false; + var onCompletedCalled = false; + string address; + using (Utilities.CreateHttpServer(out address, httpContext => + { + httpContext.Response.OnStarting(state => + { + onStartingCalled = true; + throw new Exception("Failed OnStarting"); + }, httpContext); + httpContext.Response.OnCompleted(state => + { + onCompletedCalled = true; + Assert.Same(state, httpContext); + return Task.FromResult(0); + }, httpContext); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(address); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.True(onStartingCalled); + Assert.True(onCompletedCalled); + } + } + + [Fact] + public async Task Response_OnStartingThrowsAfterWrite_WriteThrowsAndStillCallsOnCompleted() + { + var onStartingCalled = false; + var onCompletedCalled = false; + string address; + using (Utilities.CreateHttpServer(out address, httpContext => + { + httpContext.Response.OnStarting(state => + { + onStartingCalled = true; + throw new InvalidTimeZoneException("Failed OnStarting"); + }, httpContext); + httpContext.Response.OnCompleted(state => + { + onCompletedCalled = true; + Assert.Same(state, httpContext); + return Task.FromResult(0); + }, httpContext); + Assert.Throws(() => httpContext.Response.Body.Write(new byte[10], 0, 10)); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(address); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(onStartingCalled); + Assert.True(onCompletedCalled); + } + } + private async Task SendRequestAsync(string uri) { - using (HttpClient client = new HttpClient()) + using (var client = new HttpClient()) { return await client.GetAsync(uri); } diff --git a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/WebSocketTests.cs b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/WebSocketTests.cs index 896dc42a57..69b28cd960 100644 --- a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/WebSocketTests.cs +++ b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/WebSocketTests.cs @@ -110,6 +110,39 @@ namespace Microsoft.AspNetCore.Server.WebListener } } + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)] + public async Task WebSocketAccept_WithOnStarting_CallbackCalled() + { + var callbackCalled = false; + var waitHandle = new ManualResetEvent(false); + bool? upgraded = null; + string address; + using (Utilities.CreateHttpServer(out address, async httpContext => + { + httpContext.Response.OnStarting(_ => + { + callbackCalled = true; + return Task.FromResult(0); + }, null); + var webSocketFeature = httpContext.Features.Get(); + Assert.NotNull(webSocketFeature); + Assert.True(webSocketFeature.IsWebSocketRequest); + await webSocketFeature.AcceptAsync(null); + upgraded = true; + waitHandle.Set(); + })) + { + using (WebSocket clientWebSocket = await SendWebSocketRequestAsync(ConvertToWebSocketAddress(address))) + { + Assert.True(waitHandle.WaitOne(TimeSpan.FromSeconds(1)), "Timed out"); + Assert.True(upgraded.HasValue, "Upgraded not set"); + Assert.True(upgraded.Value, "Upgrade failed"); + Assert.True(callbackCalled, "Callback not called"); + } + } + } + [ConditionalFact] [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)] public async Task WebSocketAccept_SendAndReceive_Success() diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs index 9a802091f1..3a2b14fefe 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs @@ -16,7 +16,7 @@ namespace Microsoft.Net.Http.Server public class ResponseBodyTests { [Fact] - public async Task ResponseBody_BufferWriteNoHeaders_DefaultsToContentLength() + public async Task ResponseBody_WriteNoHeaders_DefaultsToChunked() { string address; using (var server = Utilities.CreateHttpServer(out address)) @@ -24,31 +24,6 @@ namespace Microsoft.Net.Http.Server Task responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); - context.Response.ShouldBuffer = true; - context.Response.Body.Write(new byte[10], 0, 10); - await context.Response.Body.WriteAsync(new byte[10], 0, 10); - context.Dispose(); - - HttpResponseMessage response = await responseTask; - Assert.Equal(200, (int)response.StatusCode); - Assert.Equal(new Version(1, 1), response.Version); - IEnumerable ignored; - Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); - Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked"); - Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync()); - } - } - - [Fact] - public async Task ResponseBody_NoBufferWriteNoHeaders_DefaultsToChunked() - { - string address; - using (var server = Utilities.CreateHttpServer(out address)) - { - Task responseTask = SendRequestAsync(address); - - var context = await server.AcceptAsync(); - context.Response.ShouldBuffer = false; context.Response.Body.Write(new byte[10], 0, 10); await context.Response.Body.WriteAsync(new byte[10], 0, 10); context.Dispose(); @@ -64,7 +39,7 @@ namespace Microsoft.Net.Http.Server } [Fact] - public async Task ResponseBody_FlushThenBuffer_DefaultsToChunkedAndTerminates() + public async Task ResponseBody_FlushThenWrite_DefaultsToChunkedAndTerminates() { string address; using (var server = Utilities.CreateHttpServer(out address)) @@ -175,13 +150,7 @@ namespace Microsoft.Net.Http.Server context.Response.Headers["Content-lenGth"] = " 20 "; context.Response.Body.Write(new byte[5], 0, 5); context.Dispose(); -#if !NETCOREAPP1_0 - // HttpClient retries the request because it didn't get a response. - context = await server.AcceptAsync(); - context.Response.Headers["Content-lenGth"] = " 20 "; - context.Response.Body.Write(new byte[5], 0, 5); - context.Dispose(); -#endif + await Assert.ThrowsAsync(() => responseTask); } } @@ -199,14 +168,7 @@ namespace Microsoft.Net.Http.Server context.Response.Body.Write(new byte[5], 0, 5); Assert.Throws(() => context.Response.Body.Write(new byte[6], 0, 6)); context.Dispose(); -#if !NETCOREAPP1_0 - // HttpClient retries the request because it didn't get a response. - context = await server.AcceptAsync(); - context.Response.Headers["Content-lenGth"] = " 10 "; - context.Response.Body.Write(new byte[5], 0, 5); - Assert.Throws(() => context.Response.Body.Write(new byte[6], 0, 6)); - context.Dispose(); -#endif + await Assert.ThrowsAsync(() => responseTask); } } @@ -237,7 +199,7 @@ namespace Microsoft.Net.Http.Server } [Fact] - public async Task ResponseBody_WriteZeroCount_StartsResponse() + public async Task ResponseBody_WriteZeroCount_StartsChunkedResponse() { string address; using (var server = Utilities.CreateHttpServer(out address)) @@ -254,74 +216,12 @@ namespace Microsoft.Net.Http.Server Assert.Equal(200, (int)response.StatusCode); Assert.Equal(new Version(1, 1), response.Version); IEnumerable ignored; - Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); - Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked"); + Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); + Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked"); Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync()); } } - [Fact] - public async Task ResponseBody_WriteMoreThanBufferLimitBufferWithNoHeaders_DefaultsToChunkedAndFlushes() - { - string address; - using (var server = Utilities.CreateHttpServer(out address)) - { - Task responseTask = SendRequestAsync(address); - - var context = await server.AcceptAsync(); - context.Response.ShouldBuffer = true; - for (int i = 0; i < 4; i++) - { - context.Response.Body.Write(new byte[1020], 0, 1020); - Assert.True(context.Response.HasStarted); - Assert.False(context.Response.HasStartedSending); - } - context.Response.Body.Write(new byte[1020], 0, 1020); - Assert.True(context.Response.HasStartedSending); - context.Response.Body.Write(new byte[1020], 0, 1020); - context.Dispose(); - - HttpResponseMessage response = await responseTask; - Assert.Equal(200, (int)response.StatusCode); - Assert.Equal(new Version(1, 1), response.Version); - 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[1020*6], await response.Content.ReadAsByteArrayAsync()); - } - } - - [Fact] - public async Task ResponseBody_WriteAsyncMoreThanBufferLimitBufferWithNoHeaders_DefaultsToChunkedAndFlushes() - { - string address; - using (var server = Utilities.CreateHttpServer(out address)) - { - Task responseTask = SendRequestAsync(address); - - var context = await server.AcceptAsync(); - context.Response.ShouldBuffer = true; - for (int i = 0; i < 4; i++) - { - await context.Response.Body.WriteAsync(new byte[1020], 0, 1020); - Assert.True(context.Response.HasStarted); - Assert.False(context.Response.HasStartedSending); - } - await context.Response.Body.WriteAsync(new byte[1020], 0, 1020); - Assert.True(context.Response.HasStartedSending); - await context.Response.Body.WriteAsync(new byte[1020], 0, 1020); - context.Dispose(); - - HttpResponseMessage response = await responseTask; - Assert.Equal(200, (int)response.StatusCode); - Assert.Equal(new Version(1, 1), response.Version); - 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[1020 * 6], await response.Content.ReadAsByteArrayAsync()); - } - } - [Fact] public async Task ResponseBody_WriteAsyncWithActiveCancellationToken_Success() { diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseCachingTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseCachingTests.cs index f4b5272d41..349f93ecfe 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseCachingTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseCachingTests.cs @@ -289,7 +289,6 @@ namespace Microsoft.Net.Http.Server context.Response.Headers["x-request-count"] = "1"; context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache context.Response.CacheTtl = TimeSpan.FromSeconds(10); - context.Response.ShouldBuffer = true; context.Response.Body.Write(new byte[10], 0, 10); await context.Response.Body.WriteAsync(new byte[10], 0, 10); context.Dispose(); diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs index 6ed772dc29..b29aa197a4 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs @@ -216,7 +216,7 @@ namespace Microsoft.Net.Http.Server var context = await server.AcceptAsync(); await context.Response.SendFileAsync(emptyFilePath, 0, null, CancellationToken.None); - Assert.True(context.Response.HasStartedSending); + Assert.True(context.Response.HasStarted); await context.Response.Body.WriteAsync(new byte[10], 0, 10, CancellationToken.None); context.Dispose(); File.Delete(emptyFilePath);