#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.
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<object,Task> 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<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)
{
_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<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)
{
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
@ -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<Stream> IHttpUpgradeFeature.UpgradeAsync()
async Task<Stream> IHttpUpgradeFeature.UpgradeAsync()
{
return _requestContext.UpgradeAsync();
await OnStart();
return await _requestContext.UpgradeAsync();
}
bool IHttpWebSocketFeature.IsWebSocketRequest => _requestContext.IsWebSocketRequest;
Task<WebSocket> IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context)
async Task<WebSocket> 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);
}
}
}
}

View File

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

View File

@ -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);

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)
{
// 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)];

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.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<Tuple<Func<object, Task>, object>> _onStartingActions;
private IList<Tuple<Func<object, Task>, 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<Tuple<Func<object, Task>, object>>();
_onCompletedActions = new List<Tuple<Func<object, Task>, 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; }
}
/// <summary>
/// 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.
/// </summary>
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()
{
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<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.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<byte> data = new ArraySegment<byte>())
{
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<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 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<byte>(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<byte>(), 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<byte> 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<byte>(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();
}

View File

@ -50,14 +50,14 @@ namespace Microsoft.Net.Http.Server
_cancellationRegistration = cancellationRegistration;
}
internal ResponseStreamAsyncResult(ResponseStream responseStream, BufferBuilder buffer, bool chunked,
internal ResponseStreamAsyncResult(ResponseStream responseStream, ArraySegment<byte> 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<byte>();
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,

View File

@ -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; }
}
/// <summary>
/// Exposes the Http.Sys timeout configurations. These may also be configured in the registry.
/// </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]
[OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
// See HTTP_VERB for known verbs

View File

@ -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<string> 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<string> 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<string> 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<string> 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<InvalidOperationException>(() => 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<HttpRequestException>(() => 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<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)
{
using (HttpClient client = new HttpClient())

View File

@ -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<string> 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<string> 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<string> 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<string> 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<IHttpSendFileFeature>();
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<HttpRequestException>(() => 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<IHttpSendFileFeature>();
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<HttpRequestException>(() => 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<string> 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<string> 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<string> 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<string> 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<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)
{
using (HttpClient client = new HttpClient())

View File

@ -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<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)
{
using (HttpClient client = new HttpClient())
using (var client = new HttpClient())
{
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]
[OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
public async Task WebSocketAccept_SendAndReceive_Success()

View File

@ -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<HttpResponseMessage> 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<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);
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<HttpRequestException>(() => responseTask);
}
}
@ -199,14 +168,7 @@ namespace Microsoft.Net.Http.Server
context.Response.Body.Write(new byte[5], 0, 5);
Assert.Throws<InvalidOperationException>(() => 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<InvalidOperationException>(() => context.Response.Body.Write(new byte[6], 0, 6));
context.Dispose();
#endif
await Assert.ThrowsAsync<HttpRequestException>(() => 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<string> 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<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]
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["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();

View File

@ -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);