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