#120 Implement response buffering.

This commit is contained in:
Chris R 2015-06-04 11:36:13 -07:00
parent 0acb8f3ed4
commit 3c044fb92e
19 changed files with 945 additions and 827 deletions

View File

@ -39,6 +39,7 @@ namespace Microsoft.AspNet.Server.WebListener
IHttpSendFileFeature, IHttpSendFileFeature,
ITlsConnectionFeature, ITlsConnectionFeature,
ITlsTokenBindingFeature, ITlsTokenBindingFeature,
IHttpBufferingFeature,
IHttpRequestLifetimeFeature, IHttpRequestLifetimeFeature,
IHttpWebSocketFeature, IHttpWebSocketFeature,
IHttpAuthenticationFeature, IHttpAuthenticationFeature,
@ -97,6 +98,7 @@ namespace Microsoft.AspNet.Server.WebListener
_features.Add(typeof(IHttpConnectionFeature), this); _features.Add(typeof(IHttpConnectionFeature), this);
_features.Add(typeof(IHttpResponseFeature), this); _features.Add(typeof(IHttpResponseFeature), this);
_features.Add(typeof(IHttpSendFileFeature), this); _features.Add(typeof(IHttpSendFileFeature), this);
_features.Add(typeof(IHttpBufferingFeature), this);
_features.Add(typeof(IHttpRequestLifetimeFeature), this); _features.Add(typeof(IHttpRequestLifetimeFeature), this);
_features.Add(typeof(IHttpAuthenticationFeature), this); _features.Add(typeof(IHttpAuthenticationFeature), this);
_features.Add(typeof(IRequestIdentifierFeature), this); _features.Add(typeof(IRequestIdentifierFeature), this);
@ -328,6 +330,16 @@ namespace Microsoft.AspNet.Server.WebListener
return Request.GetReferredTokenBindingId(); return Request.GetReferredTokenBindingId();
} }
void IHttpBufferingFeature.DisableRequestBuffering()
{
// There is no request buffering.
}
void IHttpBufferingFeature.DisableResponseBuffering()
{
Response.ShouldBuffer = false;
}
Stream IHttpResponseFeature.Body Stream IHttpResponseFeature.Body
{ {
get get
@ -356,7 +368,7 @@ namespace Microsoft.AspNet.Server.WebListener
bool IHttpResponseFeature.HeadersSent bool IHttpResponseFeature.HeadersSent
{ {
get { return Response.HeadersSent; } get { return Response.HasStarted; }
} }
void IHttpResponseFeature.OnSendingHeaders(Action<object> callback, object state) void IHttpResponseFeature.OnSendingHeaders(Action<object> callback, object state)

View File

@ -167,13 +167,14 @@ namespace Microsoft.AspNet.Server.WebListener
catch (Exception ex) catch (Exception ex)
{ {
LogHelper.LogException(_logger, "ProcessRequestAsync", ex); LogHelper.LogException(_logger, "ProcessRequestAsync", ex);
if (requestContext.Response.HeadersSent) if (requestContext.Response.HasStartedSending)
{ {
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();
SetFatalResponse(requestContext, 500); SetFatalResponse(requestContext, 500);
} }
} }
@ -195,8 +196,6 @@ namespace Microsoft.AspNet.Server.WebListener
private static void SetFatalResponse(RequestContext context, int status) private static void SetFatalResponse(RequestContext context, int status)
{ {
context.Response.StatusCode = status; context.Response.StatusCode = status;
context.Response.ReasonPhrase = string.Empty;
context.Response.Headers.Clear();
context.Response.ContentLength = 0; context.Response.ContentLength = 0;
context.Dispose(); context.Dispose();
} }

View File

@ -21,13 +21,18 @@
// </copyright> // </copyright>
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.Net.Http.Server namespace Microsoft.Net.Http.Server
{ {
internal static class Helpers internal static class Helpers
{ {
internal static readonly byte[] ChunkTerminator = new byte[] { (byte)'0', (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' };
internal static readonly byte[] CRLF = new byte[] { (byte)'\r', (byte)'\n' };
internal static Task CompletedTask() internal static Task CompletedTask()
{ {
return Task.FromResult<object>(null); return Task.FromResult<object>(null);
@ -49,5 +54,94 @@ namespace Microsoft.Net.Http.Server
{ {
return task.ConfigureAwait(continueOnCapturedContext: false); return task.ConfigureAwait(continueOnCapturedContext: false);
} }
internal static IAsyncResult ToIAsyncResult(this 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;
}
/// <summary>
/// A private utility routine to convert an integer to a chunk header,
/// which is an ASCII hex number followed by a CRLF.The header is returned
/// as a byte array.
/// Generates a right-aligned hex string and returns the start offset.
/// </summary>
/// <param name="size">Chunk size to be encoded</param>
/// <param name="offset">Out parameter where we store offset into buffer.</param>
/// <returns>A byte array with the header in int.</returns>
internal static ArraySegment<byte> GetChunkHeader(int size)
{
uint mask = 0xf0000000;
byte[] header = new byte[10];
int i;
int offset = -1;
// Loop through the size, looking at each nibble. If it's not 0
// convert it to hex. Save the index of the first non-zero
// byte.
for (i = 0; i < 8; i++, size <<= 4)
{
// offset == -1 means that we haven't found a non-zero nibble
// yet. If we haven't found one, and the current one is zero,
// don't do anything.
if (offset == -1)
{
if ((size & mask) == 0)
{
continue;
}
}
// Either we have a non-zero nibble or we're no longer skipping
// leading zeros. Convert this nibble to ASCII and save it.
uint temp = (uint)size >> 28;
if (temp < 10)
{
header[i] = (byte)(temp + '0');
}
else
{
header[i] = (byte)((temp - 10) + 'A');
}
// If we haven't found a non-zero nibble yet, we've found one
// now, so remember that.
if (offset == -1)
{
offset = i;
}
}
header[8] = (byte)'\r';
header[9] = (byte)'\n';
return new ArraySegment<byte>(header, offset, header.Length - offset);
}
} }
} }

View File

@ -0,0 +1,50 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.Net.Http.Server
{
internal class BufferBuilder
{
private List<ArraySegment<byte>> _buffers = new List<ArraySegment<byte>>();
internal IEnumerable<ArraySegment<byte>> Buffers
{
get { return _buffers; }
}
internal int BufferCount
{
get { return _buffers.Count; }
}
internal int TotalBytes { get; private set; }
internal void Add(ArraySegment<byte> data)
{
_buffers.Add(data);
TotalBytes += data.Count;
}
public void CopyAndAdd(ArraySegment<byte> data)
{
if (data.Count > 0)
{
var temp = new byte[data.Count];
Buffer.BlockCopy(data.Array, data.Offset, temp, 0, data.Count);
_buffers.Add(new ArraySegment<byte>(temp));
TotalBytes += data.Count;
}
}
public void Clear()
{
_buffers.Clear();
TotalBytes = 0;
}
}
}

View File

@ -21,15 +21,15 @@ namespace Microsoft.Net.Http.Server
private IDictionary<string, string[]> Store { get; set; } private IDictionary<string, string[]> Store { get; set; }
// Readonly after the response has been sent. // Readonly after the response has been started.
internal bool Sent { get; set; } public bool IsReadOnly { get; internal set; }
public string this[string key] public string this[string key]
{ {
get { return Get(key); } get { return Get(key); }
set set
{ {
ThrowIfSent(); ThrowIfReadOnly();
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
{ {
Remove(key); Remove(key);
@ -46,7 +46,7 @@ namespace Microsoft.Net.Http.Server
get { return Store[key]; } get { return Store[key]; }
set set
{ {
ThrowIfSent(); ThrowIfReadOnly();
Store[key] = value; Store[key] = value;
} }
} }
@ -56,11 +56,6 @@ namespace Microsoft.Net.Http.Server
get { return Store.Count; } get { return Store.Count; }
} }
public bool IsReadOnly
{
get { return Sent; }
}
public ICollection<string> Keys public ICollection<string> Keys
{ {
get { return Store.Keys; } get { return Store.Keys; }
@ -73,19 +68,19 @@ namespace Microsoft.Net.Http.Server
public void Add(KeyValuePair<string, string[]> item) public void Add(KeyValuePair<string, string[]> item)
{ {
ThrowIfSent(); ThrowIfReadOnly();
Store.Add(item); Store.Add(item);
} }
public void Add(string key, string[] value) public void Add(string key, string[] value)
{ {
ThrowIfSent(); ThrowIfReadOnly();
Store.Add(key, value); Store.Add(key, value);
} }
public void Append(string key, string value) public void Append(string key, string value)
{ {
ThrowIfSent(); ThrowIfReadOnly();
string[] values; string[] values;
if (Store.TryGetValue(key, out values)) if (Store.TryGetValue(key, out values))
{ {
@ -102,7 +97,7 @@ namespace Microsoft.Net.Http.Server
public void AppendValues(string key, params string[] values) public void AppendValues(string key, params string[] values)
{ {
ThrowIfSent(); ThrowIfReadOnly();
string[] oldValues; string[] oldValues;
if (Store.TryGetValue(key, out oldValues)) if (Store.TryGetValue(key, out oldValues))
{ {
@ -119,7 +114,7 @@ namespace Microsoft.Net.Http.Server
public void Clear() public void Clear()
{ {
ThrowIfSent(); ThrowIfReadOnly();
Store.Clear(); Store.Clear();
} }
@ -165,25 +160,25 @@ namespace Microsoft.Net.Http.Server
public bool Remove(KeyValuePair<string, string[]> item) public bool Remove(KeyValuePair<string, string[]> item)
{ {
ThrowIfSent(); ThrowIfReadOnly();
return Store.Remove(item); return Store.Remove(item);
} }
public bool Remove(string key) public bool Remove(string key)
{ {
ThrowIfSent(); ThrowIfReadOnly();
return Store.Remove(key); return Store.Remove(key);
} }
public void Set(string key, string value) public void Set(string key, string value)
{ {
ThrowIfSent(); ThrowIfReadOnly();
Store[key] = new[] { value }; Store[key] = new[] { value };
} }
public void SetValues(string key, params string[] values) public void SetValues(string key, params string[] values)
{ {
ThrowIfSent(); ThrowIfReadOnly();
Store[key] = values; Store[key] = values;
} }
@ -197,11 +192,11 @@ namespace Microsoft.Net.Http.Server
return GetEnumerator(); return GetEnumerator();
} }
private void ThrowIfSent() private void ThrowIfReadOnly()
{ {
if (Sent) if (IsReadOnly)
{ {
throw new InvalidOperationException("The response headers cannot be modified because they have already been sent."); throw new InvalidOperationException("The response headers cannot be modified because the response has already started.");
} }
} }
} }

View File

@ -158,9 +158,9 @@ namespace Microsoft.Net.Http.Server
public Task<Stream> UpgradeAsync() public Task<Stream> UpgradeAsync()
{ {
if (!IsUpgradableRequest || _response.HeadersSent) if (!IsUpgradableRequest || _response.HasStarted)
{ {
throw new InvalidOperationException(); throw new InvalidOperationException("This request cannot be upgraded. It is incompatible, or the response has already started.");
} }
// Set the status code and reason phrase // Set the status code and reason phrase

View File

@ -33,6 +33,7 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Framework.Logging; using Microsoft.Framework.Logging;
using static Microsoft.Net.Http.Server.UnsafeNclNativeMethods;
namespace Microsoft.Net.Http.Server namespace Microsoft.Net.Http.Server
{ {
@ -46,18 +47,34 @@ namespace Microsoft.Net.Http.Server
private ResponseStream _nativeStream; private ResponseStream _nativeStream;
private long _expectedBodyLength; private long _expectedBodyLength;
private BoundaryType _boundaryType; private BoundaryType _boundaryType;
private UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2 _nativeResponse; private HttpApi.HTTP_RESPONSE_V2 _nativeResponse;
private IList<Tuple<Action<object>, object>> _onSendingHeadersActions; private IList<Tuple<Action<object>, object>> _onSendingHeadersActions;
private IList<Tuple<Action<object>, object>> _onResponseCompletedActions; private IList<Tuple<Action<object>, object>> _onResponseCompletedActions;
private RequestContext _requestContext; private RequestContext _requestContext;
private bool _bufferingEnabled;
internal Response(RequestContext httpContext) internal Response(RequestContext requestContext)
{ {
// TODO: Verbose log // TODO: Verbose log
_requestContext = httpContext; _requestContext = requestContext;
_nativeResponse = new UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2();
_headers = new HeaderCollection(); _headers = new HeaderCollection();
Reset();
}
public void Reset()
{
if (_responseState >= ResponseState.StartedSending)
{
_requestContext.Abort();
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();
_headers.IsReadOnly = false;
_headers.Clear();
_reasonPhrase = null;
_boundaryType = BoundaryType.None; _boundaryType = BoundaryType.None;
_nativeResponse.Response_V1.StatusCode = (ushort)HttpStatusCode.OK; _nativeResponse.Response_V1.StatusCode = (ushort)HttpStatusCode.OK;
_nativeResponse.Response_V1.Version.MajorVersion = 1; _nativeResponse.Response_V1.Version.MajorVersion = 1;
@ -65,13 +82,17 @@ namespace Microsoft.Net.Http.Server
_responseState = ResponseState.Created; _responseState = ResponseState.Created;
_onSendingHeadersActions = new List<Tuple<Action<object>, object>>(); _onSendingHeadersActions = new List<Tuple<Action<object>, object>>();
_onResponseCompletedActions = new List<Tuple<Action<object>, object>>(); _onResponseCompletedActions = new List<Tuple<Action<object>, object>>();
_bufferingEnabled = _requestContext.Server.BufferResponses;
_expectedBodyLength = 0;
_nativeStream = null;
} }
private enum ResponseState private enum ResponseState
{ {
Created, Created,
Started,
ComputedHeaders, ComputedHeaders,
SentHeaders, StartedSending,
Closed, Closed,
} }
@ -105,14 +126,6 @@ namespace Microsoft.Net.Http.Server
} }
} }
private void CheckResponseStarted()
{
if (_responseState >= ResponseState.SentHeaders)
{
throw new InvalidOperationException("Headers already sent.");
}
}
public string ReasonPhrase public string ReasonPhrase
{ {
get { return _reasonPhrase; } get { return _reasonPhrase; }
@ -124,6 +137,16 @@ namespace Microsoft.Net.Http.Server
} }
} }
public bool ShouldBuffer
{
get { return _bufferingEnabled; }
set
{
CheckResponseStarted();
_bufferingEnabled = value;
}
}
public Stream Body public Stream Body
{ {
get get
@ -168,10 +191,7 @@ namespace Microsoft.Net.Http.Server
internal long ExpectedBodyLength internal long ExpectedBodyLength
{ {
get get { return _expectedBodyLength; }
{
return _expectedBodyLength;
}
} }
// Header accessors // Header accessors
@ -248,6 +268,7 @@ namespace Microsoft.Net.Http.Server
{ {
return; return;
} }
Start();
NotifyOnResponseCompleted(); NotifyOnResponseCompleted();
// TODO: Verbose log // TODO: Verbose log
EnsureResponseStream(); EnsureResponseStream();
@ -259,26 +280,30 @@ namespace Microsoft.Net.Http.Server
internal BoundaryType BoundaryType internal BoundaryType BoundaryType
{ {
get get { return _boundaryType; }
{
return _boundaryType;
}
} }
public bool HeadersSent public bool HasStarted
{ {
get get { return _responseState >= ResponseState.Started; }
}
private void CheckResponseStarted()
{ {
return _responseState >= ResponseState.SentHeaders; if (HasStarted)
{
throw new InvalidOperationException("Headers already sent.");
} }
} }
internal bool ComputedHeaders internal bool ComputedHeaders
{ {
get get { return _responseState >= ResponseState.ComputedHeaders; }
{
return _responseState >= ResponseState.ComputedHeaders;
} }
public bool HasStartedSending
{
get { return _responseState >= ResponseState.StartedSending; }
} }
private void EnsureResponseStream() private void EnsureResponseStream()
@ -319,14 +344,14 @@ namespace Microsoft.Net.Http.Server
// What would we loose by bypassing HttpSendHttpResponse? // What would we loose by bypassing HttpSendHttpResponse?
// //
// TODO: Consider using the HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA flag for most/all responses rather than just Opaque. // TODO: Consider using the HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA flag for most/all responses rather than just Opaque.
internal unsafe uint SendHeaders(UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK* pDataChunk, internal unsafe uint SendHeaders(HttpApi.HTTP_DATA_CHUNK[] dataChunks,
ResponseStreamAsyncResult asyncResult, ResponseStreamAsyncResult asyncResult,
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags, HttpApi.HTTP_FLAGS flags,
bool isOpaqueUpgrade) bool isOpaqueUpgrade)
{ {
Debug.Assert(!HeadersSent, "HttpListenerResponse::SendHeaders()|SentHeaders is true."); Debug.Assert(!HasStartedSending, "HttpListenerResponse::SendHeaders()|SentHeaders is true.");
_responseState = ResponseState.SentHeaders; _responseState = ResponseState.StartedSending;
var reasonPhrase = GetReasonPhrase(StatusCode); var reasonPhrase = GetReasonPhrase(StatusCode);
if (RequestContext.Logger.IsEnabled(LogLevel.Verbose)) if (RequestContext.Logger.IsEnabled(LogLevel.Verbose))
@ -344,10 +369,16 @@ namespace Microsoft.Net.Http.Server
List<GCHandle> pinnedHeaders = SerializeHeaders(isOpaqueUpgrade); List<GCHandle> pinnedHeaders = SerializeHeaders(isOpaqueUpgrade);
try try
{ {
if (pDataChunk != null) if (dataChunks != null)
{ {
_nativeResponse.Response_V1.EntityChunkCount = 1; if (pinnedHeaders == null)
_nativeResponse.Response_V1.pEntityChunks = pDataChunk; {
pinnedHeaders = new List<GCHandle>();
}
var handle = GCHandle.Alloc(dataChunks, GCHandleType.Pinned);
pinnedHeaders.Add(handle);
_nativeResponse.Response_V1.EntityChunkCount = (ushort)dataChunks.Length;
_nativeResponse.Response_V1.pEntityChunks = (HttpApi.HTTP_DATA_CHUNK*)handle.AddrOfPinnedObject();
} }
else if (asyncResult != null && asyncResult.DataChunks != null) else if (asyncResult != null && asyncResult.DataChunks != null)
{ {
@ -360,18 +391,16 @@ namespace Microsoft.Net.Http.Server
_nativeResponse.Response_V1.pEntityChunks = null; _nativeResponse.Response_V1.pEntityChunks = null;
} }
if (reasonPhrase.Length > 0)
{
byte[] reasonPhraseBytes = new byte[HeaderEncoding.GetByteCount(reasonPhrase)]; byte[] reasonPhraseBytes = new byte[HeaderEncoding.GetByteCount(reasonPhrase)];
fixed (byte* pReasonPhrase = reasonPhraseBytes) fixed (byte* pReasonPhrase = reasonPhraseBytes)
{ {
_nativeResponse.Response_V1.ReasonLength = (ushort)reasonPhraseBytes.Length; _nativeResponse.Response_V1.ReasonLength = (ushort)reasonPhraseBytes.Length;
HeaderEncoding.GetBytes(reasonPhrase, 0, reasonPhraseBytes.Length, reasonPhraseBytes, 0); HeaderEncoding.GetBytes(reasonPhrase, 0, reasonPhraseBytes.Length, reasonPhraseBytes, 0);
_nativeResponse.Response_V1.pReason = (sbyte*)pReasonPhrase; _nativeResponse.Response_V1.pReason = (sbyte*)pReasonPhrase;
fixed (UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2* pResponse = &_nativeResponse) fixed (HttpApi.HTTP_RESPONSE_V2* pResponse = &_nativeResponse)
{ {
statusCode = statusCode =
UnsafeNclNativeMethods.HttpApi.HttpSendHttpResponse( HttpApi.HttpSendHttpResponse(
RequestContext.RequestQueueHandle, RequestContext.RequestQueueHandle,
Request.RequestId, Request.RequestId,
(uint)flags, (uint)flags,
@ -384,34 +413,7 @@ namespace Microsoft.Net.Http.Server
IntPtr.Zero); IntPtr.Zero);
if (asyncResult != null && if (asyncResult != null &&
statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode == ErrorCodes.ERROR_SUCCESS &&
WebListener.SkipIOCPCallbackOnSuccess)
{
asyncResult.BytesSent = bytesSent;
// The caller will invoke IOCompleted
}
}
}
}
else
{
fixed (UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2* pResponse = &_nativeResponse)
{
statusCode =
UnsafeNclNativeMethods.HttpApi.HttpSendHttpResponse(
RequestContext.RequestQueueHandle,
Request.RequestId,
(uint)flags,
pResponse,
null,
&bytesSent,
SafeLocalFree.Zero,
0,
asyncResult == null ? SafeNativeOverlapped.Zero : asyncResult.NativeOverlapped,
IntPtr.Zero);
if (asyncResult != null &&
statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
WebListener.SkipIOCPCallbackOnSuccess) WebListener.SkipIOCPCallbackOnSuccess)
{ {
asyncResult.BytesSent = bytesSent; asyncResult.BytesSent = bytesSent;
@ -427,10 +429,20 @@ namespace Microsoft.Net.Http.Server
return statusCode; return statusCode;
} }
internal UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS ComputeHeaders(bool endOfRequest = false) internal void Start()
{
if (!HasStarted)
{ {
// Notify that this is absolutely the last chance to make changes. // Notify that this is absolutely the last chance to make changes.
NotifyOnSendingHeaders(); 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)
@ -438,7 +450,7 @@ namespace Microsoft.Net.Http.Server
RequestContext.Server.AuthenticationManager.SetAuthenticationChallenge(RequestContext); RequestContext.Server.AuthenticationManager.SetAuthenticationChallenge(RequestContext);
} }
var flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE; var flags = HttpApi.HTTP_FLAGS.NONE;
Debug.Assert(!ComputedHeaders, "HttpListenerResponse::ComputeHeaders()|ComputedHeaders is true."); Debug.Assert(!ComputedHeaders, "HttpListenerResponse::ComputeHeaders()|ComputedHeaders is true.");
_responseState = ResponseState.ComputedHeaders; _responseState = ResponseState.ComputedHeaders;
@ -478,14 +490,18 @@ namespace Microsoft.Net.Http.Server
// The application is performing it's own chunking. // The application is performing it's own chunking.
_boundaryType = BoundaryType.PassThrough; _boundaryType = BoundaryType.PassThrough;
} }
else if (endOfRequest && !(isHeadRequest && statusCanHaveBody)) // HEAD requests 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 (statusCanHaveBody) if (bufferedBytes > 0)
{
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 = 0; _expectedBodyLength = bufferedBytes;
} }
else if (keepConnectionAlive && requestVersion == Constants.V1_1) else if (keepConnectionAlive && requestVersion == Constants.V1_1)
{ {
@ -508,9 +524,10 @@ namespace Microsoft.Net.Http.Server
{ {
Headers.Append(HttpKnownHeaderNames.Connection, Constants.Close); Headers.Append(HttpKnownHeaderNames.Connection, Constants.Close);
} }
flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT; flags = HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT;
} }
Headers.IsReadOnly = true; // Prohibit further modifications.
return flags; return flags;
} }
@ -521,9 +538,9 @@ namespace Microsoft.Net.Http.Server
private List<GCHandle> SerializeHeaders(bool isOpaqueUpgrade) private List<GCHandle> SerializeHeaders(bool isOpaqueUpgrade)
{ {
Headers.Sent = true; // Prohibit further modifications. Headers.IsReadOnly = true; // Prohibit further modifications.
UnsafeNclNativeMethods.HttpApi.HTTP_UNKNOWN_HEADER[] unknownHeaders = null; HttpApi.HTTP_UNKNOWN_HEADER[] unknownHeaders = null;
UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO[] knownHeaderInfo = null; HttpApi.HTTP_RESPONSE_INFO[] knownHeaderInfo = null;
List<GCHandle> pinnedHeaders; List<GCHandle> pinnedHeaders;
GCHandle gcHandle; GCHandle gcHandle;
/* /*
@ -552,11 +569,11 @@ namespace Microsoft.Net.Http.Server
continue; continue;
} }
// See if this is an unknown header // See if this is an unknown header
lookup = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerPair.Key); lookup = HttpApi.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerPair.Key);
// Http.Sys doesn't let us send the Connection: Upgrade header as a Known header. // Http.Sys doesn't let us send the Connection: Upgrade header as a Known header.
if (lookup == -1 || if (lookup == -1 ||
(isOpaqueUpgrade && lookup == (int)UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADER_ID.Enum.HttpHeaderConnection)) (isOpaqueUpgrade && lookup == (int)HttpApi.HTTP_RESPONSE_HEADER_ID.Enum.HttpHeaderConnection))
{ {
numUnknownHeaders += headerPair.Value.Length; numUnknownHeaders += headerPair.Value.Length;
} }
@ -569,7 +586,7 @@ namespace Microsoft.Net.Http.Server
try try
{ {
fixed (UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER* pKnownHeaders = &_nativeResponse.Response_V1.Headers.KnownHeaders) fixed (HttpApi.HTTP_KNOWN_HEADER* pKnownHeaders = &_nativeResponse.Response_V1.Headers.KnownHeaders)
{ {
foreach (KeyValuePair<string, string[]> headerPair in Headers) foreach (KeyValuePair<string, string[]> headerPair in Headers)
{ {
@ -580,18 +597,18 @@ namespace Microsoft.Net.Http.Server
} }
headerName = headerPair.Key; headerName = headerPair.Key;
string[] headerValues = headerPair.Value; string[] headerValues = headerPair.Value;
lookup = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerName); lookup = HttpApi.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerName);
// Http.Sys doesn't let us send the Connection: Upgrade header as a Known header. // Http.Sys doesn't let us send the Connection: Upgrade header as a Known header.
if (lookup == -1 || if (lookup == -1 ||
(isOpaqueUpgrade && lookup == (int)UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADER_ID.Enum.HttpHeaderConnection)) (isOpaqueUpgrade && lookup == (int)HttpApi.HTTP_RESPONSE_HEADER_ID.Enum.HttpHeaderConnection))
{ {
if (unknownHeaders == null) if (unknownHeaders == null)
{ {
unknownHeaders = new UnsafeNclNativeMethods.HttpApi.HTTP_UNKNOWN_HEADER[numUnknownHeaders]; unknownHeaders = new HttpApi.HTTP_UNKNOWN_HEADER[numUnknownHeaders];
gcHandle = GCHandle.Alloc(unknownHeaders, GCHandleType.Pinned); gcHandle = GCHandle.Alloc(unknownHeaders, GCHandleType.Pinned);
pinnedHeaders.Add(gcHandle); pinnedHeaders.Add(gcHandle);
_nativeResponse.Response_V1.Headers.pUnknownHeaders = (UnsafeNclNativeMethods.HttpApi.HTTP_UNKNOWN_HEADER*)gcHandle.AddrOfPinnedObject(); _nativeResponse.Response_V1.Headers.pUnknownHeaders = (HttpApi.HTTP_UNKNOWN_HEADER*)gcHandle.AddrOfPinnedObject();
} }
for (int headerValueIndex = 0; headerValueIndex < headerValues.Length; headerValueIndex++) for (int headerValueIndex = 0; headerValueIndex < headerValues.Length; headerValueIndex++)
@ -632,29 +649,29 @@ namespace Microsoft.Net.Http.Server
{ {
if (knownHeaderInfo == null) if (knownHeaderInfo == null)
{ {
knownHeaderInfo = new UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO[numKnownMultiHeaders]; knownHeaderInfo = new HttpApi.HTTP_RESPONSE_INFO[numKnownMultiHeaders];
gcHandle = GCHandle.Alloc(knownHeaderInfo, GCHandleType.Pinned); gcHandle = GCHandle.Alloc(knownHeaderInfo, GCHandleType.Pinned);
pinnedHeaders.Add(gcHandle); pinnedHeaders.Add(gcHandle);
_nativeResponse.pResponseInfo = (UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO*)gcHandle.AddrOfPinnedObject(); _nativeResponse.pResponseInfo = (HttpApi.HTTP_RESPONSE_INFO*)gcHandle.AddrOfPinnedObject();
} }
knownHeaderInfo[_nativeResponse.ResponseInfoCount].Type = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO_TYPE.HttpResponseInfoTypeMultipleKnownHeaders; knownHeaderInfo[_nativeResponse.ResponseInfoCount].Type = HttpApi.HTTP_RESPONSE_INFO_TYPE.HttpResponseInfoTypeMultipleKnownHeaders;
knownHeaderInfo[_nativeResponse.ResponseInfoCount].Length = knownHeaderInfo[_nativeResponse.ResponseInfoCount].Length =
#if DNXCORE50 #if DNXCORE50
(uint)Marshal.SizeOf<UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS>(); (uint)Marshal.SizeOf<HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS>();
#else #else
(uint)Marshal.SizeOf(typeof(UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS)); (uint)Marshal.SizeOf(typeof(HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS));
#endif #endif
UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS header = new UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS(); HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS header = new HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS();
header.HeaderId = (UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADER_ID.Enum)lookup; header.HeaderId = (HttpApi.HTTP_RESPONSE_HEADER_ID.Enum)lookup;
header.Flags = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO_FLAGS.PreserveOrder; // TODO: The docs say this is for www-auth only. header.Flags = HttpApi.HTTP_RESPONSE_INFO_FLAGS.PreserveOrder; // TODO: The docs say this is for www-auth only.
UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER[] nativeHeaderValues = new UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER[headerValues.Length]; HttpApi.HTTP_KNOWN_HEADER[] nativeHeaderValues = new HttpApi.HTTP_KNOWN_HEADER[headerValues.Length];
gcHandle = GCHandle.Alloc(nativeHeaderValues, GCHandleType.Pinned); gcHandle = GCHandle.Alloc(nativeHeaderValues, GCHandleType.Pinned);
pinnedHeaders.Add(gcHandle); pinnedHeaders.Add(gcHandle);
header.KnownHeaders = (UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER*)gcHandle.AddrOfPinnedObject(); header.KnownHeaders = (HttpApi.HTTP_KNOWN_HEADER*)gcHandle.AddrOfPinnedObject();
for (int headerValueIndex = 0; headerValueIndex < headerValues.Length; headerValueIndex++) for (int headerValueIndex = 0; headerValueIndex < headerValues.Length; headerValueIndex++)
{ {
@ -672,7 +689,7 @@ namespace Microsoft.Net.Http.Server
// This type is a struct, not an object, so pinning it causes a boxed copy to be created. We can't do that until after all the fields are set. // This type is a struct, not an object, so pinning it causes a boxed copy to be created. We can't do that until after all the fields are set.
gcHandle = GCHandle.Alloc(header, GCHandleType.Pinned); gcHandle = GCHandle.Alloc(header, GCHandleType.Pinned);
pinnedHeaders.Add(gcHandle); pinnedHeaders.Add(gcHandle);
knownHeaderInfo[_nativeResponse.ResponseInfoCount].pInfo = (UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS*)gcHandle.AddrOfPinnedObject(); knownHeaderInfo[_nativeResponse.ResponseInfoCount].pInfo = (HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS*)gcHandle.AddrOfPinnedObject();
_nativeResponse.ResponseInfoCount++; _nativeResponse.ResponseInfoCount++;
} }
@ -704,18 +721,18 @@ namespace Microsoft.Net.Http.Server
// Subset of ComputeHeaders // Subset of ComputeHeaders
internal void SendOpaqueUpgrade() internal void SendOpaqueUpgrade()
{ {
// TODO: Should we do this notification earlier when you still have a chance to change the status code to avoid an upgrade?
// Notify that this is absolutely the last chance to make changes. // Notify that this is absolutely the last chance to make changes.
NotifyOnSendingHeaders(); Start();
_boundaryType = BoundaryType.Close;
// TODO: Send headers async? // TODO: Send headers async?
ulong errorCode = SendHeaders(null, null, ulong errorCode = SendHeaders(null, null,
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_OPAQUE | HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_OPAQUE |
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA | HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA |
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA, HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA,
true); true);
if (errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) if (errorCode != ErrorCodes.ERROR_SUCCESS)
{ {
throw new WebListenerException((int)errorCode); throw new WebListenerException((int)errorCode);
} }
@ -725,7 +742,7 @@ namespace Microsoft.Net.Http.Server
{ {
if (_responseState >= ResponseState.Closed) if (_responseState >= ResponseState.Closed)
{ {
throw new ObjectDisposedException(this.GetType().FullName); throw new ObjectDisposedException(GetType().FullName);
} }
} }
@ -746,6 +763,7 @@ namespace Microsoft.Net.Http.Server
internal void SwitchToOpaqueMode() internal void SwitchToOpaqueMode()
{ {
EnsureResponseStream(); EnsureResponseStream();
_bufferingEnabled = false;
_nativeStream.SwitchToOpaqueMode(); _nativeStream.SwitchToOpaqueMode();
} }

View File

@ -22,23 +22,27 @@
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using static Microsoft.Net.Http.Server.UnsafeNclNativeMethods;
namespace Microsoft.Net.Http.Server namespace Microsoft.Net.Http.Server
{ {
internal class ResponseStream : Stream internal class ResponseStream : Stream
{ {
private static readonly byte[] ChunkTerminator = new byte[] { (byte)'0', (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' }; 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;
@ -99,49 +103,185 @@ namespace Microsoft.Net.Http.Server
// Send headers // Send headers
public override void Flush() public override void Flush()
{ {
if (_closed || _requestContext.Response.HeadersSent) if (_closed)
{ {
return; return;
} }
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite(); FlushInternal(endOfRequest: false);
// TODO: Verbose log }
private unsafe void FlushInternal(bool endOfRequest)
{
bool startedSending = _requestContext.Response.HasStartedSending;
var byteCount = _buffer.TotalBytes;
if (byteCount == 0 && startedSending && !endOfRequest)
{
// Empty flush
return;
}
var flags = ComputeLeftToWrite(endOfRequest);
if (!_inOpaqueMode && endOfRequest && _leftToWrite > byteCount)
{
_requestContext.Abort();
// This is logged rather than thrown because it is too late for an exception to be visible in user code.
LogHelper.LogError(_requestContext.Logger, "ResponseStream::Dispose", "Fewer bytes were written than were specified in the Content-Length.");
return;
}
if (endOfRequest && _requestContext.Response.BoundaryType == BoundaryType.Close)
{
flags |= HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT;
}
else if (!endOfRequest && _leftToWrite != byteCount)
{
flags |= HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
}
UpdateWritenCount((uint)byteCount);
uint statusCode = 0;
HttpApi.HTTP_DATA_CHUNK[] dataChunks;
var pinnedBuffers = PinDataBuffers(endOfRequest, out dataChunks);
try try
{ {
uint statusCode; if (!startedSending)
unsafe
{ {
// TODO: Don't add MoreData flag if content-length == 0? statusCode = _requestContext.Response.SendHeaders(dataChunks, null, flags, false);
flags |= UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA; }
statusCode = _requestContext.Response.SendHeaders(null, null, flags, false); else
{
fixed (HttpApi.HTTP_DATA_CHUNK* pDataChunks = dataChunks)
{
statusCode = HttpApi.HttpSendResponseEntityBody(
_requestContext.RequestQueueHandle,
_requestContext.RequestId,
(uint)flags,
(ushort)dataChunks.Length,
pDataChunks,
null,
SafeLocalFree.Zero,
0,
SafeNativeOverlapped.Zero,
IntPtr.Zero);
} }
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF) if (_requestContext.Server.IgnoreWriteExceptions)
{ {
throw new IOException(string.Empty, new WebListenerException((int)statusCode)); statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS;
} }
} }
catch (Exception e) }
finally
{ {
LogHelper.LogException(_requestContext.Logger, "Flush", e); FreeDataBuffers(pinnedBuffers);
_buffer.Clear();
}
if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_HANDLE_EOF
// Don't throw for disconnects, we were already finished with the response.
&& (!endOfRequest || (statusCode != ErrorCodes.ERROR_CONNECTION_INVALID && statusCode != ErrorCodes.ERROR_INVALID_PARAMETER)))
{
Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode));
LogHelper.LogException(_requestContext.Logger, "Flush", exception);
Abort(); Abort();
throw; throw exception;
} }
} }
// Send headers private List<GCHandle> PinDataBuffers(bool endOfRequest, out HttpApi.HTTP_DATA_CHUNK[] dataChunks)
public override Task FlushAsync(CancellationToken cancellationToken)
{ {
if (_closed || _requestContext.Response.HeadersSent) 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)
{
dataChunks = new HttpApi.HTTP_DATA_CHUNK[1];
SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment<byte>(Helpers.ChunkTerminator));
return pins;
}
else if (_buffer.TotalBytes == 0)
{
// No data
dataChunks = new HttpApi.HTTP_DATA_CHUNK[0];
return pins;
}
var chunkCount = _buffer.BufferCount;
if (chunked)
{
// Chunk framing
chunkCount += 2;
if (endOfRequest)
{
// Chunk terminator
chunkCount += 1;
}
}
dataChunks = new HttpApi.HTTP_DATA_CHUNK[chunkCount];
if (chunked)
{
var chunkHeaderBuffer = Helpers.GetChunkHeader(_buffer.TotalBytes);
SetDataChunk(dataChunks, ref currentChunk, pins, chunkHeaderBuffer);
}
foreach (var buffer in _buffer.Buffers)
{
SetDataChunk(dataChunks, ref currentChunk, pins, buffer);
}
if (chunked)
{
SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment<byte>(Helpers.CRLF));
if (endOfRequest)
{
SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment<byte>(Helpers.ChunkTerminator));
}
}
return pins;
}
private static void SetDataChunk(HttpApi.HTTP_DATA_CHUNK[] chunks, ref int chunkIndex, List<GCHandle> pins, ArraySegment<byte> buffer)
{
var handle = GCHandle.Alloc(buffer.Array, GCHandleType.Pinned);
pins.Add(handle);
chunks[chunkIndex].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
chunks[chunkIndex].fromMemory.pBuffer = handle.AddrOfPinnedObject() + buffer.Offset;
chunks[chunkIndex].fromMemory.BufferLength = (uint)buffer.Count;
chunkIndex++;
}
private void FreeDataBuffers(List<GCHandle> pinnedBuffers)
{
foreach (var pin in pinnedBuffers)
{
if (pin.IsAllocated)
{
pin.Free();
}
}
}
// 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)
{ {
return Helpers.CompletedTask(); return Helpers.CompletedTask();
} }
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite();
// TODO: Verbose log
if (cancellationToken.IsCancellationRequested) bool startedSending = _requestContext.Response.HasStartedSending;
var byteCount = _buffer.TotalBytes;
if (byteCount == 0 && startedSending)
{ {
return Helpers.CanceledTask<int>(); // Empty flush
return Helpers.CompletedTask();
} }
var cancellationRegistration = default(CancellationTokenRegistration); var cancellationRegistration = default(CancellationTokenRegistration);
@ -150,26 +290,37 @@ namespace Microsoft.Net.Http.Server
cancellationRegistration = cancellationToken.Register(RequestContext.AbortDelegate, _requestContext); cancellationRegistration = cancellationToken.Register(RequestContext.AbortDelegate, _requestContext);
} }
// TODO: Don't add MoreData flag if content-length == 0? var flags = ComputeLeftToWrite();
flags |= UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA; if (_leftToWrite != byteCount)
ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, null, null, null, 0, 0, _requestContext.Response.BoundaryType == BoundaryType.Chunked, false, cancellationRegistration); {
flags |= HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
}
UpdateWritenCount((uint)byteCount);
uint statusCode = 0;
var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
var asyncResult = new ResponseStreamAsyncResult(this, _buffer, chunked, cancellationRegistration);
uint bytesSent = 0;
try try
{ {
uint statusCode; if (!startedSending)
unsafe
{ {
statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false); statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false);
bytesSent = asyncResult.BytesSent;
} }
else
if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess)
{ {
// IO operation completed synchronously - callback won't be called to signal completion. statusCode = HttpApi.HttpSendResponseEntityBody(
asyncResult.IOCompleted(statusCode); _requestContext.RequestQueueHandle,
} _requestContext.RequestId,
else if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING) (uint)flags,
{ asyncResult.DataChunkCount,
throw new IOException(string.Empty, new WebListenerException((int)statusCode)); asyncResult.DataChunks,
&bytesSent,
SafeLocalFree.Zero,
0,
asyncResult.NativeOverlapped,
IntPtr.Zero);
} }
} }
catch (Exception e) catch (Exception e)
@ -180,6 +331,34 @@ namespace Microsoft.Net.Http.Server
throw; throw;
} }
if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_IO_PENDING)
{
asyncResult.Dispose();
if (_requestContext.Server.IgnoreWriteExceptions && startedSending)
{
asyncResult.Complete();
}
else
{
Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode));
LogHelper.LogException(_requestContext.Logger, "FlushAsync", exception);
Abort();
throw exception;
}
}
if (statusCode == ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess)
{
// IO operation completed synchronously - callback won't be called to signal completion.
asyncResult.IOCompleted(statusCode, bytesSent);
}
// Last write, cache it for special cancellation handling.
if ((flags & HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA) == 0)
{
_lastWrite = asyncResult;
}
return asyncResult.Task; return asyncResult.Task;
} }
@ -195,13 +374,13 @@ namespace Microsoft.Net.Http.Server
throw new NotSupportedException(Resources.Exception_NoSeek); throw new NotSupportedException(Resources.Exception_NoSeek);
} }
public override int Read([In, Out] byte[] buffer, int offset, int size) public override int Read([In, Out] byte[] buffer, int offset, int count)
{ {
throw new InvalidOperationException(Resources.Exception_WriteOnlyStream); throw new InvalidOperationException(Resources.Exception_WriteOnlyStream);
} }
#if !DNXCORE50 #if !DNXCORE50
public override IAsyncResult BeginRead(byte[] buffer, int offset, int size, AsyncCallback callback, object state) public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
{ {
throw new InvalidOperationException(Resources.Exception_WriteOnlyStream); throw new InvalidOperationException(Resources.Exception_WriteOnlyStream);
} }
@ -225,7 +404,7 @@ namespace Microsoft.Net.Http.Server
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE; UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE;
if (!_requestContext.Response.ComputedHeaders) if (!_requestContext.Response.ComputedHeaders)
{ {
flags = _requestContext.Response.ComputeHeaders(endOfRequest: endOfRequest); flags = _requestContext.Response.ComputeHeaders(endOfRequest, _buffer.TotalBytes);
} }
if (_leftToWrite == long.MinValue) if (_leftToWrite == long.MinValue)
{ {
@ -246,229 +425,56 @@ namespace Microsoft.Net.Http.Server
return flags; return flags;
} }
public override unsafe void Write(byte[] buffer, int offset, int size) public override void Write(byte[] buffer, int offset, int count)
{ {
if (buffer == null) // Validates for null and bounds. Allows count == 0.
{ var data = new ArraySegment<byte>(buffer, offset, count);
throw new ArgumentNullException("buffer"); CheckDisposed();
}
if (offset < 0 || offset > buffer.Length)
{
throw new ArgumentOutOfRangeException("offset");
}
if (size < 0 || size > buffer.Length - offset)
{
throw new ArgumentOutOfRangeException("size");
}
if (_closed)
{
throw new ObjectDisposedException(GetType().FullName);
}
// TODO: Verbose log parameters // TODO: Verbose log parameters
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite(); // Officially starts the response and fires OnSendingHeaders
if (size == 0 && _leftToWrite != 0) _requestContext.Response.Start();
{
return;
}
if (_leftToWrite >= 0 && size > _leftToWrite)
{
throw new InvalidOperationException(Resources.Exception_TooMuchWritten);
}
// TODO: Verbose log
uint statusCode; var currentBytes = _buffer.TotalBytes + data.Count;
uint dataToWrite = (uint)size; var contentLength = _requestContext.Response.ContentLength;
SafeLocalFree bufferAsIntPtr = null; if (contentLength.HasValue && !_requestContext.Response.ComputedHeaders && contentLength.Value <= currentBytes)
IntPtr pBufferAsIntPtr = IntPtr.Zero;
bool sentHeaders = _requestContext.Response.HeadersSent;
try
{ {
if (size == 0) if (contentLength.Value < currentBytes)
{ {
// TODO: Is this code path accessible? Is this like a Flush? throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
statusCode = _requestContext.Response.SendHeaders(null, null, flags, false); }
// 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)
{
if (_leftToWrite < currentBytes)
{
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 else
{ {
fixed (byte* pDataBuffer = buffer) // Append to existing data without a copy, and then flush immidately
{ _buffer.Add(data);
byte* pBuffer = pDataBuffer; Flush();
if (_requestContext.Response.BoundaryType == BoundaryType.Chunked)
{
// TODO:
// here we need some heuristics, some time it is definitely better to split this in 3 write calls
// but for small writes it is probably good enough to just copy the data internally.
string chunkHeader = size.ToString("x", CultureInfo.InvariantCulture);
dataToWrite = dataToWrite + (uint)(chunkHeader.Length + 4);
bufferAsIntPtr = SafeLocalFree.LocalAlloc((int)dataToWrite);
pBufferAsIntPtr = bufferAsIntPtr.DangerousGetHandle();
for (int i = 0; i < chunkHeader.Length; i++)
{
Marshal.WriteByte(pBufferAsIntPtr, i, (byte)chunkHeader[i]);
}
Marshal.WriteInt16(pBufferAsIntPtr, chunkHeader.Length, 0x0A0D);
Marshal.Copy(buffer, offset, IntPtrHelper.Add(pBufferAsIntPtr, chunkHeader.Length + 2), size);
Marshal.WriteInt16(pBufferAsIntPtr, (int)(dataToWrite - 2), 0x0A0D);
pBuffer = (byte*)pBufferAsIntPtr;
offset = 0;
}
UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK dataChunk = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
dataChunk.DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
dataChunk.fromMemory.pBuffer = (IntPtr)(pBuffer + offset);
dataChunk.fromMemory.BufferLength = dataToWrite;
flags |= _leftToWrite == size ? UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE : UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
if (!sentHeaders)
{
statusCode = _requestContext.Response.SendHeaders(&dataChunk, null, flags, false);
}
else
{
statusCode =
UnsafeNclNativeMethods.HttpApi.HttpSendResponseEntityBody(
_requestContext.RequestQueueHandle,
_requestContext.RequestId,
(uint)flags,
1,
&dataChunk,
null,
SafeLocalFree.Zero,
0,
SafeNativeOverlapped.Zero,
IntPtr.Zero);
if (_requestContext.Server.IgnoreWriteExceptions)
{
statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS;
}
}
}
}
}
finally
{
if (bufferAsIntPtr != null)
{
// free unmanaged buffer
bufferAsIntPtr.Dispose();
} }
} }
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF)
{
Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode));
LogHelper.LogException(_requestContext.Logger, "Write", exception);
Abort();
throw exception;
}
UpdateWritenCount(dataToWrite);
// TODO: Verbose log data written
}
#if DNXCORE50 #if DNXCORE50
public unsafe IAsyncResult BeginWrite(byte[] buffer, int offset, int size, AsyncCallback callback, object state) public unsafe IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
#else #else
public override unsafe IAsyncResult BeginWrite(byte[] buffer, int offset, int size, AsyncCallback callback, object state) public override unsafe IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
#endif #endif
{ {
if (buffer == null) return WriteAsync(buffer, offset, count).ToIAsyncResult(callback, state);
{
throw new ArgumentNullException("buffer");
}
if (offset < 0 || offset > buffer.Length)
{
throw new ArgumentOutOfRangeException("offset");
}
if (size < 0 || size > buffer.Length - offset)
{
throw new ArgumentOutOfRangeException("size");
}
if (_closed)
{
throw new ObjectDisposedException(GetType().FullName);
}
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite();
if (size == 0 && _leftToWrite != 0)
{
ResponseStreamAsyncResult result = new ResponseStreamAsyncResult(this, state, callback);
result.Complete();
return result;
}
if (_leftToWrite >= 0 && size > _leftToWrite)
{
throw new InvalidOperationException(Resources.Exception_TooMuchWritten);
}
// TODO: Verbose log parameters
uint statusCode;
uint bytesSent = 0;
flags |= _leftToWrite == size ? UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE : UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
bool sentHeaders = _requestContext.Response.HeadersSent;
ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, state, callback, buffer, offset, size, _requestContext.Response.BoundaryType == BoundaryType.Chunked, sentHeaders);
// Update m_LeftToWrite now so we can queue up additional BeginWrite's without waiting for EndWrite.
UpdateWritenCount((uint)((_requestContext.Response.BoundaryType == BoundaryType.Chunked) ? 0 : size));
try
{
if (!sentHeaders)
{
statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false);
bytesSent = asyncResult.BytesSent;
}
else
{
statusCode =
UnsafeNclNativeMethods.HttpApi.HttpSendResponseEntityBody(
_requestContext.RequestQueueHandle,
_requestContext.RequestId,
(uint)flags,
asyncResult.DataChunkCount,
asyncResult.DataChunks,
&bytesSent,
SafeLocalFree.Zero,
0,
asyncResult.NativeOverlapped,
IntPtr.Zero);
}
}
catch (Exception e)
{
LogHelper.LogException(_requestContext.Logger, "BeginWrite", e);
asyncResult.Dispose();
Abort();
throw;
}
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
{
asyncResult.Dispose();
if (_requestContext.Server.IgnoreWriteExceptions && sentHeaders)
{
asyncResult.Complete();
}
else
{
Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode));
LogHelper.LogException(_requestContext.Logger, "BeginWrite", exception);
Abort();
throw exception;
}
}
if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess)
{
// IO operation completed synchronously - callback won't be called to signal completion.
asyncResult.IOCompleted(statusCode, bytesSent);
}
// Last write, cache it for special cancellation handling.
if ((flags & UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA) == 0)
{
_lastWrite = asyncResult;
}
return asyncResult;
} }
#if DNXCORE50 #if DNXCORE50
public void EndWrite(IAsyncResult asyncResult) public void EndWrite(IAsyncResult asyncResult)
@ -480,143 +486,58 @@ namespace Microsoft.Net.Http.Server
{ {
throw new ArgumentNullException("asyncResult"); throw new ArgumentNullException("asyncResult");
} }
ResponseStreamAsyncResult castedAsyncResult = asyncResult as ResponseStreamAsyncResult; ((Task)asyncResult).GetAwaiter().GetResult();
if (castedAsyncResult == null || castedAsyncResult.ResponseStream != this)
{
throw new ArgumentException(Resources.Exception_WrongIAsyncResult, "asyncResult");
}
if (castedAsyncResult.EndCalled)
{
throw new InvalidOperationException(Resources.Exception_EndCalledMultipleTimes);
}
castedAsyncResult.EndCalled = true;
try
{
// wait & then check for errors
// TODO: Graceful re-throw
castedAsyncResult.Task.Wait();
}
catch (Exception exception)
{
LogHelper.LogException(_requestContext.Logger, "EndWrite", exception);
Abort();
throw;
}
} }
public override unsafe Task WriteAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken) public override unsafe Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{ {
if (buffer == null) // Validates for null and bounds. Allows count == 0.
{ var data = new ArraySegment<byte>(buffer, offset, count);
throw new ArgumentNullException("buffer");
}
if (offset < 0 || offset > buffer.Length)
{
throw new ArgumentOutOfRangeException("offset");
}
if (size < 0 || size > buffer.Length - offset)
{
throw new ArgumentOutOfRangeException("size");
}
if (_closed)
{
throw new ObjectDisposedException(GetType().FullName);
}
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite();
if (size == 0 && _leftToWrite != 0)
{
return Helpers.CompletedTask();
}
if (_leftToWrite >= 0 && size > _leftToWrite)
{
throw new InvalidOperationException(Resources.Exception_TooMuchWritten);
}
// TODO: Verbose log
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
{ {
return Helpers.CanceledTask<int>(); return Helpers.CanceledTask<int>();
} }
CheckDisposed();
// TODO: Verbose log parameters
// Officially starts the response and fires OnSendingHeaders
_requestContext.Response.Start();
var cancellationRegistration = default(CancellationTokenRegistration); var currentBytes = _buffer.TotalBytes + data.Count;
if (cancellationToken.CanBeCanceled) var contentLength = _requestContext.Response.ContentLength;
if (contentLength.HasValue && !_requestContext.Response.ComputedHeaders && contentLength.Value <= currentBytes)
{ {
cancellationRegistration = cancellationToken.Register(RequestContext.AbortDelegate, _requestContext); if (contentLength.Value < currentBytes)
{
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
uint statusCode; _buffer.Add(data);
uint bytesSent = 0; return FlushAsync(cancellationToken);
flags |= _leftToWrite == size ? UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE : UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA; }
bool sentHeaders = _requestContext.Response.HeadersSent; // The last write in a response that has already started, flush immidately
ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, null, null, buffer, offset, size, _requestContext.Response.BoundaryType == BoundaryType.Chunked, sentHeaders, cancellationRegistration); else if (_requestContext.Response.ComputedHeaders && _leftToWrite > 0 && _leftToWrite <= currentBytes)
// Update m_LeftToWrite now so we can queue up additional BeginWrite's without waiting for EndWrite.
UpdateWritenCount((uint)((_requestContext.Response.BoundaryType == BoundaryType.Chunked) ? 0 : size));
try
{ {
if (!sentHeaders) if (_leftToWrite < currentBytes)
{ {
statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false); throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
bytesSent = asyncResult.BytesSent; }
_buffer.Add(data);
return FlushAsync(cancellationToken);
}
else if (_requestContext.Response.ShouldBuffer && currentBytes < MaxBufferSize)
{
_buffer.CopyAndAdd(data);
return Helpers.CompletedTask();
} }
else else
{ {
statusCode = // Append to existing data without a copy, and then flush immidately
UnsafeNclNativeMethods.HttpApi.HttpSendResponseEntityBody( _buffer.Add(data);
_requestContext.RequestQueueHandle, return FlushAsync(cancellationToken);
_requestContext.RequestId,
(uint)flags,
asyncResult.DataChunkCount,
asyncResult.DataChunks,
&bytesSent,
SafeLocalFree.Zero,
0,
asyncResult.NativeOverlapped,
IntPtr.Zero);
}
}
catch (Exception e)
{
LogHelper.LogException(_requestContext.Logger, "WriteAsync", e);
asyncResult.Dispose();
Abort();
throw;
}
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
{
asyncResult.Dispose();
if (_requestContext.Server.IgnoreWriteExceptions && sentHeaders)
{
asyncResult.Complete();
}
else
{
Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode));
LogHelper.LogException(_requestContext.Logger, "WriteAsync", exception);
Abort();
throw exception;
} }
} }
if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess) internal async Task SendFileAsync(string fileName, long offset, long? count, CancellationToken cancellationToken)
{
// IO operation completed synchronously - callback won't be called to signal completion.
asyncResult.IOCompleted(statusCode, bytesSent);
}
// Last write, cache it for special cancellation handling.
if ((flags & UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA) == 0)
{
_lastWrite = asyncResult;
}
return asyncResult.Task;
}
internal unsafe Task SendFileAsync(string fileName, long offset, long? size, CancellationToken cancellationToken)
{ {
// It's too expensive to validate the file attributes before opening the file. Open the file and then check the lengths. // It's too expensive to validate the file attributes before opening the file. Open the file and then check the lengths.
// This all happens inside of ResponseStreamAsyncResult. // This all happens inside of ResponseStreamAsyncResult.
@ -624,17 +545,26 @@ namespace Microsoft.Net.Http.Server
{ {
throw new ArgumentNullException("fileName"); throw new ArgumentNullException("fileName");
} }
if (_closed) CheckDisposed();
if (_buffer.TotalBytes > 0)
{ {
throw new ObjectDisposedException(GetType().FullName); // 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);
} }
internal unsafe Task SendFileAsyncCore(string fileName, long offset, long? count, CancellationToken cancellationToken)
{
_requestContext.Response.Start();
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite(); UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite();
if (size == 0 && _leftToWrite != 0) if (count == 0 && _leftToWrite != 0)
{ {
return Helpers.CompletedTask(); return Helpers.CompletedTask();
} }
if (_leftToWrite >= 0 && size > _leftToWrite) if (_leftToWrite >= 0 && count > _leftToWrite)
{ {
throw new InvalidOperationException(Resources.Exception_TooMuchWritten); throw new InvalidOperationException(Resources.Exception_TooMuchWritten);
} }
@ -653,30 +583,30 @@ namespace Microsoft.Net.Http.Server
uint statusCode; uint statusCode;
uint bytesSent = 0; uint bytesSent = 0;
flags |= _leftToWrite == size ? UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE : UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA; flags |= _leftToWrite == count ? HttpApi.HTTP_FLAGS.NONE : HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
bool sentHeaders = _requestContext.Response.HeadersSent; bool startedSending = _requestContext.Response.HasStartedSending;
ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, null, null, fileName, offset, size, var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
_requestContext.Response.BoundaryType == BoundaryType.Chunked, sentHeaders, cancellationRegistration); ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, fileName, offset, count, chunked, cancellationRegistration);
long bytesWritten; long bytesWritten;
if (_requestContext.Response.BoundaryType == BoundaryType.Chunked) if (chunked)
{ {
bytesWritten = 0; bytesWritten = 0;
} }
else if (size.HasValue) else if (count.HasValue)
{ {
bytesWritten = size.Value; bytesWritten = count.Value;
} }
else else
{ {
bytesWritten = asyncResult.FileLength - offset; bytesWritten = asyncResult.FileLength - offset;
} }
// Update m_LeftToWrite now so we can queue up additional calls to SendFileAsync. // Update _leftToWrite now so we can queue up additional calls to SendFileAsync.
UpdateWritenCount((uint)bytesWritten); UpdateWritenCount((uint)bytesWritten);
try try
{ {
if (!sentHeaders) if (!startedSending)
{ {
statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false); statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false);
bytesSent = asyncResult.BytesSent; bytesSent = asyncResult.BytesSent;
@ -684,8 +614,7 @@ namespace Microsoft.Net.Http.Server
else else
{ {
// TODO: If opaque then include the buffer data flag. // TODO: If opaque then include the buffer data flag.
statusCode = statusCode = HttpApi.HttpSendResponseEntityBody(
UnsafeNclNativeMethods.HttpApi.HttpSendResponseEntityBody(
_requestContext.RequestQueueHandle, _requestContext.RequestQueueHandle,
_requestContext.RequestId, _requestContext.RequestId,
(uint)flags, (uint)flags,
@ -709,7 +638,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 && sentHeaders) if (_requestContext.Server.IgnoreWriteExceptions && startedSending)
{ {
asyncResult.Complete(); asyncResult.Complete();
} }
@ -722,14 +651,14 @@ namespace Microsoft.Net.Http.Server
} }
} }
if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess) if (statusCode == ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess)
{ {
// IO operation completed synchronously - callback won't be called to signal completion. // IO operation completed synchronously - callback won't be called to signal completion.
asyncResult.IOCompleted(statusCode, bytesSent); asyncResult.IOCompleted(statusCode, bytesSent);
} }
// Last write, cache it for special cancellation handling. // Last write, cache it for special cancellation handling.
if ((flags & UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA) == 0) if ((flags & HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA) == 0)
{ {
_lastWrite = asyncResult; _lastWrite = asyncResult;
} }
@ -765,85 +694,7 @@ namespace Microsoft.Net.Http.Server
return; return;
} }
_closed = true; _closed = true;
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite(endOfRequest: true); FlushInternal(endOfRequest: true);
if (_leftToWrite > 0 && !_inOpaqueMode)
{
_requestContext.Abort();
// This is logged rather than thrown because it is too late for an exception to be visible in user code.
LogHelper.LogError(_requestContext.Logger, "ResponseStream::Dispose", "Fewer bytes were written than were specified in the Content-Length.");
return;
}
bool sentHeaders = _requestContext.Response.HeadersSent;
if (sentHeaders && _leftToWrite == 0)
{
return;
}
uint statusCode = 0;
if ((_requestContext.Response.BoundaryType == BoundaryType.Chunked
|| _requestContext.Response.BoundaryType == BoundaryType.Close
|| _requestContext.Response.BoundaryType == BoundaryType.PassThrough)
&& !_requestContext.Request.IsHeadMethod)
{
if (_requestContext.Response.BoundaryType == BoundaryType.Close)
{
flags |= UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT;
}
fixed (void* pBuffer = ChunkTerminator)
{
UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK* pDataChunk = null;
if (_requestContext.Response.BoundaryType == BoundaryType.Chunked)
{
UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK dataChunk = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
dataChunk.DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
dataChunk.fromMemory.pBuffer = (IntPtr)pBuffer;
dataChunk.fromMemory.BufferLength = (uint)ChunkTerminator.Length;
pDataChunk = &dataChunk;
}
if (!sentHeaders)
{
statusCode = _requestContext.Response.SendHeaders(pDataChunk, null, flags, false);
}
else
{
statusCode =
UnsafeNclNativeMethods.HttpApi.HttpSendResponseEntityBody(
_requestContext.RequestQueueHandle,
_requestContext.RequestId,
(uint)flags,
pDataChunk != null ? (ushort)1 : (ushort)0,
pDataChunk,
null,
SafeLocalFree.Zero,
0,
SafeNativeOverlapped.Zero,
IntPtr.Zero);
if (_requestContext.Server.IgnoreWriteExceptions)
{
statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS;
}
}
}
}
else
{
if (!sentHeaders)
{
statusCode = _requestContext.Response.SendHeaders(null, null, flags, false);
}
}
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF
// Don't throw for disconnects, we were already finished with the response.
&& statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_CONNECTION_INVALID
&& statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_INVALID_PARAMETER)
{
Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode));
LogHelper.LogException(_requestContext.Logger, "Dispose", exception);
_requestContext.Abort();
throw exception;
}
_leftToWrite = 0;
} }
} }
finally finally
@ -870,5 +721,13 @@ namespace Microsoft.Net.Http.Server
UnsafeNclNativeMethods.CancelIoEx(requestQueueHandle, asyncState.NativeOverlapped); UnsafeNclNativeMethods.CancelIoEx(requestQueueHandle, asyncState.NativeOverlapped);
} }
} }
private void CheckDisposed()
{
if (_closed)
{
throw new ObjectDisposedException(GetType().FullName);
}
}
} }
} }

View File

@ -27,117 +27,97 @@ using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using static Microsoft.Net.Http.Server.UnsafeNclNativeMethods;
namespace Microsoft.Net.Http.Server namespace Microsoft.Net.Http.Server
{ {
internal unsafe class ResponseStreamAsyncResult : IAsyncResult, IDisposable internal unsafe class ResponseStreamAsyncResult : IAsyncResult, IDisposable
{ {
private static readonly byte[] CRLF = new byte[] { (byte)'\r', (byte)'\n' };
private static readonly IOCompletionCallback IOCallback = new IOCompletionCallback(Callback); private static readonly IOCompletionCallback IOCallback = new IOCompletionCallback(Callback);
private SafeNativeOverlapped _overlapped; private SafeNativeOverlapped _overlapped;
private UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK[] _dataChunks; private HttpApi.HTTP_DATA_CHUNK[] _dataChunks;
private bool _sentHeaders;
private FileStream _fileStream; private FileStream _fileStream;
private ResponseStream _responseStream; private ResponseStream _responseStream;
private TaskCompletionSource<object> _tcs; private TaskCompletionSource<object> _tcs;
private AsyncCallback _callback;
private uint _bytesSent; private uint _bytesSent;
private CancellationTokenRegistration _cancellationRegistration; private CancellationTokenRegistration _cancellationRegistration;
internal ResponseStreamAsyncResult(ResponseStream responseStream, object userState, AsyncCallback callback) internal ResponseStreamAsyncResult(ResponseStream responseStream, CancellationTokenRegistration cancellationRegistration)
{ {
_responseStream = responseStream; _responseStream = responseStream;
_tcs = new TaskCompletionSource<object>(userState); _tcs = new TaskCompletionSource<object>();
_callback = callback;
}
internal ResponseStreamAsyncResult(ResponseStream responseStream, object userState, AsyncCallback callback,
byte[] buffer, int offset, int size, bool chunked, bool sentHeaders)
: this(responseStream, userState, callback, buffer, offset, size, chunked, sentHeaders,
new CancellationTokenRegistration())
{
}
internal ResponseStreamAsyncResult(ResponseStream responseStream, object userState, AsyncCallback callback,
byte[] buffer, int offset, int size, bool chunked, bool sentHeaders,
CancellationTokenRegistration cancellationRegistration)
: this(responseStream, userState, callback)
{
_sentHeaders = sentHeaders;
_cancellationRegistration = cancellationRegistration; _cancellationRegistration = cancellationRegistration;
var boundHandle = _responseStream.RequestContext.Server.BoundHandle; }
if (size == 0) internal ResponseStreamAsyncResult(ResponseStream responseStream, BufferBuilder buffer, bool chunked,
CancellationTokenRegistration cancellationRegistration)
: this(responseStream, cancellationRegistration)
{
var boundHandle = _responseStream.RequestContext.Server.BoundHandle;
object[] objectsToPin;
if (buffer.TotalBytes == 0)
{ {
_dataChunks = null; _dataChunks = null;
_overlapped = new SafeNativeOverlapped(boundHandle, _overlapped = new SafeNativeOverlapped(boundHandle,
boundHandle.AllocateNativeOverlapped(IOCallback, this, null)); boundHandle.AllocateNativeOverlapped(IOCallback, this, null));
return;
} }
else
{
_dataChunks = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK[chunked ? 3 : 1];
object[] objectsToPin = new object[1 + _dataChunks.Length]; _dataChunks = new HttpApi.HTTP_DATA_CHUNK[buffer.BufferCount + (chunked ? 2 : 0)];
objectsToPin[_dataChunks.Length] = _dataChunks; objectsToPin = new object[_dataChunks.Length + 1];
objectsToPin[0] = _dataChunks;
var currentChunk = 0;
var currentPin = 1;
int chunkHeaderOffset = 0; var chunkHeaderBuffer = new ArraySegment<byte>();
byte[] chunkHeaderBuffer = null;
if (chunked) if (chunked)
{ {
chunkHeaderBuffer = GetChunkHeader(size, out chunkHeaderOffset); chunkHeaderBuffer = Helpers.GetChunkHeader(buffer.TotalBytes);
SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, chunkHeaderBuffer);
_dataChunks[0] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
_dataChunks[0].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
_dataChunks[0].fromMemory.BufferLength = (uint)(chunkHeaderBuffer.Length - chunkHeaderOffset);
objectsToPin[0] = chunkHeaderBuffer;
_dataChunks[1] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
_dataChunks[1].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
_dataChunks[1].fromMemory.BufferLength = (uint)size;
objectsToPin[1] = buffer;
_dataChunks[2] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
_dataChunks[2].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
_dataChunks[2].fromMemory.BufferLength = (uint)CRLF.Length;
objectsToPin[2] = CRLF;
} }
else
{
_dataChunks[0] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
_dataChunks[0].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
_dataChunks[0].fromMemory.BufferLength = (uint)size;
objectsToPin[0] = buffer; foreach (var segment in buffer.Buffers)
{
SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, segment);
}
if (chunked)
{
SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, new ArraySegment<byte>(Helpers.CRLF));
} }
// This call will pin needed memory // This call will pin needed memory
_overlapped = new SafeNativeOverlapped(boundHandle, _overlapped = new SafeNativeOverlapped(boundHandle,
boundHandle.AllocateNativeOverlapped(IOCallback, this, objectsToPin)); boundHandle.AllocateNativeOverlapped(IOCallback, this, objectsToPin));
currentChunk = 0;
if (chunked) if (chunked)
{ {
_dataChunks[0].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer, chunkHeaderOffset); _dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer.Array, chunkHeaderBuffer.Offset);
_dataChunks[1].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset); currentChunk++;
_dataChunks[2].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(CRLF, 0);
} }
else foreach (var segment in buffer.Buffers)
{ {
_dataChunks[0].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset); _dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(segment.Array, segment.Offset);
} currentChunk++;
} }
if (chunked)
{
_dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(Helpers.CRLF, 0);
currentChunk++;
} }
internal ResponseStreamAsyncResult(ResponseStream responseStream, object userState, AsyncCallback callback, // We've captured a reference to all the buffers, clear the buffer so that it can be used to queue overlapped writes.
string fileName, long offset, long? size, bool chunked, bool sentHeaders, buffer.Clear();
CancellationTokenRegistration cancellationRegistration) }
: this(responseStream, userState, callback)
internal ResponseStreamAsyncResult(ResponseStream responseStream, string fileName, long offset,
long? count, bool chunked, CancellationTokenRegistration cancellationRegistration)
: this(responseStream, cancellationRegistration)
{ {
_sentHeaders = sentHeaders; var boundHandle = responseStream.RequestContext.Server.BoundHandle;
_cancellationRegistration = cancellationRegistration;
var boundHandle = ResponseStream.RequestContext.Server.BoundHandle;
int bufferSize = 1024 * 64; // TODO: Validate buffer size choice. int bufferSize = 1024 * 64; // TODO: Validate buffer size choice.
#if DNXCORE50 #if DNXCORE50
@ -153,13 +133,13 @@ namespace Microsoft.Net.Http.Server
_fileStream.Dispose(); _fileStream.Dispose();
throw new ArgumentOutOfRangeException("offset", offset, string.Empty); throw new ArgumentOutOfRangeException("offset", offset, string.Empty);
} }
if (size.HasValue && (size < 0 || size > length - offset)) if (count.HasValue && (count < 0 || count > length - offset))
{ {
_fileStream.Dispose(); _fileStream.Dispose();
throw new ArgumentOutOfRangeException("size", size, string.Empty); throw new ArgumentOutOfRangeException("count", count, string.Empty);
} }
if (size == 0 || (!size.HasValue && _fileStream.Length == 0)) if (count == 0 || (!count.HasValue && _fileStream.Length == 0))
{ {
_dataChunks = null; _dataChunks = null;
_overlapped = new SafeNativeOverlapped(boundHandle, _overlapped = new SafeNativeOverlapped(boundHandle,
@ -167,42 +147,34 @@ namespace Microsoft.Net.Http.Server
} }
else else
{ {
_dataChunks = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK[chunked ? 3 : 1]; _dataChunks = new HttpApi.HTTP_DATA_CHUNK[chunked ? 3 : 1];
object[] objectsToPin = new object[_dataChunks.Length]; object[] objectsToPin = new object[_dataChunks.Length];
objectsToPin[_dataChunks.Length - 1] = _dataChunks; objectsToPin[_dataChunks.Length - 1] = _dataChunks;
int chunkHeaderOffset = 0; var chunkHeaderBuffer = new ArraySegment<byte>();
byte[] chunkHeaderBuffer = null;
if (chunked) if (chunked)
{ {
chunkHeaderBuffer = GetChunkHeader((int)(size ?? _fileStream.Length - offset), out chunkHeaderOffset); chunkHeaderBuffer = Helpers.GetChunkHeader((int)(count ?? _fileStream.Length - offset));
_dataChunks[0].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
_dataChunks[0].fromMemory.BufferLength = (uint)chunkHeaderBuffer.Count;
objectsToPin[0] = chunkHeaderBuffer.Array;
_dataChunks[0] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK(); _dataChunks[1].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromFileHandle;
_dataChunks[0].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
_dataChunks[0].fromMemory.BufferLength = (uint)(chunkHeaderBuffer.Length - chunkHeaderOffset);
objectsToPin[0] = chunkHeaderBuffer;
_dataChunks[1] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
_dataChunks[1].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromFileHandle;
_dataChunks[1].fromFile.offset = (ulong)offset; _dataChunks[1].fromFile.offset = (ulong)offset;
_dataChunks[1].fromFile.count = (ulong)(size ?? -1); _dataChunks[1].fromFile.count = (ulong)(count ?? -1);
_dataChunks[1].fromFile.fileHandle = _fileStream.SafeFileHandle.DangerousGetHandle(); _dataChunks[1].fromFile.fileHandle = _fileStream.SafeFileHandle.DangerousGetHandle();
// Nothing to pin for the file handle. // Nothing to pin for the file handle.
_dataChunks[2] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK(); _dataChunks[2].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
_dataChunks[2].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory; _dataChunks[2].fromMemory.BufferLength = (uint)Helpers.CRLF.Length;
_dataChunks[2].fromMemory.BufferLength = (uint)CRLF.Length; objectsToPin[1] = Helpers.CRLF;
objectsToPin[1] = CRLF;
} }
else else
{ {
_dataChunks[0] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK(); _dataChunks[0].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromFileHandle;
_dataChunks[0].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromFileHandle;
_dataChunks[0].fromFile.offset = (ulong)offset; _dataChunks[0].fromFile.offset = (ulong)offset;
_dataChunks[0].fromFile.count = (ulong)(size ?? -1); _dataChunks[0].fromFile.count = (ulong)(count ?? -1);
_dataChunks[0].fromFile.fileHandle = _fileStream.SafeFileHandle.DangerousGetHandle(); _dataChunks[0].fromFile.fileHandle = _fileStream.SafeFileHandle.DangerousGetHandle();
} }
@ -212,15 +184,21 @@ namespace Microsoft.Net.Http.Server
if (chunked) if (chunked)
{ {
_dataChunks[0].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer, chunkHeaderOffset); // These must be set after pinning with Overlapped.
_dataChunks[2].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(CRLF, 0); _dataChunks[0].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer.Array, chunkHeaderBuffer.Offset);
_dataChunks[2].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(Helpers.CRLF, 0);
} }
} }
} }
internal ResponseStream ResponseStream private static void SetDataChunk(HttpApi.HTTP_DATA_CHUNK[] chunks, ref int chunkIndex, object[] objectsToPin, ref int pinIndex, ArraySegment<byte> segment)
{ {
get { return _responseStream; } objectsToPin[pinIndex] = segment.Array;
pinIndex++;
chunks[chunkIndex].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
// The address is not set until after we pin it with Overlapped
chunks[chunkIndex].fromMemory.BufferLength = (uint)segment.Count;
chunkIndex++;
} }
internal SafeNativeOverlapped NativeOverlapped internal SafeNativeOverlapped NativeOverlapped
@ -254,7 +232,7 @@ namespace Microsoft.Net.Http.Server
} }
} }
internal UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK* DataChunks internal HttpApi.HTTP_DATA_CHUNK* DataChunks
{ {
get get
{ {
@ -264,7 +242,7 @@ namespace Microsoft.Net.Http.Server
} }
else else
{ {
return (UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK*)(Marshal.UnsafeAddrOfPinnedArrayElement(_dataChunks, 0)); return (HttpApi.HTTP_DATA_CHUNK*)(Marshal.UnsafeAddrOfPinnedArrayElement(_dataChunks, 0));
} }
} }
} }
@ -326,111 +304,17 @@ namespace Microsoft.Net.Http.Server
internal void Complete() internal void Complete()
{ {
if (_tcs.TrySetResult(null) && _callback != null) _tcs.TrySetResult(null);
{
try
{
_callback(this);
}
catch (Exception)
{
// TODO: Exception handling? This may be an IO callback thread and throwing here could crash the app.
// TODO: Log
}
}
Dispose(); Dispose();
} }
internal void Fail(Exception ex) internal void Fail(Exception ex)
{ {
if (_tcs.TrySetException(ex) && _callback != null) _tcs.TrySetException(ex);
{
try
{
_callback(this);
}
catch (Exception)
{
// TODO: Exception handling? This may be an IO callback thread and throwing here could crash the app.
}
}
Dispose(); Dispose();
_responseStream.Abort(); _responseStream.Abort();
} }
/*++
GetChunkHeader
A private utility routine to convert an integer to a chunk header,
which is an ASCII hex number followed by a CRLF. The header is returned
as a byte array.
Input:
size - Chunk size to be encoded
offset - Out parameter where we store offset into buffer.
Returns:
A byte array with the header in int.
--*/
private static byte[] GetChunkHeader(int size, out int offset)
{
uint mask = 0xf0000000;
byte[] header = new byte[10];
int i;
offset = -1;
// Loop through the size, looking at each nibble. If it's not 0
// convert it to hex. Save the index of the first non-zero
// byte.
for (i = 0; i < 8; i++, size <<= 4)
{
// offset == -1 means that we haven't found a non-zero nibble
// yet. If we haven't found one, and the current one is zero,
// don't do anything.
if (offset == -1)
{
if ((size & mask) == 0)
{
continue;
}
}
// Either we have a non-zero nibble or we're no longer skipping
// leading zeros. Convert this nibble to ASCII and save it.
uint temp = (uint)size >> 28;
if (temp < 10)
{
header[i] = (byte)(temp + '0');
}
else
{
header[i] = (byte)((temp - 10) + 'A');
}
// If we haven't found a non-zero nibble yet, we've found one
// now, so remember that.
if (offset == -1)
{
offset = i;
}
}
header[8] = (byte)'\r';
header[9] = (byte)'\n';
return header;
}
public object AsyncState public object AsyncState
{ {
get { return _tcs.Task.AsyncState; } get { return _tcs.Task.AsyncState; }

View File

@ -91,6 +91,8 @@ 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)
{ {
@ -134,6 +136,12 @@ namespace Microsoft.Net.Http.Server
get { return _urlPrefixes; } get { return _urlPrefixes; }
} }
public bool BufferResponses
{
get { return _bufferResponses; }
set { _bufferResponses = value; }
}
internal SafeHandle RequestQueueHandle internal SafeHandle RequestQueueHandle
{ {
get get

View File

@ -13,5 +13,8 @@
<PropertyGroup> <PropertyGroup>
<SchemaVersion>2.0</SchemaVersion> <SchemaVersion>2.0</SchemaVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project> </Project>

View File

@ -71,6 +71,7 @@ namespace Microsoft.AspNet.Server.WebListener
{ {
var httpContext = new DefaultHttpContext((IFeatureCollection)env); var httpContext = new DefaultHttpContext((IFeatureCollection)env);
await httpContext.Response.WriteAsync("Hello World"); await httpContext.Response.WriteAsync("Hello World");
await httpContext.Response.Body.FlushAsync();
try try
{ {
var opaqueFeature = httpContext.GetFeature<IHttpUpgradeFeature>(); var opaqueFeature = httpContext.GetFeature<IHttpUpgradeFeature>();
@ -171,8 +172,8 @@ namespace Microsoft.AspNet.Server.WebListener
using (Stream stream = await SendOpaqueRequestAsync(method, address, extraHeader)) using (Stream stream = await SendOpaqueRequestAsync(method, address, extraHeader))
{ {
byte[] data = new byte[100]; byte[] data = new byte[100];
stream.WriteAsync(data, 0, 49).Wait(); await stream.WriteAsync(data, 0, 49);
int read = stream.ReadAsync(data, 0, data.Length).Result; int read = await stream.ReadAsync(data, 0, data.Length);
Assert.Equal(49, read); Assert.Equal(49, read);
} }
} }

View File

@ -20,6 +20,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.FeatureModel;
@ -31,7 +32,7 @@ namespace Microsoft.AspNet.Server.WebListener
public class ResponseBodyTests public class ResponseBodyTests
{ {
[Fact] [Fact]
public async Task ResponseBody_WriteNoHeaders_DefaultsToChunked() public async Task ResponseBody_WriteNoHeaders_BuffersAndSetsContentLength()
{ {
string address; string address;
using (Utilities.CreateHttpServer(out address, env => using (Utilities.CreateHttpServer(out address, env =>
@ -45,24 +46,22 @@ namespace Microsoft.AspNet.Server.WebListener
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.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync()); Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
} }
} }
[Fact] [Fact]
public async Task ResponseBody_WriteChunked_Chunked() public async Task ResponseBody_WriteNoHeadersAndFlush_DefaultsToChunked()
{ {
string address; string address;
using (Utilities.CreateHttpServer(out address, env => using (Utilities.CreateHttpServer(out address, async env =>
{ {
var httpContext = new DefaultHttpContext((IFeatureCollection)env); var httpContext = new DefaultHttpContext((IFeatureCollection)env);
httpContext.Request.Headers["transfeR-Encoding"] = " CHunked "; httpContext.Response.Body.Write(new byte[10], 0, 10);
Stream stream = httpContext.Response.Body; await httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
stream.EndWrite(stream.BeginWrite(new byte[10], 0, 10, null, null)); await httpContext.Response.Body.FlushAsync();
stream.Write(new byte[10], 0, 10);
return stream.WriteAsync(new byte[10], 0, 10);
})) }))
{ {
HttpResponseMessage response = await SendRequestAsync(address); HttpResponseMessage response = await SendRequestAsync(address);
@ -71,7 +70,30 @@ namespace Microsoft.AspNet.Server.WebListener
IEnumerable<string> ignored; IEnumerable<string> ignored;
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
Assert.Equal(new byte[30], await response.Content.ReadAsByteArrayAsync()); Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
}
}
[Fact]
public async Task ResponseBody_WriteChunked_ManuallyChunked()
{
string address;
using (Utilities.CreateHttpServer(out address, async env =>
{
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
httpContext.Response.Headers["transfeR-Encoding"] = " CHunked ";
Stream stream = httpContext.Response.Body;
var responseBytes = Encoding.ASCII.GetBytes("10\r\nManually Chunked\r\n0\r\n\r\n");
await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
}))
{
HttpResponseMessage response = await SendRequestAsync(address);
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("Manually Chunked", await response.Content.ReadAsStringAsync());
} }
} }
@ -132,7 +154,7 @@ namespace Microsoft.AspNet.Server.WebListener
} }
[Fact] [Fact]
public void ResponseBody_WriteContentLengthTooMuchWritten_Throws() public async Task ResponseBody_WriteContentLengthTooMuchWritten_Throws()
{ {
string address; string address;
using (Utilities.CreateHttpServer(out address, env => using (Utilities.CreateHttpServer(out address, env =>
@ -144,7 +166,8 @@ namespace Microsoft.AspNet.Server.WebListener
return Task.FromResult(0); return Task.FromResult(0);
})) }))
{ {
Assert.Throws<AggregateException>(() => SendRequestAsync(address).Result); var response = await SendRequestAsync(address);
Assert.Equal(500, (int)response.StatusCode);
} }
} }

View File

@ -84,7 +84,6 @@ namespace Microsoft.AspNet.Server.WebListener
{ {
HttpResponseMessage response = await SendRequestAsync(address); HttpResponseMessage response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode); Assert.Equal(200, (int)response.StatusCode);
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
Assert.True(upgradeThrew.Value); Assert.True(upgradeThrew.Value);
} }
} }

View File

@ -31,7 +31,6 @@ namespace Microsoft.Net.Http.Server
context.Dispose(); context.Dispose();
HttpResponseMessage response = await clientTask; HttpResponseMessage response = await clientTask;
Assert.Equal(200, (int)response.StatusCode); Assert.Equal(200, (int)response.StatusCode);
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
Assert.Equal("Hello World", await response.Content.ReadAsStringAsync()); Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());
} }
} }

View File

@ -15,7 +15,7 @@ namespace Microsoft.Net.Http.Server
public class ResponseBodyTests public class ResponseBodyTests
{ {
[Fact] [Fact]
public async Task ResponseBody_WriteNoHeaders_DefaultsToChunked() public async Task ResponseBody_BufferWriteNoHeaders_DefaultsToContentLength()
{ {
string address; string address;
using (var server = Utilities.CreateHttpServer(out address)) using (var server = Utilities.CreateHttpServer(out address))
@ -23,6 +23,31 @@ namespace Microsoft.Net.Http.Server
Task<HttpResponseMessage> responseTask = SendRequestAsync(address); Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync(); var context = await server.GetContextAsync();
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.GetContextAsync();
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();
@ -37,6 +62,29 @@ namespace Microsoft.Net.Http.Server
} }
} }
[Fact]
public async Task ResponseBody_FlushThenBuffer_DefaultsToChunkedAndTerminates()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Body.Write(new byte[10], 0, 10);
context.Response.Body.Flush();
await context.Response.Body.WriteAsync(new byte[10], 0, 10);
context.Dispose();
HttpResponseMessage response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> contentLength;
Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
Assert.True(response.Headers.TransferEncodingChunked.HasValue);
Assert.Equal(20, (await response.Content.ReadAsByteArrayAsync()).Length);
}
}
[Fact] [Fact]
public async Task ResponseBody_WriteChunked_ManuallyChunked() public async Task ResponseBody_WriteChunked_ManuallyChunked()
{ {
@ -111,7 +159,7 @@ namespace Microsoft.Net.Http.Server
} }
[Fact] [Fact]
public async Task ResponseBody_WriteContentLengthNotEnoughWritten_Throws() public async Task ResponseBody_WriteContentLengthNotEnoughWritten_Aborts()
{ {
string address; string address;
using (var server = Utilities.CreateHttpServer(out address)) using (var server = Utilities.CreateHttpServer(out address))
@ -123,6 +171,12 @@ namespace Microsoft.Net.Http.Server
context.Response.Body.Write(new byte[5], 0, 5); context.Response.Body.Write(new byte[5], 0, 5);
context.Dispose(); context.Dispose();
// HttpClient retries the request because it didn't get a response.
context = await server.GetContextAsync();
context.Response.Headers["Content-lenGth"] = " 20 ";
context.Response.Body.Write(new byte[5], 0, 5);
context.Dispose();
await Assert.ThrowsAsync<HttpRequestException>(() => responseTask); await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
} }
} }
@ -141,6 +195,13 @@ namespace Microsoft.Net.Http.Server
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();
// HttpClient retries the request because it didn't get a response.
context = await server.GetContextAsync();
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();
await Assert.ThrowsAsync<HttpRequestException>(() => responseTask); await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
} }
} }
@ -170,6 +231,92 @@ namespace Microsoft.Net.Http.Server
} }
} }
[Fact]
public async Task ResponseBody_WriteZeroCount_StartsResponse()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.Body.Write(new byte[10], 0, 0);
Assert.True(context.Response.HasStarted);
await context.Response.Body.WriteAsync(new byte[10], 0, 0);
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[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.GetContextAsync();
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.GetContextAsync();
context.Response.ShouldBuffer = true;
for (int i = 0; i < 4; i++)
{
await context.Response.Body.WriteAsync(new byte[1020], 0, 1020);
Assert.True(context.Response.HasStarted);
Assert.False(context.Response.HasStartedSending);
}
await context.Response.Body.WriteAsync(new byte[1020], 0, 1020);
Assert.True(context.Response.HasStartedSending);
await context.Response.Body.WriteAsync(new byte[1020], 0, 1020);
context.Dispose();
HttpResponseMessage response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal(new Version(1, 1), response.Version);
IEnumerable<string> ignored;
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
Assert.Equal(new byte[1020 * 6], await response.Content.ReadAsByteArrayAsync());
}
}
[Fact] [Fact]
public async Task ResponseBody_WriteAsyncWithActiveCancellationToken_Success() public async Task ResponseBody_WriteAsyncWithActiveCancellationToken_Success()
{ {

View File

@ -349,13 +349,13 @@ namespace Microsoft.Net.Http.Server
responseHeaders.SetValues("Custom1", "value1a", "value1b"); responseHeaders.SetValues("Custom1", "value1a", "value1b");
responseHeaders.SetValues("Custom2", "value2a, value2b"); responseHeaders.SetValues("Custom2", "value2a, value2b");
var body = context.Response.Body; var body = context.Response.Body;
Assert.False(context.Response.HeadersSent); Assert.False(context.Response.HasStarted);
body.Flush(); body.Flush();
Assert.True(context.Response.HeadersSent); Assert.True(context.Response.HasStarted);
var ex = Assert.Throws<InvalidOperationException>(() => context.Response.StatusCode = 404); var ex = Assert.Throws<InvalidOperationException>(() => context.Response.StatusCode = 404);
Assert.Equal("Headers already sent.", ex.Message); Assert.Equal("Headers already sent.", ex.Message);
ex = Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" })); ex = Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
Assert.Equal("The response headers cannot be modified because they have already been sent.", ex.Message); Assert.Equal("The response headers cannot be modified because the response has already started.", ex.Message);
context.Dispose(); context.Dispose();
@ -385,13 +385,13 @@ namespace Microsoft.Net.Http.Server
responseHeaders.SetValues("Custom1", "value1a", "value1b"); responseHeaders.SetValues("Custom1", "value1a", "value1b");
responseHeaders.SetValues("Custom2", "value2a, value2b"); responseHeaders.SetValues("Custom2", "value2a, value2b");
var body = context.Response.Body; var body = context.Response.Body;
Assert.False(context.Response.HeadersSent); Assert.False(context.Response.HasStarted);
await body.FlushAsync(); await body.FlushAsync();
Assert.True(context.Response.HeadersSent); Assert.True(context.Response.HasStarted);
var ex = Assert.Throws<InvalidOperationException>(() => context.Response.StatusCode = 404); var ex = Assert.Throws<InvalidOperationException>(() => context.Response.StatusCode = 404);
Assert.Equal("Headers already sent.", ex.Message); Assert.Equal("Headers already sent.", ex.Message);
ex = Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" })); ex = Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
Assert.Equal("The response headers cannot be modified because they have already been sent.", ex.Message); Assert.Equal("The response headers cannot be modified because the response has already started.", ex.Message);
context.Dispose(); context.Dispose();

View File

@ -202,6 +202,34 @@ namespace Microsoft.Net.Http.Server
} }
} }
[Fact]
public async Task ResponseSendFile_EmptyFileCountUnspecified_SetsChunkedAndFlushesHeaders()
{
var emptyFilePath = Path.Combine(Environment.CurrentDirectory, "zz_" + Guid.NewGuid().ToString() + "EmptyTestFile.txt");
var emptyFile = File.Create(emptyFilePath, 1024);
emptyFile.Close();
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
await context.Response.SendFileAsync(emptyFilePath, 0, null, CancellationToken.None);
Assert.True(context.Response.HasStartedSending);
await context.Response.Body.WriteAsync(new byte[10], 0, 10, CancellationToken.None);
context.Dispose();
File.Delete(emptyFilePath);
HttpResponseMessage response = await responseTask;
Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> contentLength;
Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
Assert.True(response.Headers.TransferEncodingChunked.HasValue);
Assert.Equal(10, (await response.Content.ReadAsByteArrayAsync()).Length);
}
}
[Fact] [Fact]
public async Task ResponseSendFile_ContentLength_PassedThrough() public async Task ResponseSendFile_ContentLength_PassedThrough()
{ {

View File

@ -30,7 +30,6 @@ namespace Microsoft.Net.Http.Server
context.Dispose(); context.Dispose();
HttpResponseMessage response = await clientTask; HttpResponseMessage response = await clientTask;
Assert.Equal(200, (int)response.StatusCode); Assert.Equal(200, (int)response.StatusCode);
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
Assert.Equal("Hello World", await response.Content.ReadAsStringAsync()); Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());
} }
} }