#160 Remove response body buffering, fix layering of related features

This commit is contained in:
Chris R 2016-08-03 15:55:57 -07:00
parent 81192017c9
commit fe6ecfde65
18 changed files with 646 additions and 481 deletions

View File

@ -16,8 +16,10 @@
// permissions and limitations under the License. // permissions and limitations under the License.
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using System.Net; using System.Net;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Security.Claims; using System.Security.Claims;
@ -46,8 +48,6 @@ namespace Microsoft.AspNetCore.Server.WebListener
IHttpUpgradeFeature, IHttpUpgradeFeature,
IHttpRequestIdentifierFeature IHttpRequestIdentifierFeature
{ {
private static Func<object,Task> OnStartDelegate = OnStart;
private RequestContext _requestContext; private RequestContext _requestContext;
private IFeatureCollection _features; private IFeatureCollection _features;
private bool _enableResponseCaching; private bool _enableResponseCaching;
@ -74,13 +74,18 @@ namespace Microsoft.AspNetCore.Server.WebListener
private Stream _responseStream; private Stream _responseStream;
private IHeaderDictionary _responseHeaders; private IHeaderDictionary _responseHeaders;
private List<Tuple<Func<object, Task>, object>> _onStartingActions = new List<Tuple<Func<object, Task>, object>>();
private List<Tuple<Func<object, Task>, object>> _onCompletedActions = new List<Tuple<Func<object, Task>, object>>();
private bool _responseStarted;
private bool _completed;
internal FeatureContext(RequestContext requestContext, bool enableResponseCaching) internal FeatureContext(RequestContext requestContext, bool enableResponseCaching)
{ {
_requestContext = requestContext; _requestContext = requestContext;
_features = new FeatureCollection(new StandardFeatureCollection(this)); _features = new FeatureCollection(new StandardFeatureCollection(this));
_authHandler = new AuthenticationHandler(requestContext); _authHandler = new AuthenticationHandler(requestContext);
_enableResponseCaching = enableResponseCaching; _enableResponseCaching = enableResponseCaching;
requestContext.Response.OnStarting(OnStartDelegate, this); _responseStream = new ResponseStream(requestContext.Response.Body, OnStart);
} }
internal IFeatureCollection Features internal IFeatureCollection Features
@ -346,7 +351,7 @@ namespace Microsoft.AspNetCore.Server.WebListener
void IHttpBufferingFeature.DisableResponseBuffering() void IHttpBufferingFeature.DisableResponseBuffering()
{ {
Response.ShouldBuffer = false; // TODO: What about native buffering?
} }
Stream IHttpResponseFeature.Body Stream IHttpResponseFeature.Body
@ -382,12 +387,30 @@ namespace Microsoft.AspNetCore.Server.WebListener
void IHttpResponseFeature.OnStarting(Func<object, Task> callback, object state) void IHttpResponseFeature.OnStarting(Func<object, Task> 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<Func<object, Task>, object>(callback, state));
} }
void IHttpResponseFeature.OnCompleted(Func<object, Task> callback, object state) void IHttpResponseFeature.OnCompleted(Func<object, Task> 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<Func<object, Task>, object>(callback, state));
} }
string IHttpResponseFeature.ReasonPhrase string IHttpResponseFeature.ReasonPhrase
@ -402,9 +425,10 @@ namespace Microsoft.AspNetCore.Server.WebListener
set { Response.StatusCode = value; } 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 CancellationToken IHttpRequestLifetimeFeature.RequestAborted
@ -430,14 +454,15 @@ namespace Microsoft.AspNetCore.Server.WebListener
get { return _requestContext.IsUpgradableRequest; } get { return _requestContext.IsUpgradableRequest; }
} }
Task<Stream> IHttpUpgradeFeature.UpgradeAsync() async Task<Stream> IHttpUpgradeFeature.UpgradeAsync()
{ {
return _requestContext.UpgradeAsync(); await OnStart();
return await _requestContext.UpgradeAsync();
} }
bool IHttpWebSocketFeature.IsWebSocketRequest => _requestContext.IsWebSocketRequest; bool IHttpWebSocketFeature.IsWebSocketRequest => _requestContext.IsWebSocketRequest;
Task<WebSocket> IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context) async Task<WebSocket> IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context)
{ {
// TODO: Advanced params // TODO: Advanced params
string subProtocol = null; string subProtocol = null;
@ -445,7 +470,9 @@ namespace Microsoft.AspNetCore.Server.WebListener
{ {
subProtocol = context.SubProtocol; subProtocol = context.SubProtocol;
} }
return _requestContext.AcceptWebSocketAsync(subProtocol);
await OnStart();
return await _requestContext.AcceptWebSocketAsync(subProtocol);
} }
ClaimsPrincipal IHttpAuthenticationFeature.User ClaimsPrincipal IHttpAuthenticationFeature.User
@ -480,22 +507,42 @@ namespace Microsoft.AspNetCore.Server.WebListener
set { _requestId = value; } set { _requestId = value; }
} }
private static Task OnStart(object obj) internal async Task OnStart()
{ {
var featureContext = (FeatureContext)obj; if (_responseStarted)
{
ConsiderEnablingResponseCache(featureContext); return;
return Task.FromResult(0); }
_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. // 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 // If there's something about the request or response that prevents it from caching then the response
// will complete normally without caching. // 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; 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);
}
}
} }
} }

View File

@ -28,10 +28,7 @@ namespace Microsoft.AspNetCore.Server.WebListener
{ {
internal static class Helpers internal static class Helpers
{ {
internal static Task CompletedTask() internal static Task CompletedTask { get; } = Task.FromResult(0);
{
return Task.FromResult<object>(null);
}
internal static ConfiguredTaskAwaitable SupressContext(this Task task) internal static ConfiguredTaskAwaitable SupressContext(this Task task)
{ {

View File

@ -166,26 +166,34 @@ namespace Microsoft.AspNetCore.Server.WebListener
} }
object context = null; object context = null;
Interlocked.Increment(ref _outstandingRequests);
try try
{ {
Interlocked.Increment(ref _outstandingRequests); var featureContext = new FeatureContext(requestContext, EnableResponseCaching);
FeatureContext featureContext = new FeatureContext(requestContext, EnableResponseCaching);
context = _application.CreateContext(featureContext.Features); context = _application.CreateContext(featureContext.Features);
await _application.ProcessRequestAsync(context).SupressContext(); try
requestContext.Dispose(); {
_application.DisposeContext(context, null); await _application.ProcessRequestAsync(context).SupressContext();
await featureContext.OnStart();
requestContext.Dispose();
_application.DisposeContext(context, null);
}
finally
{
await featureContext.OnCompleted();
}
} }
catch (Exception ex) catch (Exception ex)
{ {
LogHelper.LogException(_logger, "ProcessRequestAsync", ex); LogHelper.LogException(_logger, "ProcessRequestAsync", ex);
if (requestContext.Response.HasStartedSending) if (requestContext.Response.HasStarted)
{ {
requestContext.Abort(); requestContext.Abort();
} }
else else
{ {
// We haven't sent a response yet, try to send a 500 Internal Server Error // 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); SetFatalResponse(requestContext, 500);
} }
_application.DisposeContext(context, ex); _application.DisposeContext(context, ex);

View File

@ -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 file="ResponseStreamAsyncResult.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
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<Task> _onStart;
internal ResponseStream(Stream innerStream, Func<Task> 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<int>(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;
}
}
}

View File

@ -30,7 +30,7 @@ namespace Microsoft.Net.Http.Server
private static byte[] ExtractIdentifierBlob(TOKENBINDING_RESULT_DATA* pTokenBindingResultData) private static byte[] ExtractIdentifierBlob(TOKENBINDING_RESULT_DATA* pTokenBindingResultData)
{ {
// Per http://tools.ietf.org/html/draft-ietf-tokbind-protocol-00, Sec. 4, // 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 // 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. // return the remainder (starting with the hash algorithm) as an opaque blob.
byte[] retVal = new byte[checked(pTokenBindingResultData->identifierSize - 1)]; byte[] retVal = new byte[checked(pTokenBindingResultData->identifierSize - 1)];

View File

@ -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<ArraySegment<byte>> _buffers = new List<ArraySegment<byte>>();
internal IEnumerable<ArraySegment<byte>> Buffers
{
get { return _buffers; }
}
internal int BufferCount
{
get { return _buffers.Count; }
}
internal int TotalBytes { get; private set; }
internal void Add(ArraySegment<byte> data)
{
_buffers.Add(data);
TotalBytes += data.Count;
}
public void CopyAndAdd(ArraySegment<byte> 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<byte>(temp));
TotalBytes += data.Count;
}
}
public void Clear()
{
_buffers.Clear();
TotalBytes = 0;
}
}
}

View File

@ -26,11 +26,9 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using static Microsoft.Net.Http.Server.UnsafeNclNativeMethods; using static Microsoft.Net.Http.Server.UnsafeNclNativeMethods;
@ -46,25 +44,12 @@ namespace Microsoft.Net.Http.Server
private long _expectedBodyLength; private long _expectedBodyLength;
private BoundaryType _boundaryType; private BoundaryType _boundaryType;
private HttpApi.HTTP_RESPONSE_V2 _nativeResponse; private HttpApi.HTTP_RESPONSE_V2 _nativeResponse;
private IList<Tuple<Func<object, Task>, object>> _onStartingActions;
private IList<Tuple<Func<object, Task>, object>> _onCompletedActions;
private bool _bufferingEnabled;
internal Response(RequestContext requestContext) internal Response(RequestContext requestContext)
{ {
// TODO: Verbose log // TODO: Verbose log
RequestContext = requestContext; RequestContext = requestContext;
Headers = new HeaderCollection(); 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 // 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). // that we can start over (e.g. to write an error message).
_nativeResponse = new HttpApi.HTTP_RESPONSE_V2(); _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.MajorVersion = 1;
_nativeResponse.Response_V1.Version.MinorVersion = 1; _nativeResponse.Response_V1.Version.MinorVersion = 1;
_responseState = ResponseState.Created; _responseState = ResponseState.Created;
_onStartingActions = new List<Tuple<Func<object, Task>, object>>();
_onCompletedActions = new List<Tuple<Func<object, Task>, object>>();
_bufferingEnabled = RequestContext.Server.BufferResponses;
_expectedBodyLength = 0; _expectedBodyLength = 0;
_nativeStream = null; _nativeStream = null;
_cacheTtl = null; _cacheTtl = null;
@ -88,9 +70,8 @@ namespace Microsoft.Net.Http.Server
private enum ResponseState private enum ResponseState
{ {
Created, Created,
Started,
ComputedHeaders, ComputedHeaders,
StartedSending, Started,
Closed, Closed,
} }
@ -124,16 +105,6 @@ namespace Microsoft.Net.Http.Server
} }
} }
public bool ShouldBuffer
{
get { return _bufferingEnabled; }
set
{
CheckResponseStarted();
_bufferingEnabled = value;
}
}
public Stream Body public Stream Body
{ {
get get
@ -279,8 +250,6 @@ namespace Microsoft.Net.Http.Server
{ {
return; return;
} }
Start();
NotifyOnResponseCompleted();
// TODO: Verbose log // TODO: Verbose log
EnsureResponseStream(); EnsureResponseStream();
_nativeStream.Dispose(); _nativeStream.Dispose();
@ -292,11 +261,14 @@ namespace Microsoft.Net.Http.Server
get { return _boundaryType; } get { return _boundaryType; }
} }
internal bool HasComputedHeaders
{
get { return _responseState >= ResponseState.ComputedHeaders; }
}
/// <summary> /// <summary>
/// Indicates if the response status, reason, and headers are prepared to send and can /// 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, /// no longer be modified. This is caused by the first write or flush to the response body.
/// the response may not have been flushed to the network yet if the body is buffered.
/// See HasStartedSending.
/// </summary> /// </summary>
public bool HasStarted public bool HasStarted
{ {
@ -311,19 +283,6 @@ namespace Microsoft.Net.Http.Server
} }
} }
internal bool ComputedHeaders
{
get { return _responseState >= ResponseState.ComputedHeaders; }
}
/// <summary>
/// Indicates the initial response has been flushed to the network and can no longer be modified or Reset.
/// </summary>
public bool HasStartedSending
{
get { return _responseState >= ResponseState.StartedSending; }
}
private void EnsureResponseStream() private void EnsureResponseStream()
{ {
if (_nativeStream == null) if (_nativeStream == null)
@ -367,9 +326,9 @@ namespace Microsoft.Net.Http.Server
HttpApi.HTTP_FLAGS flags, HttpApi.HTTP_FLAGS flags,
bool isOpaqueUpgrade) 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); var reasonPhrase = GetReasonPhrase(StatusCode);
/* /*
@ -448,21 +407,8 @@ namespace Microsoft.Net.Http.Server
return statusCode; 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 // 401
if (StatusCode == (ushort)HttpStatusCode.Unauthorized) if (StatusCode == (ushort)HttpStatusCode.Unauthorized)
{ {
@ -470,7 +416,7 @@ namespace Microsoft.Net.Http.Server
} }
var flags = HttpApi.HTTP_FLAGS.NONE; var flags = HttpApi.HTTP_FLAGS.NONE;
Debug.Assert(!ComputedHeaders, "HttpListenerResponse::ComputeHeaders()|ComputedHeaders is true."); Debug.Assert(!HasComputedHeaders, nameof(HasComputedHeaders) + " is true.");
_responseState = ResponseState.ComputedHeaders; _responseState = ResponseState.ComputedHeaders;
// Gather everything from the request that affects the response: // 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. else if (endOfRequest && !(isHeadRequest && statusCanHaveBody)) // HEAD requests should always end without a body. Assume a GET response would have a body.
{ {
if (bufferedBytes > 0) if (statusCanHaveBody)
{
Headers[HttpKnownHeaderNames.ContentLength] = bufferedBytes.ToString(CultureInfo.InvariantCulture);
}
else if (statusCanHaveBody)
{ {
Headers[HttpKnownHeaderNames.ContentLength] = Constants.Zero; Headers[HttpKnownHeaderNames.ContentLength] = Constants.Zero;
} }
_boundaryType = BoundaryType.ContentLength; _boundaryType = BoundaryType.ContentLength;
_expectedBodyLength = bufferedBytes; _expectedBodyLength = 0;
} }
else if (keepConnectionAlive && requestVersion == Constants.V1_1) else if (keepConnectionAlive && requestVersion == Constants.V1_1)
{ {
@ -726,8 +668,6 @@ namespace Microsoft.Net.Http.Server
// Subset of ComputeHeaders // Subset of ComputeHeaders
internal void SendOpaqueUpgrade() internal void SendOpaqueUpgrade()
{ {
// Notify that this is absolutely the last chance to make changes.
Start();
_boundaryType = BoundaryType.Close; _boundaryType = BoundaryType.Close;
// TODO: Send headers async? // TODO: Send headers async?
@ -765,70 +705,7 @@ namespace Microsoft.Net.Http.Server
internal void SwitchToOpaqueMode() internal void SwitchToOpaqueMode()
{ {
EnsureResponseStream(); EnsureResponseStream();
_bufferingEnabled = false;
_nativeStream.SwitchToOpaqueMode(); _nativeStream.SwitchToOpaqueMode();
} }
public void OnStarting(Func<object, Task> callback, object state)
{
var actions = _onStartingActions;
if (actions == null)
{
throw new InvalidOperationException("Response already started");
}
actions.Add(new Tuple<Func<object, Task>, object>(callback, state));
}
public void OnCompleted(Func<object, Task> callback, object state)
{
var actions = _onCompletedActions;
if (actions == null)
{
throw new InvalidOperationException("Response already completed");
}
actions.Add(new Tuple<Func<object, Task>, 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);
}
}
}
} }
} }

View File

@ -23,8 +23,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
@ -36,13 +36,10 @@ namespace Microsoft.Net.Http.Server
{ {
internal class ResponseStream : Stream internal class ResponseStream : Stream
{ {
private const int MaxBufferSize = 4 * 1024;
private RequestContext _requestContext; private RequestContext _requestContext;
private long _leftToWrite = long.MinValue; private long _leftToWrite = long.MinValue;
private bool _closed; private bool _closed;
private bool _inOpaqueMode; private bool _inOpaqueMode;
private BufferBuilder _buffer = new BufferBuilder();
// The last write needs special handling to cancel. // The last write needs special handling to cancel.
private ResponseStreamAsyncResult _lastWrite; private ResponseStreamAsyncResult _lastWrite;
@ -117,18 +114,20 @@ namespace Microsoft.Net.Http.Server
FlushInternal(endOfRequest: false); 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<byte> data = new ArraySegment<byte>())
{ {
bool startedSending = _requestContext.Response.HasStartedSending; Debug.Assert(!(endOfRequest && data.Count > 0), "Data is not supported at the end of the request.");
var byteCount = _buffer.TotalBytes;
if (byteCount == 0 && startedSending && !endOfRequest) var started = _requestContext.Response.HasStarted;
if (data.Count == 0 && started && !endOfRequest)
{ {
// Empty flush // Empty flush
return; return;
} }
var flags = ComputeLeftToWrite(endOfRequest); var flags = ComputeLeftToWrite(endOfRequest);
if (!_inOpaqueMode && endOfRequest && _leftToWrite > byteCount) if (!_inOpaqueMode && endOfRequest && _leftToWrite > data.Count)
{ {
_requestContext.Abort(); _requestContext.Abort();
// This is logged rather than thrown because it is too late for an exception to be visible in user code. // 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; 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; flags |= HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
} }
UpdateWritenCount((uint)byteCount); UpdateWritenCount((uint)data.Count);
uint statusCode = 0; uint statusCode = 0;
HttpApi.HTTP_DATA_CHUNK[] dataChunks; HttpApi.HTTP_DATA_CHUNK[] dataChunks;
var pinnedBuffers = PinDataBuffers(endOfRequest, out dataChunks); var pinnedBuffers = PinDataBuffers(endOfRequest, data, out dataChunks);
try try
{ {
if (!startedSending) if (!started)
{ {
statusCode = _requestContext.Response.SendHeaders(dataChunks, null, flags, false); statusCode = _requestContext.Response.SendHeaders(dataChunks, null, flags, false);
} }
@ -181,7 +180,6 @@ namespace Microsoft.Net.Http.Server
finally finally
{ {
FreeDataBuffers(pinnedBuffers); FreeDataBuffers(pinnedBuffers);
_buffer.Clear();
} }
if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_HANDLE_EOF if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_HANDLE_EOF
@ -195,27 +193,27 @@ namespace Microsoft.Net.Http.Server
} }
} }
private List<GCHandle> PinDataBuffers(bool endOfRequest, out HttpApi.HTTP_DATA_CHUNK[] dataChunks) private List<GCHandle> PinDataBuffers(bool endOfRequest, ArraySegment<byte> data, out HttpApi.HTTP_DATA_CHUNK[] dataChunks)
{ {
var pins = new List<GCHandle>(); var pins = new List<GCHandle>();
var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked; var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
var currentChunk = 0; var currentChunk = 0;
// Figure out how many data chunks // 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]; dataChunks = new HttpApi.HTTP_DATA_CHUNK[1];
SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment<byte>(Helpers.ChunkTerminator)); SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment<byte>(Helpers.ChunkTerminator));
return pins; return pins;
} }
else if (_buffer.TotalBytes == 0) else if (data.Count == 0)
{ {
// No data // No data
dataChunks = new HttpApi.HTTP_DATA_CHUNK[0]; dataChunks = new HttpApi.HTTP_DATA_CHUNK[0];
return pins; return pins;
} }
var chunkCount = _buffer.BufferCount; var chunkCount = 1;
if (chunked) if (chunked)
{ {
// Chunk framing // Chunk framing
@ -231,14 +229,11 @@ namespace Microsoft.Net.Http.Server
if (chunked) if (chunked)
{ {
var chunkHeaderBuffer = Helpers.GetChunkHeader(_buffer.TotalBytes); var chunkHeaderBuffer = Helpers.GetChunkHeader(data.Count);
SetDataChunk(dataChunks, ref currentChunk, pins, chunkHeaderBuffer); SetDataChunk(dataChunks, ref currentChunk, pins, chunkHeaderBuffer);
} }
foreach (var buffer in _buffer.Buffers) SetDataChunk(dataChunks, ref currentChunk, pins, data);
{
SetDataChunk(dataChunks, ref currentChunk, pins, buffer);
}
if (chunked) if (chunked)
{ {
@ -274,18 +269,20 @@ namespace Microsoft.Net.Http.Server
} }
} }
public override Task FlushAsync(CancellationToken cancellationToken)
// Simpler than Flush because it will never be called at the end of the request from Dispose.
public unsafe override Task FlushAsync(CancellationToken cancellationToken)
{ {
if (_closed) if (_closed)
{ {
return Helpers.CompletedTask(); return Helpers.CompletedTask();
} }
return FlushInternalAsync(new ArraySegment<byte>(), cancellationToken);
}
bool startedSending = _requestContext.Response.HasStartedSending; // Simpler than Flush because it will never be called at the end of the request from Dispose.
var byteCount = _buffer.TotalBytes; private unsafe Task FlushInternalAsync(ArraySegment<byte> data, CancellationToken cancellationToken)
if (byteCount == 0 && startedSending) {
var started = _requestContext.Response.HasStarted;
if (data.Count == 0 && started)
{ {
// Empty flush // Empty flush
return Helpers.CompletedTask(); return Helpers.CompletedTask();
@ -298,19 +295,19 @@ namespace Microsoft.Net.Http.Server
} }
var flags = ComputeLeftToWrite(); var flags = ComputeLeftToWrite();
if (_leftToWrite != byteCount) if (_leftToWrite != data.Count)
{ {
flags |= HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA; flags |= HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
} }
UpdateWritenCount((uint)byteCount); UpdateWritenCount((uint)data.Count);
uint statusCode = 0; uint statusCode = 0;
var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked; 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; uint bytesSent = 0;
try try
{ {
if (!startedSending) if (!started)
{ {
statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false); statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false);
bytesSent = asyncResult.BytesSent; bytesSent = asyncResult.BytesSent;
@ -341,7 +338,7 @@ namespace Microsoft.Net.Http.Server
if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_IO_PENDING) if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_IO_PENDING)
{ {
asyncResult.Dispose(); asyncResult.Dispose();
if (_requestContext.Server.IgnoreWriteExceptions && startedSending) if (_requestContext.Server.IgnoreWriteExceptions && started)
{ {
asyncResult.Complete(); asyncResult.Complete();
} }
@ -408,10 +405,10 @@ namespace Microsoft.Net.Http.Server
private HttpApi.HTTP_FLAGS ComputeLeftToWrite(bool endOfRequest = false) private HttpApi.HTTP_FLAGS ComputeLeftToWrite(bool endOfRequest = false)
{ {
HttpApi.HTTP_FLAGS flags = HttpApi.HTTP_FLAGS.NONE; var flags = HttpApi.HTTP_FLAGS.NONE;
if (!_requestContext.Response.ComputedHeaders) if (!_requestContext.Response.HasComputedHeaders)
{ {
flags = _requestContext.Response.ComputeHeaders(endOfRequest, _buffer.TotalBytes); flags = _requestContext.Response.ComputeHeaders(endOfRequest);
} }
if (_leftToWrite == long.MinValue) if (_leftToWrite == long.MinValue)
{ {
@ -437,47 +434,31 @@ namespace Microsoft.Net.Http.Server
var data = new ArraySegment<byte>(buffer, offset, count); var data = new ArraySegment<byte>(buffer, offset, count);
CheckDisposed(); CheckDisposed();
// TODO: Verbose log parameters // 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; 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."); 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 // The last write in a response that has already started, flush immediately
else if (_requestContext.Response.ComputedHeaders && _leftToWrite >= 0 && _leftToWrite <= currentBytes) 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."); 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 #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 #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 #endif
{ {
return WriteAsync(buffer, offset, count).ToIAsyncResult(callback, state); return WriteAsync(buffer, offset, count).ToIAsyncResult(callback, state);
@ -490,7 +471,7 @@ namespace Microsoft.Net.Http.Server
{ {
if (asyncResult == null) if (asyncResult == null)
{ {
throw new ArgumentNullException("asyncResult"); throw new ArgumentNullException(nameof(asyncResult));
} }
((Task)asyncResult).GetAwaiter().GetResult(); ((Task)asyncResult).GetAwaiter().GetResult();
} }
@ -505,42 +486,25 @@ namespace Microsoft.Net.Http.Server
} }
CheckDisposed(); CheckDisposed();
// TODO: Verbose log parameters // 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; 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."); 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 // The last write in a response that has already started, flush immediately
else if (_requestContext.Response.ComputedHeaders && _leftToWrite > 0 && _leftToWrite <= currentBytes) 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."); 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) 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"); throw new ArgumentNullException("fileName");
} }
CheckDisposed(); CheckDisposed();
if (_buffer.TotalBytes > 0)
{ // We can't mix await and unsafe so separate the unsafe code into another method.
// 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.
await SendFileAsyncCore(fileName, offset, count, cancellationToken); await SendFileAsyncCore(fileName, offset, count, cancellationToken);
} }
internal unsafe Task SendFileAsyncCore(string fileName, long offset, long? count, CancellationToken cancellationToken) internal unsafe Task SendFileAsyncCore(string fileName, long offset, long? count, CancellationToken cancellationToken)
{ {
_requestContext.Response.Start(); var flags = ComputeLeftToWrite();
HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite();
if (count == 0 && _leftToWrite != 0) if (count == 0 && _leftToWrite != 0)
{ {
return Helpers.CompletedTask(); return Helpers.CompletedTask();
@ -589,7 +547,7 @@ namespace Microsoft.Net.Http.Server
uint statusCode; uint statusCode;
uint bytesSent = 0; uint bytesSent = 0;
bool startedSending = _requestContext.Response.HasStartedSending; var started = _requestContext.Response.HasStarted;
var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked; var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, fileName, offset, count, chunked, cancellationRegistration); ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, fileName, offset, count, chunked, cancellationRegistration);
@ -612,7 +570,7 @@ namespace Microsoft.Net.Http.Server
try try
{ {
if (!startedSending) if (!started)
{ {
statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false); statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false);
bytesSent = asyncResult.BytesSent; bytesSent = asyncResult.BytesSent;
@ -644,7 +602,7 @@ namespace Microsoft.Net.Http.Server
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING) if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
{ {
asyncResult.Dispose(); asyncResult.Dispose();
if (_requestContext.Server.IgnoreWriteExceptions && startedSending) if (_requestContext.Server.IgnoreWriteExceptions && started)
{ {
asyncResult.Complete(); asyncResult.Complete();
} }

View File

@ -50,14 +50,14 @@ namespace Microsoft.Net.Http.Server
_cancellationRegistration = cancellationRegistration; _cancellationRegistration = cancellationRegistration;
} }
internal ResponseStreamAsyncResult(ResponseStream responseStream, BufferBuilder buffer, bool chunked, internal ResponseStreamAsyncResult(ResponseStream responseStream, ArraySegment<byte> data, bool chunked,
CancellationTokenRegistration cancellationRegistration) CancellationTokenRegistration cancellationRegistration)
: this(responseStream, cancellationRegistration) : this(responseStream, cancellationRegistration)
{ {
var boundHandle = _responseStream.RequestContext.Server.RequestQueue.BoundHandle; var boundHandle = _responseStream.RequestContext.Server.RequestQueue.BoundHandle;
object[] objectsToPin; object[] objectsToPin;
if (buffer.TotalBytes == 0) if (data.Count == 0)
{ {
_dataChunks = null; _dataChunks = null;
_overlapped = new SafeNativeOverlapped(boundHandle, _overlapped = new SafeNativeOverlapped(boundHandle,
@ -65,7 +65,7 @@ namespace Microsoft.Net.Http.Server
return; 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 = new object[_dataChunks.Length + 1];
objectsToPin[0] = _dataChunks; objectsToPin[0] = _dataChunks;
var currentChunk = 0; var currentChunk = 0;
@ -74,14 +74,11 @@ namespace Microsoft.Net.Http.Server
var chunkHeaderBuffer = new ArraySegment<byte>(); var chunkHeaderBuffer = new ArraySegment<byte>();
if (chunked) if (chunked)
{ {
chunkHeaderBuffer = Helpers.GetChunkHeader(buffer.TotalBytes); chunkHeaderBuffer = Helpers.GetChunkHeader(data.Count);
SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, chunkHeaderBuffer); SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, chunkHeaderBuffer);
} }
foreach (var segment in buffer.Buffers) SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, data);
{
SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, segment);
}
if (chunked) if (chunked)
{ {
@ -98,19 +95,15 @@ namespace Microsoft.Net.Http.Server
_dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer.Array, chunkHeaderBuffer.Offset); _dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer.Array, chunkHeaderBuffer.Offset);
currentChunk++; currentChunk++;
} }
foreach (var segment in buffer.Buffers)
{ _dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(data.Array, data.Offset);
_dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(segment.Array, segment.Offset); currentChunk++;
currentChunk++;
}
if (chunked) if (chunked)
{ {
_dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(Helpers.CRLF, 0); _dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(Helpers.CRLF, 0);
currentChunk++; 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, internal ResponseStreamAsyncResult(ResponseStream responseStream, string fileName, long offset,

View File

@ -72,8 +72,6 @@ namespace Microsoft.Net.Http.Server
// The native request queue // The native request queue
private long? _requestQueueLength; private long? _requestQueueLength;
private bool _bufferResponses = true;
public WebListener() public WebListener()
: this(null) : this(null)
{ {
@ -169,12 +167,6 @@ namespace Microsoft.Net.Http.Server
get { return _urlPrefixes; } get { return _urlPrefixes; }
} }
public bool BufferResponses
{
get { return _bufferResponses; }
set { _bufferResponses = value; }
}
/// <summary> /// <summary>
/// Exposes the Http.Sys timeout configurations. These may also be configured in the registry. /// Exposes the Http.Sys timeout configurations. These may also be configured in the registry.
/// </summary> /// </summary>

View File

@ -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<IHttpUpgradeFeature>();
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] [ConditionalTheory]
[OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)] [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
// See HTTP_VERB for known verbs // See HTTP_VERB for known verbs

View File

@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.WebListener
public class ResponseBodyTests public class ResponseBodyTests
{ {
[Fact] [Fact]
public async Task ResponseBody_WriteNoHeaders_BuffersAndSetsContentLength() public async Task ResponseBody_WriteNoHeaders_SetsChunked()
{ {
string address; string address;
using (Utilities.CreateHttpServer(out address, httpContext => 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); 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(200, (int)response.StatusCode);
Assert.Equal(new Version(1, 1), response.Version); Assert.Equal(new Version(1, 1), response.Version);
IEnumerable<string> ignored; IEnumerable<string> ignored;
Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked"); Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync()); Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
} }
} }
@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Server.WebListener
await httpContext.Response.Body.FlushAsync(); await httpContext.Response.Body.FlushAsync();
})) }))
{ {
HttpResponseMessage response = await SendRequestAsync(address); var response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode); Assert.Equal(200, (int)response.StatusCode);
Assert.Equal(new Version(1, 1), response.Version); Assert.Equal(new Version(1, 1), response.Version);
IEnumerable<string> ignored; IEnumerable<string> ignored;
@ -82,7 +82,7 @@ namespace Microsoft.AspNetCore.Server.WebListener
await stream.WriteAsync(responseBytes, 0, responseBytes.Length); 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(200, (int)response.StatusCode);
Assert.Equal(new Version(1, 1), response.Version); Assert.Equal(new Version(1, 1), response.Version);
IEnumerable<string> ignored; IEnumerable<string> ignored;
@ -109,7 +109,7 @@ namespace Microsoft.AspNetCore.Server.WebListener
await stream.WriteAsync(new byte[10], 0, 10); 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(200, (int)response.StatusCode);
Assert.Equal(new Version(1, 1), response.Version); Assert.Equal(new Version(1, 1), response.Version);
IEnumerable<string> contentLength; IEnumerable<string> contentLength;
@ -152,24 +152,26 @@ namespace Microsoft.AspNetCore.Server.WebListener
[Fact] [Fact]
public async Task ResponseBody_WriteContentLengthTooMuchWritten_Throws() public async Task ResponseBody_WriteContentLengthTooMuchWritten_Throws()
{ {
var completed = false;
string address; string address;
using (Utilities.CreateHttpServer(out address, httpContext => using (Utilities.CreateHttpServer(out address, httpContext =>
{ {
httpContext.Response.Headers["Content-lenGth"] = " 10 "; httpContext.Response.Headers["Content-lenGth"] = " 10 ";
httpContext.Response.Body.Write(new byte[5], 0, 5); httpContext.Response.Body.Write(new byte[5], 0, 5);
httpContext.Response.Body.Write(new byte[6], 0, 6); Assert.Throws<InvalidOperationException>(() => httpContext.Response.Body.Write(new byte[6], 0, 6));
completed = true;
return Task.FromResult(0); return Task.FromResult(0);
})) }))
{ {
var response = await SendRequestAsync(address); await Assert.ThrowsAsync<HttpRequestException>(() => SendRequestAsync(address));
Assert.Equal(500, (int)response.StatusCode); Assert.True(completed);
} }
} }
[Fact] [Fact]
public async Task ResponseBody_WriteContentLengthExtraWritten_Throws() public async Task ResponseBody_WriteContentLengthExtraWritten_Throws()
{ {
ManualResetEvent waitHandle = new ManualResetEvent(false); var waitHandle = new ManualResetEvent(false);
bool? appThrew = null; bool? appThrew = null;
string address; string address;
using (Utilities.CreateHttpServer(out address, httpContext => 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<string> 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<string> 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<string> 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<HttpResponseMessage> SendRequestAsync(string uri) private async Task<HttpResponseMessage> SendRequestAsync(string uri)
{ {
using (HttpClient client = new HttpClient()) using (HttpClient client = new HttpClient())

View File

@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.Server.WebListener
return Task.FromResult(0); return Task.FromResult(0);
})) }))
{ {
HttpResponseMessage response = await SendRequestAsync(address); var response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode); Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> ignored; IEnumerable<string> ignored;
Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
@ -86,7 +86,7 @@ namespace Microsoft.AspNetCore.Server.WebListener
[Fact] [Fact]
public async Task ResponseSendFile_MissingFile_Throws() public async Task ResponseSendFile_MissingFile_Throws()
{ {
ManualResetEvent waitHandle = new ManualResetEvent(false); var waitHandle = new ManualResetEvent(false);
bool? appThrew = null; bool? appThrew = null;
string address; string address;
using (Utilities.CreateHttpServer(out address, httpContext => using (Utilities.CreateHttpServer(out address, httpContext =>
@ -165,7 +165,7 @@ namespace Microsoft.AspNetCore.Server.WebListener
return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
})) }))
{ {
HttpResponseMessage response = await SendRequestAsync(address); var response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode); Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> contentLength; IEnumerable<string> contentLength;
Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); 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); return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
})) }))
{ {
HttpResponseMessage response = await SendRequestAsync(address); var response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode); Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> contentLength; IEnumerable<string> contentLength;
Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); 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); 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); Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> contentLength; IEnumerable<string> contentLength;
Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
@ -216,30 +216,34 @@ namespace Microsoft.AspNetCore.Server.WebListener
[Fact] [Fact]
public async Task ResponseSendFile_OffsetOutOfRange_Throws() public async Task ResponseSendFile_OffsetOutOfRange_Throws()
{ {
var completed = false;
string address; string address;
using (Utilities.CreateHttpServer(out address, httpContext => using (Utilities.CreateHttpServer(out address, async httpContext =>
{ {
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>(); var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
return sendFile.SendFileAsync(AbsoluteFilePath, 1234567, null, CancellationToken.None); await sendFile.SendFileAsync(AbsoluteFilePath, 1234567, null, CancellationToken.None);
completed = true;
})) }))
{ {
HttpResponseMessage response = await SendRequestAsync(address); await Assert.ThrowsAsync<HttpRequestException>(() => SendRequestAsync(address));
Assert.Equal(500, (int)response.StatusCode); Assert.False(completed);
} }
} }
[Fact] [Fact]
public async Task ResponseSendFile_CountOutOfRange_Throws() public async Task ResponseSendFile_CountOutOfRange_Throws()
{ {
var completed = false;
string address; string address;
using (Utilities.CreateHttpServer(out address, httpContext => using (Utilities.CreateHttpServer(out address, async httpContext =>
{ {
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>(); var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
return sendFile.SendFileAsync(AbsoluteFilePath, 0, 1234567, CancellationToken.None); await sendFile.SendFileAsync(AbsoluteFilePath, 0, 1234567, CancellationToken.None);
completed = true;
})) }))
{ {
HttpResponseMessage response = await SendRequestAsync(address); await Assert.ThrowsAsync<HttpRequestException>(() => SendRequestAsync(address));
Assert.Equal(500, (int)response.StatusCode); Assert.False(completed);
} }
} }
@ -253,7 +257,7 @@ namespace Microsoft.AspNetCore.Server.WebListener
return sendFile.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None); return sendFile.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None);
})) }))
{ {
HttpResponseMessage response = await SendRequestAsync(address); var response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode); Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> contentLength; IEnumerable<string> contentLength;
Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); 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); return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
})) }))
{ {
HttpResponseMessage response = await SendRequestAsync(address); var response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode); Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> contentLength; IEnumerable<string> contentLength;
Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); 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); return sendFile.SendFileAsync(AbsoluteFilePath, 0, 10, CancellationToken.None);
})) }))
{ {
HttpResponseMessage response = await SendRequestAsync(address); var response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode); Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> contentLength; IEnumerable<string> contentLength;
Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); 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); return sendFile.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None);
})) }))
{ {
HttpResponseMessage response = await SendRequestAsync(address); var response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode); Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> contentLength; IEnumerable<string> contentLength;
Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length"); 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<IHttpSendFileFeature>();
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<string> 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<HttpResponseMessage> SendRequestAsync(string uri) private async Task<HttpResponseMessage> SendRequestAsync(string uri)
{ {
using (HttpClient client = new HttpClient()) using (HttpClient client = new HttpClient())

View File

@ -16,6 +16,7 @@
// permissions and limitations under the License. // permissions and limitations under the License.
using System; using System;
using System.IO;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -102,7 +103,7 @@ namespace Microsoft.AspNetCore.Server.WebListener
} }
[Fact] [Fact]
public async Task Response_100_Throws() public async Task Response_StatusCode100_Throws()
{ {
string address; string address;
using (Utilities.CreateHttpServer(out address, httpContext => using (Utilities.CreateHttpServer(out address, httpContext =>
@ -117,7 +118,7 @@ namespace Microsoft.AspNetCore.Server.WebListener
} }
[Fact] [Fact]
public async Task Response_0_Throws() public async Task Response_StatusCode0_Throws()
{ {
string address; string address;
using (Utilities.CreateHttpServer(out address, httpContext => 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<InvalidTimeZoneException>(() => 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<HttpResponseMessage> SendRequestAsync(string uri) private async Task<HttpResponseMessage> SendRequestAsync(string uri)
{ {
using (HttpClient client = new HttpClient()) using (var client = new HttpClient())
{ {
return await client.GetAsync(uri); return await client.GetAsync(uri);
} }

View File

@ -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<IHttpWebSocketFeature>();
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] [ConditionalFact]
[OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)] [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
public async Task WebSocketAccept_SendAndReceive_Success() public async Task WebSocketAccept_SendAndReceive_Success()

View File

@ -16,7 +16,7 @@ namespace Microsoft.Net.Http.Server
public class ResponseBodyTests public class ResponseBodyTests
{ {
[Fact] [Fact]
public async Task ResponseBody_BufferWriteNoHeaders_DefaultsToContentLength() public async Task ResponseBody_WriteNoHeaders_DefaultsToChunked()
{ {
string address; string address;
using (var server = Utilities.CreateHttpServer(out address)) using (var server = Utilities.CreateHttpServer(out address))
@ -24,31 +24,6 @@ namespace Microsoft.Net.Http.Server
Task<HttpResponseMessage> responseTask = SendRequestAsync(address); Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
var context = await server.AcceptAsync(); 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<string> 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<HttpResponseMessage> responseTask = SendRequestAsync(address);
var context = await server.AcceptAsync();
context.Response.ShouldBuffer = false;
context.Response.Body.Write(new byte[10], 0, 10); context.Response.Body.Write(new byte[10], 0, 10);
await context.Response.Body.WriteAsync(new byte[10], 0, 10); await context.Response.Body.WriteAsync(new byte[10], 0, 10);
context.Dispose(); context.Dispose();
@ -64,7 +39,7 @@ namespace Microsoft.Net.Http.Server
} }
[Fact] [Fact]
public async Task ResponseBody_FlushThenBuffer_DefaultsToChunkedAndTerminates() public async Task ResponseBody_FlushThenWrite_DefaultsToChunkedAndTerminates()
{ {
string address; string address;
using (var server = Utilities.CreateHttpServer(out 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.Headers["Content-lenGth"] = " 20 ";
context.Response.Body.Write(new byte[5], 0, 5); context.Response.Body.Write(new byte[5], 0, 5);
context.Dispose(); 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<HttpRequestException>(() => responseTask); await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
} }
} }
@ -199,14 +168,7 @@ namespace Microsoft.Net.Http.Server
context.Response.Body.Write(new byte[5], 0, 5); context.Response.Body.Write(new byte[5], 0, 5);
Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(new byte[6], 0, 6)); Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(new byte[6], 0, 6));
context.Dispose(); 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<InvalidOperationException>(() => context.Response.Body.Write(new byte[6], 0, 6));
context.Dispose();
#endif
await Assert.ThrowsAsync<HttpRequestException>(() => responseTask); await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
} }
} }
@ -237,7 +199,7 @@ namespace Microsoft.Net.Http.Server
} }
[Fact] [Fact]
public async Task ResponseBody_WriteZeroCount_StartsResponse() public async Task ResponseBody_WriteZeroCount_StartsChunkedResponse()
{ {
string address; string address;
using (var server = Utilities.CreateHttpServer(out 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(200, (int)response.StatusCode);
Assert.Equal(new Version(1, 1), response.Version); Assert.Equal(new Version(1, 1), response.Version);
IEnumerable<string> ignored; IEnumerable<string> ignored;
Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked"); Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync()); 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<HttpResponseMessage> 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<string> 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<HttpResponseMessage> 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<string> 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] [Fact]
public async Task ResponseBody_WriteAsyncWithActiveCancellationToken_Success() public async Task ResponseBody_WriteAsyncWithActiveCancellationToken_Success()
{ {

View File

@ -289,7 +289,6 @@ namespace Microsoft.Net.Http.Server
context.Response.Headers["x-request-count"] = "1"; context.Response.Headers["x-request-count"] = "1";
context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
context.Response.CacheTtl = TimeSpan.FromSeconds(10); context.Response.CacheTtl = TimeSpan.FromSeconds(10);
context.Response.ShouldBuffer = true;
context.Response.Body.Write(new byte[10], 0, 10); context.Response.Body.Write(new byte[10], 0, 10);
await context.Response.Body.WriteAsync(new byte[10], 0, 10); await context.Response.Body.WriteAsync(new byte[10], 0, 10);
context.Dispose(); context.Dispose();

View File

@ -216,7 +216,7 @@ namespace Microsoft.Net.Http.Server
var context = await server.AcceptAsync(); var context = await server.AcceptAsync();
await context.Response.SendFileAsync(emptyFilePath, 0, null, CancellationToken.None); 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); await context.Response.Body.WriteAsync(new byte[10], 0, 10, CancellationToken.None);
context.Dispose(); context.Dispose();
File.Delete(emptyFilePath); File.Delete(emptyFilePath);