#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()
{
if (HasStarted)
{ {
return _responseState >= ResponseState.SentHeaders; 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,45 +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)];
fixed (byte* pReasonPhrase = reasonPhraseBytes)
{ {
byte[] reasonPhraseBytes = new byte[HeaderEncoding.GetByteCount(reasonPhrase)]; _nativeResponse.Response_V1.ReasonLength = (ushort)reasonPhraseBytes.Length;
fixed (byte* pReasonPhrase = reasonPhraseBytes) HeaderEncoding.GetBytes(reasonPhrase, 0, reasonPhraseBytes.Length, reasonPhraseBytes, 0);
{ _nativeResponse.Response_V1.pReason = (sbyte*)pReasonPhrase;
_nativeResponse.Response_V1.ReasonLength = (ushort)reasonPhraseBytes.Length; fixed (HttpApi.HTTP_RESPONSE_V2* pResponse = &_nativeResponse)
HeaderEncoding.GetBytes(reasonPhrase, 0, reasonPhraseBytes.Length, reasonPhraseBytes, 0);
_nativeResponse.Response_V1.pReason = (sbyte*)pReasonPhrase;
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)
{
asyncResult.BytesSent = bytesSent;
// The caller will invoke IOCompleted
}
}
}
}
else
{
fixed (UnsafeNclNativeMethods.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,
@ -411,7 +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) 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()
{ {
// Notify that this is absolutely the last chance to make changes. if (!HasStarted)
NotifyOnSendingHeaders(); {
// Notify that this is absolutely the last chance to make changes.
NotifyOnSendingHeaders();
Headers.IsReadOnly = true; // Prohibit further modifications.
_responseState = ResponseState.Started;
}
}
internal HttpApi.HTTP_FLAGS ComputeHeaders(bool endOfRequest = false, int bufferedBytes = 0)
{
Headers.IsReadOnly = false; // Temporarily allow modification.
// 401 // 401
if (StatusCode == (ushort)HttpStatusCode.Unauthorized) if (StatusCode == (ushort)HttpStatusCode.Unauthorized)
@ -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
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF)
{ {
throw new IOException(string.Empty, new WebListenerException((int)statusCode)); 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 (_requestContext.Server.IgnoreWriteExceptions)
{
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);
}
else
{
fixed (byte* pDataBuffer = buffer)
{
byte* pBuffer = pDataBuffer;
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;
}
}
}
} }
// or the last write in a response that hasn't started yet, flush immideately
_buffer.Add(data);
Flush();
} }
finally // The last write in a response that has already started, flush immidately
else if (_requestContext.Response.ComputedHeaders && _leftToWrite >= 0 && _leftToWrite <= currentBytes)
{ {
if (bufferAsIntPtr != null) if (_leftToWrite < currentBytes)
{ {
// free unmanaged buffer throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
bufferAsIntPtr.Dispose();
} }
_buffer.Add(data);
Flush();
} }
else if (_requestContext.Response.ShouldBuffer && currentBytes < MaxBufferSize)
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF)
{ {
Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode)); _buffer.CopyAndAdd(data);
LogHelper.LogException(_requestContext.Logger, "Write", exception); }
Abort(); else
throw exception; {
// Append to existing data without a copy, and then flush immidately
_buffer.Add(data);
Flush();
} }
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)
}
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, null, null, buffer, offset, size, _requestContext.Response.BoundaryType == BoundaryType.Chunked, sentHeaders, cancellationRegistration);
// 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); throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
bytesSent = asyncResult.BytesSent;
} }
else // The last write in a response that hasn't started yet, flush immideately
_buffer.Add(data);
return FlushAsync(cancellationToken);
}
// The last write in a response that has already started, flush immidately
else if (_requestContext.Response.ComputedHeaders && _leftToWrite > 0 && _leftToWrite <= currentBytes)
{
if (_leftToWrite < currentBytes)
{ {
statusCode = throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
UnsafeNclNativeMethods.HttpApi.HttpSendResponseEntityBody(
_requestContext.RequestQueueHandle,
_requestContext.RequestId,
(uint)flags,
asyncResult.DataChunkCount,
asyncResult.DataChunks,
&bytesSent,
SafeLocalFree.Zero,
0,
asyncResult.NativeOverlapped,
IntPtr.Zero);
} }
_buffer.Add(data);
return FlushAsync(cancellationToken);
} }
catch (Exception e) else if (_requestContext.Response.ShouldBuffer && currentBytes < MaxBufferSize)
{ {
LogHelper.LogException(_requestContext.Logger, "WriteAsync", e); _buffer.CopyAndAdd(data);
asyncResult.Dispose(); return Helpers.CompletedTask();
Abort();
throw;
} }
else
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
{ {
asyncResult.Dispose(); // Append to existing data without a copy, and then flush immidately
if (_requestContext.Server.IgnoreWriteExceptions && sentHeaders) _buffer.Add(data);
{ return FlushAsync(cancellationToken);
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)
{
// 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) internal async Task SendFileAsync(string fileName, long offset, long? count, 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 HttpApi.HTTP_DATA_CHUNK[buffer.BufferCount + (chunked ? 2 : 0)];
objectsToPin = new object[_dataChunks.Length + 1];
objectsToPin[0] = _dataChunks;
var currentChunk = 0;
var currentPin = 1;
var chunkHeaderBuffer = new ArraySegment<byte>();
if (chunked)
{ {
_dataChunks = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK[chunked ? 3 : 1]; chunkHeaderBuffer = Helpers.GetChunkHeader(buffer.TotalBytes);
SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, chunkHeaderBuffer);
object[] objectsToPin = new object[1 + _dataChunks.Length];
objectsToPin[_dataChunks.Length] = _dataChunks;
int chunkHeaderOffset = 0;
byte[] chunkHeaderBuffer = null;
if (chunked)
{
chunkHeaderBuffer = GetChunkHeader(size, out chunkHeaderOffset);
_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;
}
// This call will pin needed memory
_overlapped = new SafeNativeOverlapped(boundHandle,
boundHandle.AllocateNativeOverlapped(IOCallback, this, objectsToPin));
if (chunked)
{
_dataChunks[0].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer, chunkHeaderOffset);
_dataChunks[1].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset);
_dataChunks[2].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(CRLF, 0);
}
else
{
_dataChunks[0].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset);
}
} }
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
_overlapped = new SafeNativeOverlapped(boundHandle,
boundHandle.AllocateNativeOverlapped(IOCallback, this, objectsToPin));
currentChunk = 0;
if (chunked)
{
_dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer.Array, chunkHeaderBuffer.Offset);
currentChunk++;
}
foreach (var segment in buffer.Buffers)
{
_dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(segment.Array, segment.Offset);
currentChunk++;
}
if (chunked)
{
_dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(Helpers.CRLF, 0);
currentChunk++;
}
// We've captured a reference to all the buffers, clear the buffer so that it can be used to queue overlapped writes.
buffer.Clear();
} }
internal ResponseStreamAsyncResult(ResponseStream responseStream, object userState, AsyncCallback callback, internal ResponseStreamAsyncResult(ResponseStream responseStream, string fileName, long offset,
string fileName, long offset, long? size, bool chunked, bool sentHeaders, long? count, bool chunked, CancellationTokenRegistration cancellationRegistration)
CancellationTokenRegistration cancellationRegistration) : this(responseStream, cancellationRegistration)
: this(responseStream, userState, callback)
{ {
_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());
} }
} }