#160 Remove response body buffering, fix layering of related features
This commit is contained in:
parent
81192017c9
commit
fe6ecfde65
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)];
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue