diff --git a/src/Microsoft.AspNet.Server.WebListener/FeatureContext.cs b/src/Microsoft.AspNet.Server.WebListener/FeatureContext.cs index 18a0c9c9c9..10401145c6 100644 --- a/src/Microsoft.AspNet.Server.WebListener/FeatureContext.cs +++ b/src/Microsoft.AspNet.Server.WebListener/FeatureContext.cs @@ -39,6 +39,7 @@ namespace Microsoft.AspNet.Server.WebListener IHttpSendFileFeature, ITlsConnectionFeature, ITlsTokenBindingFeature, + IHttpBufferingFeature, IHttpRequestLifetimeFeature, IHttpWebSocketFeature, IHttpAuthenticationFeature, @@ -97,6 +98,7 @@ namespace Microsoft.AspNet.Server.WebListener _features.Add(typeof(IHttpConnectionFeature), this); _features.Add(typeof(IHttpResponseFeature), this); _features.Add(typeof(IHttpSendFileFeature), this); + _features.Add(typeof(IHttpBufferingFeature), this); _features.Add(typeof(IHttpRequestLifetimeFeature), this); _features.Add(typeof(IHttpAuthenticationFeature), this); _features.Add(typeof(IRequestIdentifierFeature), this); @@ -328,6 +330,16 @@ namespace Microsoft.AspNet.Server.WebListener return Request.GetReferredTokenBindingId(); } + void IHttpBufferingFeature.DisableRequestBuffering() + { + // There is no request buffering. + } + + void IHttpBufferingFeature.DisableResponseBuffering() + { + Response.ShouldBuffer = false; + } + Stream IHttpResponseFeature.Body { get @@ -356,7 +368,7 @@ namespace Microsoft.AspNet.Server.WebListener bool IHttpResponseFeature.HeadersSent { - get { return Response.HeadersSent; } + get { return Response.HasStarted; } } void IHttpResponseFeature.OnSendingHeaders(Action callback, object state) diff --git a/src/Microsoft.AspNet.Server.WebListener/MessagePump.cs b/src/Microsoft.AspNet.Server.WebListener/MessagePump.cs index a43d893307..7bfdaead54 100644 --- a/src/Microsoft.AspNet.Server.WebListener/MessagePump.cs +++ b/src/Microsoft.AspNet.Server.WebListener/MessagePump.cs @@ -167,13 +167,14 @@ namespace Microsoft.AspNet.Server.WebListener catch (Exception ex) { LogHelper.LogException(_logger, "ProcessRequestAsync", ex); - if (requestContext.Response.HeadersSent) + if (requestContext.Response.HasStartedSending) { requestContext.Abort(); } else { // We haven't sent a response yet, try to send a 500 Internal Server Error + requestContext.Response.Reset(); SetFatalResponse(requestContext, 500); } } @@ -195,8 +196,6 @@ namespace Microsoft.AspNet.Server.WebListener private static void SetFatalResponse(RequestContext context, int status) { context.Response.StatusCode = status; - context.Response.ReasonPhrase = string.Empty; - context.Response.Headers.Clear(); context.Response.ContentLength = 0; context.Dispose(); } diff --git a/src/Microsoft.Net.Http.Server/Helpers.cs b/src/Microsoft.Net.Http.Server/Helpers.cs index 0d89af2dde..428edffa1d 100644 --- a/src/Microsoft.Net.Http.Server/Helpers.cs +++ b/src/Microsoft.Net.Http.Server/Helpers.cs @@ -21,13 +21,18 @@ // // ----------------------------------------------------------------------- +using System; using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.Net.Http.Server { 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() { return Task.FromResult(null); @@ -49,5 +54,94 @@ namespace Microsoft.Net.Http.Server { return task.ConfigureAwait(continueOnCapturedContext: false); } + + internal static IAsyncResult ToIAsyncResult(this Task task, AsyncCallback callback, object state) + { + var tcs = new TaskCompletionSource(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; + } + + /// + /// 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. + /// + /// Chunk size to be encoded + /// Out parameter where we store offset into buffer. + /// A byte array with the header in int. + internal static ArraySegment 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(header, offset, header.Length - offset); + } } } diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/BufferBuilder.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/BufferBuilder.cs new file mode 100644 index 0000000000..5cb08d271d --- /dev/null +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/BufferBuilder.cs @@ -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> _buffers = new List>(); + + internal IEnumerable> Buffers + { + get { return _buffers; } + } + + internal int BufferCount + { + get { return _buffers.Count; } + } + + internal int TotalBytes { get; private set; } + + internal void Add(ArraySegment data) + { + _buffers.Add(data); + TotalBytes += data.Count; + } + + public void CopyAndAdd(ArraySegment 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(temp)); + TotalBytes += data.Count; + } + } + + public void Clear() + { + _buffers.Clear(); + TotalBytes = 0; + } + } +} diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/HeaderCollection.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/HeaderCollection.cs index 4699dbd1e1..87ee134733 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/HeaderCollection.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/HeaderCollection.cs @@ -21,15 +21,15 @@ namespace Microsoft.Net.Http.Server private IDictionary Store { get; set; } - // Readonly after the response has been sent. - internal bool Sent { get; set; } + // Readonly after the response has been started. + public bool IsReadOnly { get; internal set; } public string this[string key] { get { return Get(key); } set { - ThrowIfSent(); + ThrowIfReadOnly(); if (string.IsNullOrEmpty(value)) { Remove(key); @@ -46,7 +46,7 @@ namespace Microsoft.Net.Http.Server get { return Store[key]; } set { - ThrowIfSent(); + ThrowIfReadOnly(); Store[key] = value; } } @@ -56,11 +56,6 @@ namespace Microsoft.Net.Http.Server get { return Store.Count; } } - public bool IsReadOnly - { - get { return Sent; } - } - public ICollection Keys { get { return Store.Keys; } @@ -73,19 +68,19 @@ namespace Microsoft.Net.Http.Server public void Add(KeyValuePair item) { - ThrowIfSent(); + ThrowIfReadOnly(); Store.Add(item); } public void Add(string key, string[] value) { - ThrowIfSent(); + ThrowIfReadOnly(); Store.Add(key, value); } public void Append(string key, string value) { - ThrowIfSent(); + ThrowIfReadOnly(); string[] values; if (Store.TryGetValue(key, out values)) { @@ -102,7 +97,7 @@ namespace Microsoft.Net.Http.Server public void AppendValues(string key, params string[] values) { - ThrowIfSent(); + ThrowIfReadOnly(); string[] oldValues; if (Store.TryGetValue(key, out oldValues)) { @@ -119,7 +114,7 @@ namespace Microsoft.Net.Http.Server public void Clear() { - ThrowIfSent(); + ThrowIfReadOnly(); Store.Clear(); } @@ -165,25 +160,25 @@ namespace Microsoft.Net.Http.Server public bool Remove(KeyValuePair item) { - ThrowIfSent(); + ThrowIfReadOnly(); return Store.Remove(item); } public bool Remove(string key) { - ThrowIfSent(); + ThrowIfReadOnly(); return Store.Remove(key); } public void Set(string key, string value) { - ThrowIfSent(); + ThrowIfReadOnly(); Store[key] = new[] { value }; } public void SetValues(string key, params string[] values) { - ThrowIfSent(); + ThrowIfReadOnly(); Store[key] = values; } @@ -197,11 +192,11 @@ namespace Microsoft.Net.Http.Server 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."); } } } diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/RequestContext.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/RequestContext.cs index b2232554ca..757cb4e7de 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/RequestContext.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/RequestContext.cs @@ -158,9 +158,9 @@ namespace Microsoft.Net.Http.Server public Task 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 diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs index d0cf0c98a7..e33a031e5f 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs @@ -33,6 +33,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Framework.Logging; +using static Microsoft.Net.Http.Server.UnsafeNclNativeMethods; namespace Microsoft.Net.Http.Server { @@ -46,18 +47,34 @@ namespace Microsoft.Net.Http.Server private ResponseStream _nativeStream; private long _expectedBodyLength; private BoundaryType _boundaryType; - private UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2 _nativeResponse; + private HttpApi.HTTP_RESPONSE_V2 _nativeResponse; private IList, object>> _onSendingHeadersActions; private IList, object>> _onResponseCompletedActions; private RequestContext _requestContext; + private bool _bufferingEnabled; - internal Response(RequestContext httpContext) + internal Response(RequestContext requestContext) { // TODO: Verbose log - _requestContext = httpContext; - _nativeResponse = new UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2(); + _requestContext = requestContext; _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; _nativeResponse.Response_V1.StatusCode = (ushort)HttpStatusCode.OK; _nativeResponse.Response_V1.Version.MajorVersion = 1; @@ -65,13 +82,17 @@ namespace Microsoft.Net.Http.Server _responseState = ResponseState.Created; _onSendingHeadersActions = new List, object>>(); _onResponseCompletedActions = new List, object>>(); + _bufferingEnabled = _requestContext.Server.BufferResponses; + _expectedBodyLength = 0; + _nativeStream = null; } private enum ResponseState { Created, + Started, ComputedHeaders, - SentHeaders, + StartedSending, 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 { 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 { get @@ -168,10 +191,7 @@ namespace Microsoft.Net.Http.Server internal long ExpectedBodyLength { - get - { - return _expectedBodyLength; - } + get { return _expectedBodyLength; } } // Header accessors @@ -248,6 +268,7 @@ namespace Microsoft.Net.Http.Server { return; } + Start(); NotifyOnResponseCompleted(); // TODO: Verbose log EnsureResponseStream(); @@ -259,26 +280,30 @@ namespace Microsoft.Net.Http.Server internal BoundaryType BoundaryType { - get - { - return _boundaryType; - } + get { 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 { - get - { - return _responseState >= ResponseState.ComputedHeaders; - } + get { return _responseState >= ResponseState.ComputedHeaders; } + } + + public bool HasStartedSending + { + get { return _responseState >= ResponseState.StartedSending; } } private void EnsureResponseStream() @@ -319,14 +344,14 @@ namespace Microsoft.Net.Http.Server // 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. - internal unsafe uint SendHeaders(UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK* pDataChunk, + internal unsafe uint SendHeaders(HttpApi.HTTP_DATA_CHUNK[] dataChunks, ResponseStreamAsyncResult asyncResult, - UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags, + HttpApi.HTTP_FLAGS flags, 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); if (RequestContext.Logger.IsEnabled(LogLevel.Verbose)) @@ -344,10 +369,16 @@ namespace Microsoft.Net.Http.Server List pinnedHeaders = SerializeHeaders(isOpaqueUpgrade); try { - if (pDataChunk != null) + if (dataChunks != null) { - _nativeResponse.Response_V1.EntityChunkCount = 1; - _nativeResponse.Response_V1.pEntityChunks = pDataChunk; + if (pinnedHeaders == null) + { + pinnedHeaders = new List(); + } + 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) { @@ -360,45 +391,16 @@ namespace Microsoft.Net.Http.Server _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)]; - fixed (byte* pReasonPhrase = reasonPhraseBytes) - { - _nativeResponse.Response_V1.ReasonLength = (ushort)reasonPhraseBytes.Length; - 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) + _nativeResponse.Response_V1.ReasonLength = (ushort)reasonPhraseBytes.Length; + HeaderEncoding.GetBytes(reasonPhrase, 0, reasonPhraseBytes.Length, reasonPhraseBytes, 0); + _nativeResponse.Response_V1.pReason = (sbyte*)pReasonPhrase; + fixed (HttpApi.HTTP_RESPONSE_V2* pResponse = &_nativeResponse) { statusCode = - UnsafeNclNativeMethods.HttpApi.HttpSendHttpResponse( + HttpApi.HttpSendHttpResponse( RequestContext.RequestQueueHandle, Request.RequestId, (uint)flags, @@ -411,7 +413,7 @@ namespace Microsoft.Net.Http.Server IntPtr.Zero); if (asyncResult != null && - statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && + statusCode == ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess) { asyncResult.BytesSent = bytesSent; @@ -427,10 +429,20 @@ namespace Microsoft.Net.Http.Server 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. - NotifyOnSendingHeaders(); + if (!HasStarted) + { + // Notify that this is absolutely the last chance to make changes. + NotifyOnSendingHeaders(); + Headers.IsReadOnly = true; // Prohibit further modifications. + _responseState = ResponseState.Started; + } + } + + internal HttpApi.HTTP_FLAGS ComputeHeaders(bool endOfRequest = false, int bufferedBytes = 0) + { + Headers.IsReadOnly = false; // Temporarily allow modification. // 401 if (StatusCode == (ushort)HttpStatusCode.Unauthorized) @@ -438,7 +450,7 @@ namespace Microsoft.Net.Http.Server 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."); _responseState = ResponseState.ComputedHeaders; @@ -478,14 +490,18 @@ namespace Microsoft.Net.Http.Server // The application is performing it's own chunking. _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; } _boundaryType = BoundaryType.ContentLength; - _expectedBodyLength = 0; + _expectedBodyLength = bufferedBytes; } else if (keepConnectionAlive && requestVersion == Constants.V1_1) { @@ -508,9 +524,10 @@ namespace Microsoft.Net.Http.Server { 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; } @@ -521,9 +538,9 @@ namespace Microsoft.Net.Http.Server private List SerializeHeaders(bool isOpaqueUpgrade) { - Headers.Sent = true; // Prohibit further modifications. - UnsafeNclNativeMethods.HttpApi.HTTP_UNKNOWN_HEADER[] unknownHeaders = null; - UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO[] knownHeaderInfo = null; + Headers.IsReadOnly = true; // Prohibit further modifications. + HttpApi.HTTP_UNKNOWN_HEADER[] unknownHeaders = null; + HttpApi.HTTP_RESPONSE_INFO[] knownHeaderInfo = null; List pinnedHeaders; GCHandle gcHandle; /* @@ -552,11 +569,11 @@ namespace Microsoft.Net.Http.Server continue; } // 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. 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; } @@ -569,7 +586,7 @@ namespace Microsoft.Net.Http.Server 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 headerPair in Headers) { @@ -580,18 +597,18 @@ namespace Microsoft.Net.Http.Server } headerName = headerPair.Key; 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. 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) { - unknownHeaders = new UnsafeNclNativeMethods.HttpApi.HTTP_UNKNOWN_HEADER[numUnknownHeaders]; + unknownHeaders = new HttpApi.HTTP_UNKNOWN_HEADER[numUnknownHeaders]; gcHandle = GCHandle.Alloc(unknownHeaders, GCHandleType.Pinned); 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++) @@ -632,29 +649,29 @@ namespace Microsoft.Net.Http.Server { if (knownHeaderInfo == null) { - knownHeaderInfo = new UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO[numKnownMultiHeaders]; + knownHeaderInfo = new HttpApi.HTTP_RESPONSE_INFO[numKnownMultiHeaders]; gcHandle = GCHandle.Alloc(knownHeaderInfo, GCHandleType.Pinned); 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 = #if DNXCORE50 - (uint)Marshal.SizeOf(); + (uint)Marshal.SizeOf(); #else - (uint)Marshal.SizeOf(typeof(UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS)); + (uint)Marshal.SizeOf(typeof(HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS)); #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.Flags = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO_FLAGS.PreserveOrder; // TODO: The docs say this is for www-auth only. + header.HeaderId = (HttpApi.HTTP_RESPONSE_HEADER_ID.Enum)lookup; + 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); 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++) { @@ -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. gcHandle = GCHandle.Alloc(header, GCHandleType.Pinned); 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++; } @@ -704,18 +721,18 @@ namespace Microsoft.Net.Http.Server // Subset of ComputeHeaders 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. - NotifyOnSendingHeaders(); + Start(); + _boundaryType = BoundaryType.Close; // TODO: Send headers async? ulong errorCode = SendHeaders(null, null, - UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_OPAQUE | - UnsafeNclNativeMethods.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_OPAQUE | + HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA | + HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA, true); - if (errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) + if (errorCode != ErrorCodes.ERROR_SUCCESS) { throw new WebListenerException((int)errorCode); } @@ -725,7 +742,7 @@ namespace Microsoft.Net.Http.Server { 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() { EnsureResponseStream(); + _bufferingEnabled = false; _nativeStream.SwitchToOpaqueMode(); } diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs index c7df4fdff7..c00c8af45f 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs @@ -22,23 +22,27 @@ // ------------------------------------------------------------------------------ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using static Microsoft.Net.Http.Server.UnsafeNclNativeMethods; namespace Microsoft.Net.Http.Server { 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 long _leftToWrite = long.MinValue; private bool _closed; private bool _inOpaqueMode; + private BufferBuilder _buffer = new BufferBuilder(); + // The last write needs special handling to cancel. private ResponseStreamAsyncResult _lastWrite; @@ -99,49 +103,185 @@ namespace Microsoft.Net.Http.Server // Send headers public override void Flush() { - if (_closed || _requestContext.Response.HeadersSent) + if (_closed) { return; } - UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite(); - // TODO: Verbose log + FlushInternal(endOfRequest: false); + } + 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 { - uint statusCode; - unsafe + if (!startedSending) { - // TODO: Don't add MoreData flag if content-length == 0? - flags |= UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA; - statusCode = _requestContext.Response.SendHeaders(null, null, flags, false); + statusCode = _requestContext.Response.SendHeaders(dataChunks, null, flags, false); } - - if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF) + else { - 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(); - throw; + throw exception; } } - // Send headers - public override Task FlushAsync(CancellationToken cancellationToken) + private List PinDataBuffers(bool endOfRequest, out HttpApi.HTTP_DATA_CHUNK[] dataChunks) { - if (_closed || _requestContext.Response.HeadersSent) + var pins = new List(); + 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(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(Helpers.CRLF)); + + if (endOfRequest) + { + SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment(Helpers.ChunkTerminator)); + } + } + + return pins; + } + + private static void SetDataChunk(HttpApi.HTTP_DATA_CHUNK[] chunks, ref int chunkIndex, List pins, ArraySegment 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 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(); } - 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(); + // Empty flush + return Helpers.CompletedTask(); } var cancellationRegistration = default(CancellationTokenRegistration); @@ -150,26 +290,37 @@ namespace Microsoft.Net.Http.Server cancellationRegistration = cancellationToken.Register(RequestContext.AbortDelegate, _requestContext); } - // TODO: Don't add MoreData flag if content-length == 0? - flags |= UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA; - ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, null, null, null, 0, 0, _requestContext.Response.BoundaryType == BoundaryType.Chunked, false, cancellationRegistration); + var flags = ComputeLeftToWrite(); + if (_leftToWrite != byteCount) + { + 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 { - uint statusCode; - unsafe + if (!startedSending) { statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false); + bytesSent = asyncResult.BytesSent; } - - if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess) + else { - // IO operation completed synchronously - callback won't be called to signal completion. - asyncResult.IOCompleted(statusCode); - } - else if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING) - { - throw new IOException(string.Empty, new WebListenerException((int)statusCode)); + statusCode = HttpApi.HttpSendResponseEntityBody( + _requestContext.RequestQueueHandle, + _requestContext.RequestId, + (uint)flags, + asyncResult.DataChunkCount, + asyncResult.DataChunks, + &bytesSent, + SafeLocalFree.Zero, + 0, + asyncResult.NativeOverlapped, + IntPtr.Zero); } } catch (Exception e) @@ -180,6 +331,34 @@ namespace Microsoft.Net.Http.Server 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; } @@ -195,13 +374,13 @@ namespace Microsoft.Net.Http.Server 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); } #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); } @@ -225,7 +404,7 @@ namespace Microsoft.Net.Http.Server UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE; if (!_requestContext.Response.ComputedHeaders) { - flags = _requestContext.Response.ComputeHeaders(endOfRequest: endOfRequest); + flags = _requestContext.Response.ComputeHeaders(endOfRequest, _buffer.TotalBytes); } if (_leftToWrite == long.MinValue) { @@ -246,229 +425,56 @@ namespace Microsoft.Net.Http.Server 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) - { - 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); - } + // Validates for null and bounds. Allows count == 0. + var data = new ArraySegment(buffer, offset, count); + CheckDisposed(); // TODO: Verbose log parameters - UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite(); - if (size == 0 && _leftToWrite != 0) - { - return; - } - if (_leftToWrite >= 0 && size > _leftToWrite) - { - throw new InvalidOperationException(Resources.Exception_TooMuchWritten); - } - // TODO: Verbose log + // Officially starts the response and fires OnSendingHeaders + _requestContext.Response.Start(); - uint statusCode; - uint dataToWrite = (uint)size; - SafeLocalFree bufferAsIntPtr = null; - IntPtr pBufferAsIntPtr = IntPtr.Zero; - bool sentHeaders = _requestContext.Response.HeadersSent; - try + var currentBytes = _buffer.TotalBytes + data.Count; + var contentLength = _requestContext.Response.ContentLength; + if (contentLength.HasValue && !_requestContext.Response.ComputedHeaders && contentLength.Value <= currentBytes) { - if (size == 0) + if (contentLength.Value < currentBytes) { - // TODO: Is this code path accessible? Is this like a Flush? - 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; - } - } - } + throw new InvalidOperationException("More bytes written than specified in the Content-Length header."); } + // or the last write in a response that hasn't started yet, flush immideately + _buffer.Add(data); + Flush(); } - 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 - bufferAsIntPtr.Dispose(); + throw new InvalidOperationException("More bytes written than specified in the Content-Length header."); } + _buffer.Add(data); + Flush(); } - - if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF) + else if (_requestContext.Response.ShouldBuffer && currentBytes < MaxBufferSize) { - Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode)); - LogHelper.LogException(_requestContext.Logger, "Write", exception); - Abort(); - throw exception; + _buffer.CopyAndAdd(data); + } + else + { + // Append to existing data without a copy, and then flush immidately + _buffer.Add(data); + Flush(); } - UpdateWritenCount(dataToWrite); - - // TODO: Verbose log data written } + #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 - 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 { - if (buffer == null) - { - 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; + return WriteAsync(buffer, offset, count).ToIAsyncResult(callback, state); } #if DNXCORE50 public void EndWrite(IAsyncResult asyncResult) @@ -480,143 +486,58 @@ namespace Microsoft.Net.Http.Server { throw new ArgumentNullException("asyncResult"); } - ResponseStreamAsyncResult castedAsyncResult = asyncResult as ResponseStreamAsyncResult; - 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; - } + ((Task)asyncResult).GetAwaiter().GetResult(); } - 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) - { - 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 - + // Validates for null and bounds. Allows count == 0. + var data = new ArraySegment(buffer, offset, count); if (cancellationToken.IsCancellationRequested) { return Helpers.CanceledTask(); } + CheckDisposed(); + // TODO: Verbose log parameters + // Officially starts the response and fires OnSendingHeaders + _requestContext.Response.Start(); - var cancellationRegistration = default(CancellationTokenRegistration); - if (cancellationToken.CanBeCanceled) + var currentBytes = _buffer.TotalBytes + data.Count; + var contentLength = _requestContext.Response.ContentLength; + if (contentLength.HasValue && !_requestContext.Response.ComputedHeaders && contentLength.Value <= currentBytes) { - cancellationRegistration = cancellationToken.Register(RequestContext.AbortDelegate, _requestContext); - } - - 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) + if (contentLength.Value < currentBytes) { - statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false); - bytesSent = asyncResult.BytesSent; + throw new InvalidOperationException("More bytes written than specified in the Content-Length header."); } - 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 = - UnsafeNclNativeMethods.HttpApi.HttpSendResponseEntityBody( - _requestContext.RequestQueueHandle, - _requestContext.RequestId, - (uint)flags, - asyncResult.DataChunkCount, - asyncResult.DataChunks, - &bytesSent, - SafeLocalFree.Zero, - 0, - asyncResult.NativeOverlapped, - IntPtr.Zero); + throw new InvalidOperationException("More bytes written than specified in the Content-Length header."); } + _buffer.Add(data); + return FlushAsync(cancellationToken); } - catch (Exception e) + else if (_requestContext.Response.ShouldBuffer && currentBytes < MaxBufferSize) { - LogHelper.LogException(_requestContext.Logger, "WriteAsync", e); - asyncResult.Dispose(); - Abort(); - throw; + _buffer.CopyAndAdd(data); + return Helpers.CompletedTask(); } - - if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING) + else { - asyncResult.Dispose(); - if (_requestContext.Server.IgnoreWriteExceptions && sentHeaders) - { - asyncResult.Complete(); - } - else - { - Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode)); - LogHelper.LogException(_requestContext.Logger, "WriteAsync", exception); - Abort(); - throw exception; - } + // Append to existing data without a copy, and then flush immidately + _buffer.Add(data); + return FlushAsync(cancellationToken); } - - 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. // This all happens inside of ResponseStreamAsyncResult. @@ -624,17 +545,26 @@ namespace Microsoft.Net.Http.Server { 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(); - if (size == 0 && _leftToWrite != 0) + if (count == 0 && _leftToWrite != 0) { return Helpers.CompletedTask(); } - if (_leftToWrite >= 0 && size > _leftToWrite) + if (_leftToWrite >= 0 && count > _leftToWrite) { throw new InvalidOperationException(Resources.Exception_TooMuchWritten); } @@ -653,30 +583,30 @@ namespace Microsoft.Net.Http.Server 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, fileName, offset, size, - _requestContext.Response.BoundaryType == BoundaryType.Chunked, sentHeaders, cancellationRegistration); + flags |= _leftToWrite == count ? HttpApi.HTTP_FLAGS.NONE : HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA; + bool startedSending = _requestContext.Response.HasStartedSending; + var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked; + ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, fileName, offset, count, chunked, cancellationRegistration); long bytesWritten; - if (_requestContext.Response.BoundaryType == BoundaryType.Chunked) + if (chunked) { bytesWritten = 0; } - else if (size.HasValue) + else if (count.HasValue) { - bytesWritten = size.Value; + bytesWritten = count.Value; } else { 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); try { - if (!sentHeaders) + if (!startedSending) { statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false); bytesSent = asyncResult.BytesSent; @@ -684,8 +614,7 @@ namespace Microsoft.Net.Http.Server else { // TODO: If opaque then include the buffer data flag. - statusCode = - UnsafeNclNativeMethods.HttpApi.HttpSendResponseEntityBody( + statusCode = HttpApi.HttpSendResponseEntityBody( _requestContext.RequestQueueHandle, _requestContext.RequestId, (uint)flags, @@ -709,7 +638,7 @@ namespace Microsoft.Net.Http.Server if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING) { asyncResult.Dispose(); - if (_requestContext.Server.IgnoreWriteExceptions && sentHeaders) + if (_requestContext.Server.IgnoreWriteExceptions && startedSending) { 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. 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) + if ((flags & HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA) == 0) { _lastWrite = asyncResult; } @@ -765,85 +694,7 @@ namespace Microsoft.Net.Http.Server return; } _closed = true; - UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite(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; + FlushInternal(endOfRequest: true); } } finally @@ -870,5 +721,13 @@ namespace Microsoft.Net.Http.Server UnsafeNclNativeMethods.CancelIoEx(requestQueueHandle, asyncState.NativeOverlapped); } } + + private void CheckDisposed() + { + if (_closed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } } } diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs index 95f09b4052..3510080030 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs @@ -27,117 +27,97 @@ using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using static Microsoft.Net.Http.Server.UnsafeNclNativeMethods; namespace Microsoft.Net.Http.Server { 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 SafeNativeOverlapped _overlapped; - private UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK[] _dataChunks; - private bool _sentHeaders; + private HttpApi.HTTP_DATA_CHUNK[] _dataChunks; private FileStream _fileStream; private ResponseStream _responseStream; private TaskCompletionSource _tcs; - private AsyncCallback _callback; private uint _bytesSent; private CancellationTokenRegistration _cancellationRegistration; - internal ResponseStreamAsyncResult(ResponseStream responseStream, object userState, AsyncCallback callback) + internal ResponseStreamAsyncResult(ResponseStream responseStream, CancellationTokenRegistration cancellationRegistration) { _responseStream = responseStream; - _tcs = new TaskCompletionSource(userState); - _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; + _tcs = new TaskCompletionSource(); _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; _overlapped = new SafeNativeOverlapped(boundHandle, 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(); + if (chunked) { - _dataChunks = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK[chunked ? 3 : 1]; - - 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); - } + chunkHeaderBuffer = Helpers.GetChunkHeader(buffer.TotalBytes); + SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, chunkHeaderBuffer); } + + 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(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, - string fileName, long offset, long? size, bool chunked, bool sentHeaders, - CancellationTokenRegistration cancellationRegistration) - : this(responseStream, userState, callback) + internal ResponseStreamAsyncResult(ResponseStream responseStream, string fileName, long offset, + long? count, bool chunked, CancellationTokenRegistration cancellationRegistration) + : this(responseStream, cancellationRegistration) { - _sentHeaders = sentHeaders; - _cancellationRegistration = cancellationRegistration; - var boundHandle = ResponseStream.RequestContext.Server.BoundHandle; + var boundHandle = responseStream.RequestContext.Server.BoundHandle; int bufferSize = 1024 * 64; // TODO: Validate buffer size choice. #if DNXCORE50 @@ -153,13 +133,13 @@ namespace Microsoft.Net.Http.Server _fileStream.Dispose(); 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(); - 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; _overlapped = new SafeNativeOverlapped(boundHandle, @@ -167,42 +147,34 @@ namespace Microsoft.Net.Http.Server } 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]; objectsToPin[_dataChunks.Length - 1] = _dataChunks; - int chunkHeaderOffset = 0; - byte[] chunkHeaderBuffer = null; + var chunkHeaderBuffer = new ArraySegment(); 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[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].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromFileHandle; _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(); // Nothing to pin for the file handle. - _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[1] = CRLF; + _dataChunks[2].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory; + _dataChunks[2].fromMemory.BufferLength = (uint)Helpers.CRLF.Length; + objectsToPin[1] = Helpers.CRLF; } else { - _dataChunks[0] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK(); - _dataChunks[0].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromFileHandle; + _dataChunks[0].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromFileHandle; _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(); } @@ -212,15 +184,21 @@ namespace Microsoft.Net.Http.Server if (chunked) { - _dataChunks[0].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer, chunkHeaderOffset); - _dataChunks[2].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(CRLF, 0); + // These must be set after pinning with Overlapped. + _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 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 @@ -254,7 +232,7 @@ namespace Microsoft.Net.Http.Server } } - internal UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK* DataChunks + internal HttpApi.HTTP_DATA_CHUNK* DataChunks { get { @@ -264,7 +242,7 @@ namespace Microsoft.Net.Http.Server } 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() { - if (_tcs.TrySetResult(null) && _callback != 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 - } - } + _tcs.TrySetResult(null); Dispose(); } internal void Fail(Exception ex) { - if (_tcs.TrySetException(ex) && _callback != null) - { - try - { - _callback(this); - } - catch (Exception) - { - // TODO: Exception handling? This may be an IO callback thread and throwing here could crash the app. - } - } + _tcs.TrySetException(ex); Dispose(); _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 { get { return _tcs.Task.AsyncState; } diff --git a/src/Microsoft.Net.Http.Server/WebListener.cs b/src/Microsoft.Net.Http.Server/WebListener.cs index 018058abe9..fa8099e275 100644 --- a/src/Microsoft.Net.Http.Server/WebListener.cs +++ b/src/Microsoft.Net.Http.Server/WebListener.cs @@ -91,6 +91,8 @@ namespace Microsoft.Net.Http.Server // The native request queue private long? _requestQueueLength; + private bool _bufferResponses = true; + public WebListener() : this(null) { @@ -134,6 +136,12 @@ namespace Microsoft.Net.Http.Server get { return _urlPrefixes; } } + public bool BufferResponses + { + get { return _bufferResponses; } + set { _bufferResponses = value; } + } + internal SafeHandle RequestQueueHandle { get diff --git a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/Microsoft.AspNet.Server.WebListener.FunctionalTests.xproj b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/Microsoft.AspNet.Server.WebListener.FunctionalTests.xproj index 80131fc053..a7ac76a466 100644 --- a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/Microsoft.AspNet.Server.WebListener.FunctionalTests.xproj +++ b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/Microsoft.AspNet.Server.WebListener.FunctionalTests.xproj @@ -13,5 +13,8 @@ 2.0 + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/OpaqueUpgradeTests.cs b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/OpaqueUpgradeTests.cs index fccf18ba9a..6d5057bba3 100644 --- a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/OpaqueUpgradeTests.cs +++ b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/OpaqueUpgradeTests.cs @@ -71,6 +71,7 @@ namespace Microsoft.AspNet.Server.WebListener { var httpContext = new DefaultHttpContext((IFeatureCollection)env); await httpContext.Response.WriteAsync("Hello World"); + await httpContext.Response.Body.FlushAsync(); try { var opaqueFeature = httpContext.GetFeature(); @@ -171,8 +172,8 @@ namespace Microsoft.AspNet.Server.WebListener using (Stream stream = await SendOpaqueRequestAsync(method, address, extraHeader)) { byte[] data = new byte[100]; - stream.WriteAsync(data, 0, 49).Wait(); - int read = stream.ReadAsync(data, 0, data.Length).Result; + await stream.WriteAsync(data, 0, 49); + int read = await stream.ReadAsync(data, 0, data.Length); Assert.Equal(49, read); } } diff --git a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseBodyTests.cs b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseBodyTests.cs index 7f77ff1880..6c0ae01f85 100644 --- a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseBodyTests.cs +++ b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseBodyTests.cs @@ -20,6 +20,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.FeatureModel; @@ -31,7 +32,7 @@ namespace Microsoft.AspNet.Server.WebListener public class ResponseBodyTests { [Fact] - public async Task ResponseBody_WriteNoHeaders_DefaultsToChunked() + public async Task ResponseBody_WriteNoHeaders_BuffersAndSetsContentLength() { string address; using (Utilities.CreateHttpServer(out address, env => @@ -45,24 +46,22 @@ namespace Microsoft.AspNet.Server.WebListener Assert.Equal(200, (int)response.StatusCode); Assert.Equal(new Version(1, 1), response.Version); IEnumerable ignored; - Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); - Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); + 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_WriteChunked_Chunked() + public async Task ResponseBody_WriteNoHeadersAndFlush_DefaultsToChunked() { string address; - using (Utilities.CreateHttpServer(out address, env => + using (Utilities.CreateHttpServer(out address, async env => { var httpContext = new DefaultHttpContext((IFeatureCollection)env); - httpContext.Request.Headers["transfeR-Encoding"] = " CHunked "; - Stream stream = httpContext.Response.Body; - stream.EndWrite(stream.BeginWrite(new byte[10], 0, 10, null, null)); - stream.Write(new byte[10], 0, 10); - return stream.WriteAsync(new byte[10], 0, 10); + httpContext.Response.Body.Write(new byte[10], 0, 10); + await httpContext.Response.Body.WriteAsync(new byte[10], 0, 10); + await httpContext.Response.Body.FlushAsync(); })) { HttpResponseMessage response = await SendRequestAsync(address); @@ -71,7 +70,30 @@ namespace Microsoft.AspNet.Server.WebListener IEnumerable ignored; Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); 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 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] - public void ResponseBody_WriteContentLengthTooMuchWritten_Throws() + public async Task ResponseBody_WriteContentLengthTooMuchWritten_Throws() { string address; using (Utilities.CreateHttpServer(out address, env => @@ -144,7 +166,8 @@ namespace Microsoft.AspNet.Server.WebListener return Task.FromResult(0); })) { - Assert.Throws(() => SendRequestAsync(address).Result); + var response = await SendRequestAsync(address); + Assert.Equal(500, (int)response.StatusCode); } } diff --git a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/WebSocketTests.cs b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/WebSocketTests.cs index 544ef17a22..6fbcc31eb4 100644 --- a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/WebSocketTests.cs +++ b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/WebSocketTests.cs @@ -84,7 +84,6 @@ namespace Microsoft.AspNet.Server.WebListener { HttpResponseMessage response = await SendRequestAsync(address); Assert.Equal(200, (int)response.StatusCode); - Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); Assert.True(upgradeThrew.Value); } } diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/OpaqueUpgradeTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/OpaqueUpgradeTests.cs index 9d3d0553ec..e077352d42 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/OpaqueUpgradeTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/OpaqueUpgradeTests.cs @@ -31,7 +31,6 @@ namespace Microsoft.Net.Http.Server context.Dispose(); HttpResponseMessage response = await clientTask; Assert.Equal(200, (int)response.StatusCode); - Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); Assert.Equal("Hello World", await response.Content.ReadAsStringAsync()); } } diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs index 779b7080fb..8cae679baa 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.Net.Http.Server public class ResponseBodyTests { [Fact] - public async Task ResponseBody_WriteNoHeaders_DefaultsToChunked() + public async Task ResponseBody_BufferWriteNoHeaders_DefaultsToContentLength() { string address; using (var server = Utilities.CreateHttpServer(out address)) @@ -23,6 +23,31 @@ namespace Microsoft.Net.Http.Server Task responseTask = SendRequestAsync(address); 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 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 responseTask = SendRequestAsync(address); + + var context = await server.GetContextAsync(); + context.Response.ShouldBuffer = false; context.Response.Body.Write(new byte[10], 0, 10); await context.Response.Body.WriteAsync(new byte[10], 0, 10); context.Dispose(); @@ -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 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 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] public async Task ResponseBody_WriteChunked_ManuallyChunked() { @@ -111,7 +159,7 @@ namespace Microsoft.Net.Http.Server } [Fact] - public async Task ResponseBody_WriteContentLengthNotEnoughWritten_Throws() + public async Task ResponseBody_WriteContentLengthNotEnoughWritten_Aborts() { string 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.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(() => responseTask); } } @@ -141,6 +195,13 @@ namespace Microsoft.Net.Http.Server Assert.Throws(() => context.Response.Body.Write(new byte[6], 0, 6)); 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(() => context.Response.Body.Write(new byte[6], 0, 6)); + context.Dispose(); + await Assert.ThrowsAsync(() => 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 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 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 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 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 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 ignored; + Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); + Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); + Assert.Equal(new byte[1020 * 6], await response.Content.ReadAsByteArrayAsync()); + } + } + [Fact] public async Task ResponseBody_WriteAsyncWithActiveCancellationToken_Success() { diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseHeaderTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseHeaderTests.cs index f16bc98142..2f6648aaf3 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseHeaderTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseHeaderTests.cs @@ -349,13 +349,13 @@ namespace Microsoft.Net.Http.Server responseHeaders.SetValues("Custom1", "value1a", "value1b"); responseHeaders.SetValues("Custom2", "value2a, value2b"); var body = context.Response.Body; - Assert.False(context.Response.HeadersSent); + Assert.False(context.Response.HasStarted); body.Flush(); - Assert.True(context.Response.HeadersSent); + Assert.True(context.Response.HasStarted); var ex = Assert.Throws(() => context.Response.StatusCode = 404); Assert.Equal("Headers already sent.", ex.Message); ex = Assert.Throws(() => 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(); @@ -385,13 +385,13 @@ namespace Microsoft.Net.Http.Server responseHeaders.SetValues("Custom1", "value1a", "value1b"); responseHeaders.SetValues("Custom2", "value2a, value2b"); var body = context.Response.Body; - Assert.False(context.Response.HeadersSent); + Assert.False(context.Response.HasStarted); await body.FlushAsync(); - Assert.True(context.Response.HeadersSent); + Assert.True(context.Response.HasStarted); var ex = Assert.Throws(() => context.Response.StatusCode = 404); Assert.Equal("Headers already sent.", ex.Message); ex = Assert.Throws(() => 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(); diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs index b81fd6d514..66014c92db 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs @@ -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 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 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] public async Task ResponseSendFile_ContentLength_PassedThrough() { diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/WebSocketTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/WebSocketTests.cs index 377bb51cbd..1d600fbd6b 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/WebSocketTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/WebSocketTests.cs @@ -30,7 +30,6 @@ namespace Microsoft.Net.Http.Server context.Dispose(); HttpResponseMessage response = await clientTask; Assert.Equal(200, (int)response.StatusCode); - Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); Assert.Equal("Hello World", await response.Content.ReadAsStringAsync()); } }