#120 Implement response buffering.
This commit is contained in:
Родитель
0acb8f3ed4
Коммит
3c044fb92e
|
@ -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<object> callback, object state)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -21,13 +21,18 @@
|
|||
// </copyright>
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
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<object>(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<int>(state);
|
||||
task.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
tcs.TrySetException(t.Exception.InnerExceptions);
|
||||
}
|
||||
else if (t.IsCanceled)
|
||||
{
|
||||
tcs.TrySetCanceled();
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs.TrySetResult(0);
|
||||
}
|
||||
|
||||
if (callback != null)
|
||||
{
|
||||
callback(tcs.Task);
|
||||
}
|
||||
}, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A private utility routine to convert an integer to a chunk header,
|
||||
/// which is an ASCII hex number followed by a CRLF.The header is returned
|
||||
/// as a byte array.
|
||||
/// Generates a right-aligned hex string and returns the start offset.
|
||||
/// </summary>
|
||||
/// <param name="size">Chunk size to be encoded</param>
|
||||
/// <param name="offset">Out parameter where we store offset into buffer.</param>
|
||||
/// <returns>A byte array with the header in int.</returns>
|
||||
internal static ArraySegment<byte> GetChunkHeader(int size)
|
||||
{
|
||||
uint mask = 0xf0000000;
|
||||
byte[] header = new byte[10];
|
||||
int i;
|
||||
int offset = -1;
|
||||
|
||||
// Loop through the size, looking at each nibble. If it's not 0
|
||||
// convert it to hex. Save the index of the first non-zero
|
||||
// byte.
|
||||
|
||||
for (i = 0; i < 8; i++, size <<= 4)
|
||||
{
|
||||
// offset == -1 means that we haven't found a non-zero nibble
|
||||
// yet. If we haven't found one, and the current one is zero,
|
||||
// don't do anything.
|
||||
|
||||
if (offset == -1)
|
||||
{
|
||||
if ((size & mask) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Either we have a non-zero nibble or we're no longer skipping
|
||||
// leading zeros. Convert this nibble to ASCII and save it.
|
||||
|
||||
uint temp = (uint)size >> 28;
|
||||
|
||||
if (temp < 10)
|
||||
{
|
||||
header[i] = (byte)(temp + '0');
|
||||
}
|
||||
else
|
||||
{
|
||||
header[i] = (byte)((temp - 10) + 'A');
|
||||
}
|
||||
|
||||
// If we haven't found a non-zero nibble yet, we've found one
|
||||
// now, so remember that.
|
||||
|
||||
if (offset == -1)
|
||||
{
|
||||
offset = i;
|
||||
}
|
||||
}
|
||||
|
||||
header[8] = (byte)'\r';
|
||||
header[9] = (byte)'\n';
|
||||
|
||||
return new ArraySegment<byte>(header, offset, header.Length - offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Net.Http.Server
|
||||
{
|
||||
internal class BufferBuilder
|
||||
{
|
||||
private List<ArraySegment<byte>> _buffers = new List<ArraySegment<byte>>();
|
||||
|
||||
internal IEnumerable<ArraySegment<byte>> Buffers
|
||||
{
|
||||
get { return _buffers; }
|
||||
}
|
||||
|
||||
internal int BufferCount
|
||||
{
|
||||
get { return _buffers.Count; }
|
||||
}
|
||||
|
||||
internal int TotalBytes { get; private set; }
|
||||
|
||||
internal void Add(ArraySegment<byte> data)
|
||||
{
|
||||
_buffers.Add(data);
|
||||
TotalBytes += data.Count;
|
||||
}
|
||||
|
||||
public void CopyAndAdd(ArraySegment<byte> data)
|
||||
{
|
||||
if (data.Count > 0)
|
||||
{
|
||||
var temp = new byte[data.Count];
|
||||
Buffer.BlockCopy(data.Array, data.Offset, temp, 0, data.Count);
|
||||
_buffers.Add(new ArraySegment<byte>(temp));
|
||||
TotalBytes += data.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_buffers.Clear();
|
||||
TotalBytes = 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,15 +21,15 @@ namespace Microsoft.Net.Http.Server
|
|||
|
||||
private IDictionary<string, string[]> 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<string> Keys
|
||||
{
|
||||
get { return Store.Keys; }
|
||||
|
@ -73,19 +68,19 @@ namespace Microsoft.Net.Http.Server
|
|||
|
||||
public void Add(KeyValuePair<string, string[]> 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<string, string[]> 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -158,9 +158,9 @@ namespace Microsoft.Net.Http.Server
|
|||
|
||||
public Task<Stream> UpgradeAsync()
|
||||
{
|
||||
if (!IsUpgradableRequest || _response.HeadersSent)
|
||||
if (!IsUpgradableRequest || _response.HasStarted)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
throw new InvalidOperationException("This request cannot be upgraded. It is incompatible, or the response has already started.");
|
||||
}
|
||||
|
||||
// Set the status code and reason phrase
|
||||
|
|
|
@ -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<Tuple<Action<object>, object>> _onSendingHeadersActions;
|
||||
private IList<Tuple<Action<object>, 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<Tuple<Action<object>, object>>();
|
||||
_onResponseCompletedActions = new List<Tuple<Action<object>, 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<GCHandle> 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<GCHandle>();
|
||||
}
|
||||
var handle = GCHandle.Alloc(dataChunks, GCHandleType.Pinned);
|
||||
pinnedHeaders.Add(handle);
|
||||
_nativeResponse.Response_V1.EntityChunkCount = (ushort)dataChunks.Length;
|
||||
_nativeResponse.Response_V1.pEntityChunks = (HttpApi.HTTP_DATA_CHUNK*)handle.AddrOfPinnedObject();
|
||||
}
|
||||
else if (asyncResult != null && asyncResult.DataChunks != null)
|
||||
{
|
||||
|
@ -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<GCHandle> 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<GCHandle> 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<string, string[]> 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<UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS>();
|
||||
(uint)Marshal.SizeOf<HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS>();
|
||||
#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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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<GCHandle> PinDataBuffers(bool endOfRequest, out HttpApi.HTTP_DATA_CHUNK[] dataChunks)
|
||||
{
|
||||
if (_closed || _requestContext.Response.HeadersSent)
|
||||
var pins = new List<GCHandle>();
|
||||
var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
|
||||
|
||||
var currentChunk = 0;
|
||||
// Figure out how many data chunks
|
||||
if (chunked && _buffer.TotalBytes == 0 && endOfRequest)
|
||||
{
|
||||
dataChunks = new HttpApi.HTTP_DATA_CHUNK[1];
|
||||
SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment<byte>(Helpers.ChunkTerminator));
|
||||
return pins;
|
||||
}
|
||||
else if (_buffer.TotalBytes == 0)
|
||||
{
|
||||
// No data
|
||||
dataChunks = new HttpApi.HTTP_DATA_CHUNK[0];
|
||||
return pins;
|
||||
}
|
||||
|
||||
var chunkCount = _buffer.BufferCount;
|
||||
if (chunked)
|
||||
{
|
||||
// Chunk framing
|
||||
chunkCount += 2;
|
||||
|
||||
if (endOfRequest)
|
||||
{
|
||||
// Chunk terminator
|
||||
chunkCount += 1;
|
||||
}
|
||||
}
|
||||
dataChunks = new HttpApi.HTTP_DATA_CHUNK[chunkCount];
|
||||
|
||||
if (chunked)
|
||||
{
|
||||
var chunkHeaderBuffer = Helpers.GetChunkHeader(_buffer.TotalBytes);
|
||||
SetDataChunk(dataChunks, ref currentChunk, pins, chunkHeaderBuffer);
|
||||
}
|
||||
|
||||
foreach (var buffer in _buffer.Buffers)
|
||||
{
|
||||
SetDataChunk(dataChunks, ref currentChunk, pins, buffer);
|
||||
}
|
||||
|
||||
if (chunked)
|
||||
{
|
||||
SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment<byte>(Helpers.CRLF));
|
||||
|
||||
if (endOfRequest)
|
||||
{
|
||||
SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment<byte>(Helpers.ChunkTerminator));
|
||||
}
|
||||
}
|
||||
|
||||
return pins;
|
||||
}
|
||||
|
||||
private static void SetDataChunk(HttpApi.HTTP_DATA_CHUNK[] chunks, ref int chunkIndex, List<GCHandle> pins, ArraySegment<byte> buffer)
|
||||
{
|
||||
var handle = GCHandle.Alloc(buffer.Array, GCHandleType.Pinned);
|
||||
pins.Add(handle);
|
||||
chunks[chunkIndex].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
||||
chunks[chunkIndex].fromMemory.pBuffer = handle.AddrOfPinnedObject() + buffer.Offset;
|
||||
chunks[chunkIndex].fromMemory.BufferLength = (uint)buffer.Count;
|
||||
chunkIndex++;
|
||||
}
|
||||
|
||||
private void FreeDataBuffers(List<GCHandle> pinnedBuffers)
|
||||
{
|
||||
foreach (var pin in pinnedBuffers)
|
||||
{
|
||||
if (pin.IsAllocated)
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Simpler than Flush because it will never be called at the end of the request from Dispose.
|
||||
public unsafe override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_closed)
|
||||
{
|
||||
return Helpers.CompletedTask();
|
||||
}
|
||||
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite();
|
||||
// TODO: Verbose log
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
bool startedSending = _requestContext.Response.HasStartedSending;
|
||||
var byteCount = _buffer.TotalBytes;
|
||||
if (byteCount == 0 && startedSending)
|
||||
{
|
||||
return Helpers.CanceledTask<int>();
|
||||
// Empty flush
|
||||
return Helpers.CompletedTask();
|
||||
}
|
||||
|
||||
var cancellationRegistration = default(CancellationTokenRegistration);
|
||||
|
@ -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<byte>(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<byte>(buffer, offset, count);
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Helpers.CanceledTask<int>();
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<object> _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<object>(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<object>();
|
||||
_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<byte>();
|
||||
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<byte>(Helpers.CRLF));
|
||||
}
|
||||
|
||||
// This call will pin needed memory
|
||||
_overlapped = new SafeNativeOverlapped(boundHandle,
|
||||
boundHandle.AllocateNativeOverlapped(IOCallback, this, objectsToPin));
|
||||
|
||||
currentChunk = 0;
|
||||
if (chunked)
|
||||
{
|
||||
_dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer.Array, chunkHeaderBuffer.Offset);
|
||||
currentChunk++;
|
||||
}
|
||||
foreach (var segment in buffer.Buffers)
|
||||
{
|
||||
_dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(segment.Array, segment.Offset);
|
||||
currentChunk++;
|
||||
}
|
||||
if (chunked)
|
||||
{
|
||||
_dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(Helpers.CRLF, 0);
|
||||
currentChunk++;
|
||||
}
|
||||
|
||||
// We've captured a reference to all the buffers, clear the buffer so that it can be used to queue overlapped writes.
|
||||
buffer.Clear();
|
||||
}
|
||||
|
||||
internal ResponseStreamAsyncResult(ResponseStream responseStream, object userState, AsyncCallback callback,
|
||||
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<byte>();
|
||||
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<byte> segment)
|
||||
{
|
||||
get { return _responseStream; }
|
||||
objectsToPin[pinIndex] = segment.Array;
|
||||
pinIndex++;
|
||||
chunks[chunkIndex].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
||||
// The address is not set until after we pin it with Overlapped
|
||||
chunks[chunkIndex].fromMemory.BufferLength = (uint)segment.Count;
|
||||
chunkIndex++;
|
||||
}
|
||||
|
||||
internal SafeNativeOverlapped NativeOverlapped
|
||||
|
@ -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; }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -13,5 +13,8 @@
|
|||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
|
@ -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<IHttpUpgradeFeature>();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<string> 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<string> ignored;
|
||||
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
|
||||
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
|
||||
Assert.Equal(new byte[30], await response.Content.ReadAsByteArrayAsync());
|
||||
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResponseBody_WriteChunked_ManuallyChunked()
|
||||
{
|
||||
string address;
|
||||
using (Utilities.CreateHttpServer(out address, async env =>
|
||||
{
|
||||
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
||||
httpContext.Response.Headers["transfeR-Encoding"] = " CHunked ";
|
||||
Stream stream = httpContext.Response.Body;
|
||||
var responseBytes = Encoding.ASCII.GetBytes("10\r\nManually Chunked\r\n0\r\n\r\n");
|
||||
await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
|
||||
}))
|
||||
{
|
||||
HttpResponseMessage response = await SendRequestAsync(address);
|
||||
Assert.Equal(200, (int)response.StatusCode);
|
||||
Assert.Equal(new Version(1, 1), response.Version);
|
||||
IEnumerable<string> ignored;
|
||||
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
|
||||
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
|
||||
Assert.Equal("Manually Chunked", await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,7 +154,7 @@ namespace Microsoft.AspNet.Server.WebListener
|
|||
}
|
||||
|
||||
[Fact]
|
||||
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<AggregateException>(() => SendRequestAsync(address).Result);
|
||||
var response = await SendRequestAsync(address);
|
||||
Assert.Equal(500, (int)response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<HttpResponseMessage> 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<string> ignored;
|
||||
Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
|
||||
Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
|
||||
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResponseBody_NoBufferWriteNoHeaders_DefaultsToChunked()
|
||||
{
|
||||
string address;
|
||||
using (var server = Utilities.CreateHttpServer(out address))
|
||||
{
|
||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
||||
|
||||
var context = await server.GetContextAsync();
|
||||
context.Response.ShouldBuffer = false;
|
||||
context.Response.Body.Write(new byte[10], 0, 10);
|
||||
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<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
||||
|
||||
var context = await server.GetContextAsync();
|
||||
context.Response.Body.Write(new byte[10], 0, 10);
|
||||
context.Response.Body.Flush();
|
||||
await context.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||
context.Dispose();
|
||||
|
||||
HttpResponseMessage response = await responseTask;
|
||||
Assert.Equal(200, (int)response.StatusCode);
|
||||
IEnumerable<string> contentLength;
|
||||
Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
|
||||
Assert.True(response.Headers.TransferEncodingChunked.HasValue);
|
||||
Assert.Equal(20, (await response.Content.ReadAsByteArrayAsync()).Length);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
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<HttpRequestException>(() => responseTask);
|
||||
}
|
||||
}
|
||||
|
@ -141,6 +195,13 @@ namespace Microsoft.Net.Http.Server
|
|||
Assert.Throws<InvalidOperationException>(() => 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<InvalidOperationException>(() => context.Response.Body.Write(new byte[6], 0, 6));
|
||||
context.Dispose();
|
||||
|
||||
await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
|
||||
}
|
||||
}
|
||||
|
@ -170,6 +231,92 @@ namespace Microsoft.Net.Http.Server
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResponseBody_WriteZeroCount_StartsResponse()
|
||||
{
|
||||
string address;
|
||||
using (var server = Utilities.CreateHttpServer(out address))
|
||||
{
|
||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
||||
|
||||
var context = await server.GetContextAsync();
|
||||
context.Response.Body.Write(new byte[10], 0, 0);
|
||||
Assert.True(context.Response.HasStarted);
|
||||
await context.Response.Body.WriteAsync(new byte[10], 0, 0);
|
||||
context.Dispose();
|
||||
|
||||
HttpResponseMessage response = await responseTask;
|
||||
Assert.Equal(200, (int)response.StatusCode);
|
||||
Assert.Equal(new Version(1, 1), response.Version);
|
||||
IEnumerable<string> ignored;
|
||||
Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
|
||||
Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
|
||||
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResponseBody_WriteMoreThanBufferLimitBufferWithNoHeaders_DefaultsToChunkedAndFlushes()
|
||||
{
|
||||
string address;
|
||||
using (var server = Utilities.CreateHttpServer(out address))
|
||||
{
|
||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
||||
|
||||
var context = await server.GetContextAsync();
|
||||
context.Response.ShouldBuffer = true;
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
context.Response.Body.Write(new byte[1020], 0, 1020);
|
||||
Assert.True(context.Response.HasStarted);
|
||||
Assert.False(context.Response.HasStartedSending);
|
||||
}
|
||||
context.Response.Body.Write(new byte[1020], 0, 1020);
|
||||
Assert.True(context.Response.HasStartedSending);
|
||||
context.Response.Body.Write(new byte[1020], 0, 1020);
|
||||
context.Dispose();
|
||||
|
||||
HttpResponseMessage response = await responseTask;
|
||||
Assert.Equal(200, (int)response.StatusCode);
|
||||
Assert.Equal(new Version(1, 1), response.Version);
|
||||
IEnumerable<string> ignored;
|
||||
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
|
||||
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
|
||||
Assert.Equal(new byte[1020*6], await response.Content.ReadAsByteArrayAsync());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResponseBody_WriteAsyncMoreThanBufferLimitBufferWithNoHeaders_DefaultsToChunkedAndFlushes()
|
||||
{
|
||||
string address;
|
||||
using (var server = Utilities.CreateHttpServer(out address))
|
||||
{
|
||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
||||
|
||||
var context = await server.GetContextAsync();
|
||||
context.Response.ShouldBuffer = true;
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
await context.Response.Body.WriteAsync(new byte[1020], 0, 1020);
|
||||
Assert.True(context.Response.HasStarted);
|
||||
Assert.False(context.Response.HasStartedSending);
|
||||
}
|
||||
await context.Response.Body.WriteAsync(new byte[1020], 0, 1020);
|
||||
Assert.True(context.Response.HasStartedSending);
|
||||
await context.Response.Body.WriteAsync(new byte[1020], 0, 1020);
|
||||
context.Dispose();
|
||||
|
||||
HttpResponseMessage response = await responseTask;
|
||||
Assert.Equal(200, (int)response.StatusCode);
|
||||
Assert.Equal(new Version(1, 1), response.Version);
|
||||
IEnumerable<string> ignored;
|
||||
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
|
||||
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
|
||||
Assert.Equal(new byte[1020 * 6], await response.Content.ReadAsByteArrayAsync());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResponseBody_WriteAsyncWithActiveCancellationToken_Success()
|
||||
{
|
||||
|
|
|
@ -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<InvalidOperationException>(() => context.Response.StatusCode = 404);
|
||||
Assert.Equal("Headers already sent.", ex.Message);
|
||||
ex = Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
|
||||
Assert.Equal("The response headers cannot be modified because they have already been sent.", ex.Message);
|
||||
Assert.Equal("The response headers cannot be modified because the response has already started.", ex.Message);
|
||||
|
||||
context.Dispose();
|
||||
|
||||
|
@ -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<InvalidOperationException>(() => context.Response.StatusCode = 404);
|
||||
Assert.Equal("Headers already sent.", ex.Message);
|
||||
ex = Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
|
||||
Assert.Equal("The response headers cannot be modified because they have already been sent.", ex.Message);
|
||||
Assert.Equal("The response headers cannot be modified because the response has already started.", ex.Message);
|
||||
|
||||
context.Dispose();
|
||||
|
||||
|
|
|
@ -202,6 +202,34 @@ namespace Microsoft.Net.Http.Server
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResponseSendFile_EmptyFileCountUnspecified_SetsChunkedAndFlushesHeaders()
|
||||
{
|
||||
var emptyFilePath = Path.Combine(Environment.CurrentDirectory, "zz_" + Guid.NewGuid().ToString() + "EmptyTestFile.txt");
|
||||
var emptyFile = File.Create(emptyFilePath, 1024);
|
||||
emptyFile.Close();
|
||||
|
||||
string address;
|
||||
using (var server = Utilities.CreateHttpServer(out address))
|
||||
{
|
||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
||||
|
||||
var context = await server.GetContextAsync();
|
||||
await context.Response.SendFileAsync(emptyFilePath, 0, null, CancellationToken.None);
|
||||
Assert.True(context.Response.HasStartedSending);
|
||||
await context.Response.Body.WriteAsync(new byte[10], 0, 10, CancellationToken.None);
|
||||
context.Dispose();
|
||||
File.Delete(emptyFilePath);
|
||||
|
||||
HttpResponseMessage response = await responseTask;
|
||||
Assert.Equal(200, (int)response.StatusCode);
|
||||
IEnumerable<string> contentLength;
|
||||
Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
|
||||
Assert.True(response.Headers.TransferEncodingChunked.HasValue);
|
||||
Assert.Equal(10, (await response.Content.ReadAsByteArrayAsync()).Length);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResponseSendFile_ContentLength_PassedThrough()
|
||||
{
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче