#112, #113 Sort out default response modes, allow manual chunking.

This commit is contained in:
Chris R 2015-05-15 14:55:54 -07:00
parent 748a6e1090
commit 2681e8b3d1
11 changed files with 320 additions and 309 deletions

View File

@ -29,6 +29,9 @@ namespace Microsoft.Net.Http.Server
{ {
internal const string HttpScheme = "http"; internal const string HttpScheme = "http";
internal const string HttpsScheme = "https"; internal const string HttpsScheme = "https";
internal const string Chunked = "chunked";
internal const string Close = "close";
internal const string Zero = "0";
internal const string SchemeDelimiter = "://"; internal const string SchemeDelimiter = "://";
internal static Version V1_0 = new Version(1, 0); internal static Version V1_0 = new Version(1, 0);

View File

@ -28,6 +28,8 @@ namespace Microsoft.Net.Http.Server
None = 0, None = 0,
Chunked = 1, // Transfer-Encoding: chunked Chunked = 1, // Transfer-Encoding: chunked
ContentLength = 2, // Content-Length: XXX ContentLength = 2, // Content-Length: XXX
Invalid = 3, Close = 3, // Connection: close
PassThrough = 4, // The application is handling the boundary themselves (e.g. chunking themselves).
Invalid = 5,
} }
} }

View File

@ -263,6 +263,11 @@ namespace Microsoft.Net.Http.Server
get { return _httpMethod; } get { return _httpMethod; }
} }
public bool IsHeadMethod
{
get { return string.Equals(_httpMethod, "HEAD", StringComparison.OrdinalIgnoreCase); }
}
public Stream Body public Stream Body
{ {
get get
@ -413,7 +418,7 @@ namespace Microsoft.Net.Http.Server
{ {
get get
{ {
return Headers.Get(HttpKnownHeaderNames.ContentLength); return Headers.Get(HttpKnownHeaderNames.ContentType);
} }
} }

View File

@ -38,13 +38,13 @@ namespace Microsoft.Net.Http.Server
{ {
public sealed unsafe class Response public sealed unsafe class Response
{ {
private static readonly string[] ZeroContentLength = new[] { "0" }; private static readonly string[] ZeroContentLength = new[] { Constants.Zero };
private ResponseState _responseState; private ResponseState _responseState;
private HeaderCollection _headers; private HeaderCollection _headers;
private string _reasonPhrase; private string _reasonPhrase;
private ResponseStream _nativeStream; private ResponseStream _nativeStream;
private long _contentLength; private long _expectedBodyLength;
private BoundaryType _boundaryType; private BoundaryType _boundaryType;
private UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2 _nativeResponse; private UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2 _nativeResponse;
private IList<Tuple<Action<object>, object>> _onSendingHeadersActions; private IList<Tuple<Action<object>, object>> _onSendingHeadersActions;
@ -166,11 +166,11 @@ namespace Microsoft.Net.Http.Server
get { return _headers; } get { return _headers; }
} }
internal long CalculatedLength internal long ExpectedBodyLength
{ {
get get
{ {
return _contentLength; return _expectedBodyLength;
} }
} }
@ -184,7 +184,7 @@ namespace Microsoft.Net.Http.Server
if (!string.IsNullOrWhiteSpace(contentLengthString)) if (!string.IsNullOrWhiteSpace(contentLengthString))
{ {
contentLengthString = contentLengthString.Trim(); contentLengthString = contentLengthString.Trim();
if (string.Equals("0", contentLengthString, StringComparison.Ordinal)) if (string.Equals(Constants.Zero, contentLengthString, StringComparison.Ordinal))
{ {
return 0; return 0;
} }
@ -225,7 +225,7 @@ namespace Microsoft.Net.Http.Server
{ {
get get
{ {
return Headers.Get(HttpKnownHeaderNames.ContentLength); return Headers.Get(HttpKnownHeaderNames.ContentType);
} }
set set
{ {
@ -241,44 +241,6 @@ namespace Microsoft.Net.Http.Server
} }
} }
private Version GetProtocolVersion()
{
/*
Version requestVersion = Request.ProtocolVersion;
Version responseVersion = requestVersion;
string protocolVersion = RequestContext.Environment.Get<string>(Constants.HttpResponseProtocolKey);
// Optional
if (!string.IsNullOrWhiteSpace(protocolVersion))
{
if (string.Equals("HTTP/1.1", protocolVersion, StringComparison.OrdinalIgnoreCase))
{
responseVersion = Constants.V1_1;
}
if (string.Equals("HTTP/1.0", protocolVersion, StringComparison.OrdinalIgnoreCase))
{
responseVersion = Constants.V1_0;
}
else
{
// TODO: Just log? It's too late to get this to user code.
throw new ArgumentException(string.Empty, Constants.HttpResponseProtocolKey);
}
}
if (requestVersion == responseVersion)
{
return requestVersion;
}
// Return the lesser of the two versions. There are only two, so it it will always be 1.0.
return Constants.V1_0;*/
// TODO: IHttpResponseInformation does not define a response protocol version. Http.Sys doesn't let
// us send anything but 1.1 anyways, but we could at least use it to set things like the connection header.
return Request.ProtocolVersion;
}
// should only be called from RequestContext // should only be called from RequestContext
internal void Dispose() internal void Dispose()
{ {
@ -476,121 +438,87 @@ namespace Microsoft.Net.Http.Server
RequestContext.Server.AuthenticationManager.SetAuthenticationChallenge(RequestContext); RequestContext.Server.AuthenticationManager.SetAuthenticationChallenge(RequestContext);
} }
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE; var flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE;
Debug.Assert(!ComputedHeaders, "HttpListenerResponse::ComputeHeaders()|ComputedHeaders is true."); Debug.Assert(!ComputedHeaders, "HttpListenerResponse::ComputeHeaders()|ComputedHeaders is true.");
_responseState = ResponseState.ComputedHeaders; _responseState = ResponseState.ComputedHeaders;
/*
// here we would check for BoundaryType.Raw, in this case we wouldn't need to do anything
if (m_BoundaryType==BoundaryType.Raw) {
return flags;
}
*/
// Check the response headers to determine the correct keep alive and boundary type. // Gather everything from the request that affects the response:
Version responseVersion = GetProtocolVersion(); var requestVersion = Request.ProtocolVersion;
_nativeResponse.Response_V1.Version.MajorVersion = (ushort)responseVersion.Major; var requestConnectionString = Request.Headers.Get(HttpKnownHeaderNames.Connection);
_nativeResponse.Response_V1.Version.MinorVersion = (ushort)responseVersion.Minor; var isHeadRequest = Request.IsHeadMethod;
bool keepAlive = responseVersion >= Constants.V1_1; var requestCloseSet = Matches(Constants.Close, requestConnectionString);
string connectionString = Headers.Get(HttpKnownHeaderNames.Connection);
string keepAliveString = Headers.Get(HttpKnownHeaderNames.KeepAlive);
bool closeSet = false;
bool keepAliveSet = false;
if (!string.IsNullOrWhiteSpace(connectionString) && string.Equals("close", connectionString.Trim(), StringComparison.OrdinalIgnoreCase)) // Gather everything the app may have set on the response:
// Http.Sys does not allow us to specify the response protocol version, assume this is a HTTP/1.1 response when making decisions.
var responseConnectionString = Headers.Get(HttpKnownHeaderNames.Connection);
var transferEncodingString = Headers.Get(HttpKnownHeaderNames.TransferEncoding);
var responseContentLength = ContentLength;
var responseCloseSet = Matches(Constants.Close, responseConnectionString);
var responseChunkedSet = Matches(Constants.Chunked, transferEncodingString);
var statusCanHaveBody = CanSendResponseBody(_requestContext.Response.StatusCode);
// Determine if the connection will be kept alive or closed.
var keepConnectionAlive = true;
if (requestVersion <= Constants.V1_0 // Http.Sys does not support "Keep-Alive: true" or "Connection: Keep-Alive"
|| (requestVersion == Constants.V1_1 && requestCloseSet)
|| responseCloseSet)
{ {
keepAlive = false; keepConnectionAlive = false;
closeSet = true;
}
else if (!string.IsNullOrWhiteSpace(keepAliveString) && string.Equals("true", keepAliveString.Trim(), StringComparison.OrdinalIgnoreCase))
{
keepAlive = true;
keepAliveSet = true;
} }
// Content-Length takes priority // Determine the body format. If the user asks to do something, let them, otherwise choose a good default for the scenario.
long? contentLength = ContentLength; if (responseContentLength.HasValue)
string transferEncodingString = Headers.Get(HttpKnownHeaderNames.TransferEncoding);
if (responseVersion == Constants.V1_0 && !string.IsNullOrEmpty(transferEncodingString)
&& string.Equals("chunked", transferEncodingString.Trim(), StringComparison.OrdinalIgnoreCase))
{ {
// A 1.0 client can't process chunked responses.
Headers.Remove(HttpKnownHeaderNames.TransferEncoding);
transferEncodingString = null;
}
if (contentLength.HasValue)
{
_contentLength = contentLength.Value;
_boundaryType = BoundaryType.ContentLength; _boundaryType = BoundaryType.ContentLength;
if (_contentLength == 0) // ComputeLeftToWrite checks for HEAD requests when setting _leftToWrite
{ _expectedBodyLength = responseContentLength.Value;
flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE;
}
} }
else if (!string.IsNullOrWhiteSpace(transferEncodingString) else if (responseChunkedSet)
&& string.Equals("chunked", transferEncodingString.Trim(), StringComparison.OrdinalIgnoreCase)) {
// 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.
{
if (statusCanHaveBody)
{
Headers[HttpKnownHeaderNames.ContentLength] = Constants.Zero;
}
_boundaryType = BoundaryType.ContentLength;
_expectedBodyLength = 0;
}
else if (keepConnectionAlive && requestVersion == Constants.V1_1)
{ {
// Then Transfer-Encoding: chunked
_boundaryType = BoundaryType.Chunked; _boundaryType = BoundaryType.Chunked;
} Headers[HttpKnownHeaderNames.TransferEncoding] = Constants.Chunked;
else if (endOfRequest)
{
// The request is ending without a body, add a Content-Length: 0 header.
Headers[HttpKnownHeaderNames.ContentLength] = "0";
_boundaryType = BoundaryType.ContentLength;
_contentLength = 0;
flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE;
} }
else else
{ {
// Then fall back to Connection:Close transparent mode. // The length cannot be determined, so we must close the connection
_boundaryType = BoundaryType.None; keepConnectionAlive = false;
flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE; // seems like HTTP_SEND_RESPONSE_FLAG_MORE_DATA but this hangs the app; _boundaryType = BoundaryType.Close;
if (responseVersion == Constants.V1_0)
{
keepAlive = false;
}
else
{
Headers[HttpKnownHeaderNames.TransferEncoding] = "chunked";
_boundaryType = BoundaryType.Chunked;
}
if (CanSendResponseBody(_requestContext.Response.StatusCode))
{
_contentLength = -1;
}
else
{
Headers[HttpKnownHeaderNames.ContentLength] = "0";
_contentLength = 0;
_boundaryType = BoundaryType.ContentLength;
}
} }
// Also, Keep-Alive vs Connection Close // Managed connection lifetime
if (!keepAlive) if (!keepConnectionAlive)
{ {
if (!closeSet) // All Http.Sys responses are v1.1, so use 1.1 response headers
// Note that if we don't add this header, Http.Sys will often do it for us.
if (!responseCloseSet)
{ {
Headers.Append(HttpKnownHeaderNames.Connection, "close"); Headers.Append(HttpKnownHeaderNames.Connection, Constants.Close);
}
if (flags == UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE)
{
flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT;
}
}
else
{
if (Request.ProtocolVersion.Minor == 0 && !keepAliveSet)
{
Headers[HttpKnownHeaderNames.KeepAlive] = "true";
} }
flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT;
} }
return flags; return flags;
} }
private static bool Matches(string knownValue, string input)
{
return string.Equals(knownValue, input?.Trim(), StringComparison.OrdinalIgnoreCase);
}
private List<GCHandle> SerializeHeaders(bool isOpaqueUpgrade) private List<GCHandle> SerializeHeaders(bool isOpaqueUpgrade)
{ {
Headers.Sent = true; // Prohibit further modifications. Headers.Sent = true; // Prohibit further modifications.

View File

@ -231,7 +231,7 @@ namespace Microsoft.Net.Http.Server
} }
else if (_requestContext.Response.BoundaryType == BoundaryType.ContentLength) else if (_requestContext.Response.BoundaryType == BoundaryType.ContentLength)
{ {
_leftToWrite = _requestContext.Response.CalculatedLength; _leftToWrite = _requestContext.Response.ExpectedBodyLength;
} }
else else
{ {
@ -764,7 +764,7 @@ namespace Microsoft.Net.Http.Server
if (_leftToWrite > 0 && !_inOpaqueMode) if (_leftToWrite > 0 && !_inOpaqueMode)
{ {
_requestContext.Abort(); _requestContext.Abort();
// TODO: Reduce this to a logged warning, it is thrown too late to be visible in user code. // 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."); LogHelper.LogError(_requestContext.Logger, "ResponseStream::Dispose", "Fewer bytes were written than were specified in the Content-Length.");
return; return;
} }
@ -775,9 +775,12 @@ namespace Microsoft.Net.Http.Server
} }
uint statusCode = 0; uint statusCode = 0;
if ((_requestContext.Response.BoundaryType == BoundaryType.Chunked || _requestContext.Response.BoundaryType == BoundaryType.None) && (String.Compare(_requestContext.Request.Method, "HEAD", StringComparison.OrdinalIgnoreCase) != 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.None) if (_requestContext.Response.BoundaryType == BoundaryType.Close)
{ {
flags |= UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT; flags |= UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT;
} }

View File

@ -99,28 +99,7 @@ namespace Microsoft.AspNet.Server.WebListener
Assert.Equal(new byte[30], await response.Content.ReadAsByteArrayAsync()); Assert.Equal(new byte[30], await response.Content.ReadAsByteArrayAsync());
} }
} }
/* TODO: response protocol
[Fact]
public async Task ResponseBody_Http10WriteNoHeaders_DefaultsConnectionClose()
{
string address;
using (Utilities.CreateHttpServer(out address, env =>
{
env["owin.ResponseProtocol"] = "HTTP/1.0";
env.Get<Stream>("owin.ResponseBody").Write(new byte[10], 0, 10);
return env.Get<Stream>("owin.ResponseBody").WriteAsync(new byte[10], 0, 10);
}))
{
HttpResponseMessage response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal(new Version(1, 1), response.Version); // Http.Sys won't transmit 1.0
IEnumerable<string> ignored;
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
Assert.Null(response.Headers.TransferEncodingChunked);
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
}
}
*/
[Fact] [Fact]
public async Task ResponseBody_WriteContentLengthNoneWritten_Throws() public async Task ResponseBody_WriteContentLengthNoneWritten_Throws()
{ {

View File

@ -16,9 +16,11 @@
// permissions and limitations under the License. // permissions and limitations under the License.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.FeatureModel;
using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Features;
@ -134,6 +136,7 @@ namespace Microsoft.AspNet.Server.WebListener
var responseInfo = httpContext.GetFeature<IHttpResponseFeature>(); var responseInfo = httpContext.GetFeature<IHttpResponseFeature>();
var responseHeaders = responseInfo.Headers; var responseHeaders = responseInfo.Headers;
responseHeaders["Connection"] = new string[] { "Close" }; responseHeaders["Connection"] = new string[] { "Close" };
httpContext.Response.Body.Flush(); // Http.Sys adds the Content-Length: header for us if we don't flush
return Task.FromResult(0); return Task.FromResult(0);
})) }))
{ {
@ -141,47 +144,12 @@ namespace Microsoft.AspNet.Server.WebListener
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
Assert.True(response.Headers.ConnectionClose.Value); Assert.True(response.Headers.ConnectionClose.Value);
Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection")); Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
}
}
/* TODO:
[Fact]
public async Task ResponseHeaders_SendsHttp10_Gets11Close()
{
string address;
using (Utilities.CreateHttpServer(out address, env =>
{
env["owin.ResponseProtocol"] = "HTTP/1.0";
return Task.FromResult(0);
}))
{
HttpResponseMessage response = await SendRequestAsync(address);
response.EnsureSuccessStatusCode();
Assert.Equal(new Version(1, 1), response.Version);
Assert.True(response.Headers.ConnectionClose.Value);
Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
}
}
[Fact]
public async Task ResponseHeaders_SendsHttp10WithBody_Gets11Close()
{
string address;
using (Utilities.CreateHttpServer(out address, env =>
{
env["owin.ResponseProtocol"] = "HTTP/1.0";
return env.Get<Stream>("owin.ResponseBody").WriteAsync(new byte[10], 0, 10);
}))
{
HttpResponseMessage response = await SendRequestAsync(address);
response.EnsureSuccessStatusCode();
Assert.Equal(new Version(1, 1), response.Version);
Assert.False(response.Headers.TransferEncodingChunked.HasValue); Assert.False(response.Headers.TransferEncodingChunked.HasValue);
Assert.False(response.Content.Headers.Contains("Content-Length")); IEnumerable<string> values;
Assert.True(response.Headers.ConnectionClose.Value); var result = response.Content.Headers.TryGetValues("Content-Length", out values);
Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection")); Assert.False(result);
} }
} }
*/
[Fact] [Fact]
public async Task ResponseHeaders_HTTP10Request_Gets11Close() public async Task ResponseHeaders_HTTP10Request_Gets11Close()
@ -201,12 +169,13 @@ namespace Microsoft.AspNet.Server.WebListener
Assert.Equal(new Version(1, 1), response.Version); Assert.Equal(new Version(1, 1), response.Version);
Assert.True(response.Headers.ConnectionClose.Value); Assert.True(response.Headers.ConnectionClose.Value);
Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection")); Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
Assert.False(response.Headers.TransferEncodingChunked.HasValue);
} }
} }
} }
[Fact] [Fact]
public async Task ResponseHeaders_HTTP10Request_RemovesChunkedHeader() public async Task ResponseHeaders_HTTP10RequestWithChunkedHeader_ManualChunking()
{ {
string address; string address;
using (Utilities.CreateHttpServer(out address, env => using (Utilities.CreateHttpServer(out address, env =>
@ -215,7 +184,8 @@ namespace Microsoft.AspNet.Server.WebListener
var responseInfo = httpContext.GetFeature<IHttpResponseFeature>(); var responseInfo = httpContext.GetFeature<IHttpResponseFeature>();
var responseHeaders = responseInfo.Headers; var responseHeaders = responseInfo.Headers;
responseHeaders["Transfer-Encoding"] = new string[] { "chunked" }; responseHeaders["Transfer-Encoding"] = new string[] { "chunked" };
return responseInfo.Body.WriteAsync(new byte[10], 0, 10); var responseBytes = Encoding.ASCII.GetBytes("10\r\nManually Chunked\r\n0\r\n\r\n");
return responseInfo.Body.WriteAsync(responseBytes, 0, responseBytes.Length);
})) }))
{ {
using (HttpClient client = new HttpClient()) using (HttpClient client = new HttpClient())
@ -225,10 +195,11 @@ namespace Microsoft.AspNet.Server.WebListener
HttpResponseMessage response = await client.SendAsync(request); HttpResponseMessage response = await client.SendAsync(request);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
Assert.Equal(new Version(1, 1), response.Version); Assert.Equal(new Version(1, 1), response.Version);
Assert.False(response.Headers.TransferEncodingChunked.HasValue); Assert.True(response.Headers.TransferEncodingChunked.HasValue);
Assert.False(response.Content.Headers.Contains("Content-Length")); Assert.False(response.Content.Headers.Contains("Content-Length"));
Assert.True(response.Headers.ConnectionClose.Value); Assert.True(response.Headers.ConnectionClose.Value);
Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection")); Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
Assert.Equal("Manually Chunked", await response.Content.ReadAsStringAsync());
} }
} }
} }

View File

@ -162,14 +162,13 @@ namespace Microsoft.AspNet.Server.WebListener
} }
[Fact] [Fact]
public async Task ResponseSendFile_Chunked_Chunked() public async Task ResponseSendFile_Unspecified_Chunked()
{ {
string address; string address;
using (Utilities.CreateHttpServer(out address, env => using (Utilities.CreateHttpServer(out address, env =>
{ {
var httpContext = new DefaultHttpContext((IFeatureCollection)env); var httpContext = new DefaultHttpContext((IFeatureCollection)env);
var sendFile = httpContext.GetFeature<IHttpSendFileFeature>(); var sendFile = httpContext.GetFeature<IHttpSendFileFeature>();
httpContext.Response.Headers["Transfer-EncodinG"] = "CHUNKED";
return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
})) }))
{ {
@ -183,14 +182,13 @@ namespace Microsoft.AspNet.Server.WebListener
} }
[Fact] [Fact]
public async Task ResponseSendFile_MultipleChunks_Chunked() public async Task ResponseSendFile_MultipleWrites_Chunked()
{ {
string address; string address;
using (Utilities.CreateHttpServer(out address, env => using (Utilities.CreateHttpServer(out address, env =>
{ {
var httpContext = new DefaultHttpContext((IFeatureCollection)env); var httpContext = new DefaultHttpContext((IFeatureCollection)env);
var sendFile = httpContext.GetFeature<IHttpSendFileFeature>(); var sendFile = httpContext.GetFeature<IHttpSendFileFeature>();
httpContext.Response.Headers["Transfer-EncodinG"] = "CHUNKED";
sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None).Wait(); sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None).Wait();
return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
})) }))
@ -205,7 +203,7 @@ namespace Microsoft.AspNet.Server.WebListener
} }
[Fact] [Fact]
public async Task ResponseSendFile_ChunkedHalfOfFile_Chunked() public async Task ResponseSendFile_HalfOfFile_Chunked()
{ {
string address; string address;
using (Utilities.CreateHttpServer(out address, env => using (Utilities.CreateHttpServer(out address, env =>
@ -225,7 +223,7 @@ namespace Microsoft.AspNet.Server.WebListener
} }
[Fact] [Fact]
public async Task ResponseSendFile_ChunkedOffsetOutOfRange_Throws() public async Task ResponseSendFile_OffsetOutOfRange_Throws()
{ {
string address; string address;
using (Utilities.CreateHttpServer(out address, env => using (Utilities.CreateHttpServer(out address, env =>
@ -241,7 +239,7 @@ namespace Microsoft.AspNet.Server.WebListener
} }
[Fact] [Fact]
public async Task ResponseSendFile_ChunkedCountOutOfRange_Throws() public async Task ResponseSendFile_CountOutOfRange_Throws()
{ {
string address; string address;
using (Utilities.CreateHttpServer(out address, env => using (Utilities.CreateHttpServer(out address, env =>
@ -257,7 +255,7 @@ namespace Microsoft.AspNet.Server.WebListener
} }
[Fact] [Fact]
public async Task ResponseSendFile_ChunkedCount0_Chunked() public async Task ResponseSendFile_Count0_Chunked()
{ {
string address; string address;
using (Utilities.CreateHttpServer(out address, env => using (Utilities.CreateHttpServer(out address, env =>

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit; using Xunit;
@ -37,7 +38,7 @@ namespace Microsoft.Net.Http.Server
} }
[Fact] [Fact]
public async Task ResponseBody_WriteChunked_Chunked() public async Task ResponseBody_WriteChunked_ManuallyChunked()
{ {
string address; string address;
using (var server = Utilities.CreateHttpServer(out address)) using (var server = Utilities.CreateHttpServer(out address))
@ -45,11 +46,10 @@ namespace Microsoft.Net.Http.Server
Task<HttpResponseMessage> responseTask = SendRequestAsync(address); Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync(); var context = await server.GetContextAsync();
context.Request.Headers["transfeR-Encoding"] = " CHunked "; context.Response.Headers["transfeR-Encoding"] = " CHunked ";
Stream stream = context.Response.Body; Stream stream = context.Response.Body;
stream.EndWrite(stream.BeginWrite(new byte[10], 0, 10, null, null)); var responseBytes = Encoding.ASCII.GetBytes("10\r\nManually Chunked\r\n0\r\n\r\n");
stream.Write(new byte[10], 0, 10); await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
await stream.WriteAsync(new byte[10], 0, 10);
context.Dispose(); context.Dispose();
HttpResponseMessage response = await responseTask; HttpResponseMessage response = await responseTask;
@ -58,7 +58,7 @@ namespace Microsoft.Net.Http.Server
IEnumerable<string> ignored; IEnumerable<string> ignored;
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
Assert.Equal(new byte[30], await response.Content.ReadAsByteArrayAsync()); Assert.Equal("Manually Chunked", await response.Content.ReadAsStringAsync());
} }
} }
@ -88,43 +88,28 @@ namespace Microsoft.Net.Http.Server
Assert.Equal(new byte[30], await response.Content.ReadAsByteArrayAsync()); Assert.Equal(new byte[30], await response.Content.ReadAsByteArrayAsync());
} }
} }
/* TODO: response protocol
[Fact] [Fact]
public async Task ResponseBody_Http10WriteNoHeaders_DefaultsConnectionClose() public async Task ResponseBody_WriteContentLengthNoneWritten_Aborts()
{ {
using (Utilities.CreateHttpServer(env => string address;
using (var server = Utilities.CreateHttpServer(out address))
{ {
env["owin.ResponseProtocol"] = "HTTP/1.0"; Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
env.Get<Stream>("owin.ResponseBody").Write(new byte[10], 0, 10);
return env.Get<Stream>("owin.ResponseBody").WriteAsync(new byte[10], 0, 10);
}))
{
HttpResponseMessage response = await SendRequestAsync(Address);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal(new Version(1, 1), response.Version); // Http.Sys won't transmit 1.0
IEnumerable<string> ignored;
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
Assert.Null(response.Headers.TransferEncodingChunked);
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
}
}
*/
/* TODO: Why does this test time out?
[Fact]
public async Task ResponseBody_WriteContentLengthNoneWritten_Throws()
{
using (var server = Utilities.CreateHttpServer())
{
Task<HttpResponseMessage> responseTask = SendRequestAsync(Address);
var context = await server.GetContextAsync(); var context = await server.GetContextAsync();
context.Response.Headers["Content-lenGth"] = new[] { " 20 " }; context.Response.Headers["Content-lenGth"] = " 20 ";
context.Dispose();
// HttpClient retries the request because it didn't get a response.
context = await server.GetContextAsync();
context.Response.Headers["Content-lenGth"] = " 20 ";
context.Dispose(); context.Dispose();
await Assert.ThrowsAsync<HttpRequestException>(() => responseTask); await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
} }
} }
*/
[Fact] [Fact]
public async Task ResponseBody_WriteContentLengthNotEnoughWritten_Throws() public async Task ResponseBody_WriteContentLengthNotEnoughWritten_Throws()
{ {

View File

@ -4,6 +4,7 @@ using System;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit; using Xunit;
@ -12,7 +13,7 @@ namespace Microsoft.Net.Http.Server
public class ResponseHeaderTests public class ResponseHeaderTests
{ {
[Fact] [Fact]
public async Task ResponseHeaders_ServerSendsDefaultHeaders_Success() public async Task ResponseHeaders_11Request_ServerSendsDefaultHeaders()
{ {
string address; string address;
using (var server = Utilities.CreateHttpServer(out address)) using (var server = Utilities.CreateHttpServer(out address))
@ -33,6 +34,143 @@ namespace Microsoft.Net.Http.Server
} }
} }
[Fact]
public async Task ResponseHeaders_10Request_ServerSendsDefaultHeaders()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
Task<HttpResponseMessage> responseTask = SendRequestAsync(address, usehttp11: false);
var context = await server.GetContextAsync();
context.Dispose();
HttpResponseMessage response = await responseTask;
response.EnsureSuccessStatusCode();
Assert.Equal(3, response.Headers.Count());
Assert.False(response.Headers.TransferEncodingChunked.HasValue);
Assert.True(response.Headers.ConnectionClose.Value);
Assert.True(response.Headers.Date.HasValue);
Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString());
Assert.Equal(1, response.Content.Headers.Count());
Assert.Equal(0, response.Content.Headers.ContentLength);
}
}
[Fact]
public async Task ResponseHeaders_11HeadRequest_ServerSendsDefaultHeaders()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
Task<HttpResponseMessage> responseTask = SendHeadRequestAsync(address);
var context = await server.GetContextAsync();
context.Dispose();
HttpResponseMessage response = await responseTask;
response.EnsureSuccessStatusCode();
Assert.Equal(3, response.Headers.Count());
Assert.True(response.Headers.TransferEncodingChunked.Value);
Assert.True(response.Headers.Date.HasValue);
Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString());
Assert.False(response.Content.Headers.Contains("Content-Length"));
Assert.Equal(0, response.Content.Headers.Count());
}
}
[Fact]
public async Task ResponseHeaders_10HeadRequest_ServerSendsDefaultHeaders()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
Task<HttpResponseMessage> responseTask = SendHeadRequestAsync(address, usehttp11: false);
var context = await server.GetContextAsync();
context.Dispose();
HttpResponseMessage response = await responseTask;
response.EnsureSuccessStatusCode();
Assert.Equal(3, response.Headers.Count());
Assert.False(response.Headers.TransferEncodingChunked.HasValue);
Assert.True(response.Headers.ConnectionClose.Value);
Assert.True(response.Headers.Date.HasValue);
Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString());
Assert.False(response.Content.Headers.Contains("Content-Length"));
Assert.Equal(0, response.Content.Headers.Count());
}
}
[Fact]
public async Task ResponseHeaders_11HeadRequestWithContentLength_Success()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
Task<HttpResponseMessage> responseTask = SendHeadRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.ContentLength = 20;
context.Dispose();
HttpResponseMessage response = await responseTask;
response.EnsureSuccessStatusCode();
Assert.Equal(2, response.Headers.Count());
Assert.False(response.Headers.TransferEncodingChunked.HasValue);
Assert.True(response.Headers.Date.HasValue);
Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString());
Assert.Equal(1, response.Content.Headers.Count());
Assert.Equal(20, response.Content.Headers.ContentLength);
}
}
[Fact]
public async Task ResponseHeaders_11RequestStatusCodeWithoutBody_NoContentLengthOrChunkedOrClose()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.StatusCode = 204; // No Content
context.Dispose();
HttpResponseMessage response = await responseTask;
response.EnsureSuccessStatusCode();
Assert.Equal(2, response.Headers.Count());
Assert.False(response.Headers.TransferEncodingChunked.HasValue);
Assert.True(response.Headers.Date.HasValue);
Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString());
Assert.False(response.Content.Headers.Contains("Content-Length"));
Assert.Equal(0, response.Content.Headers.Count());
}
}
[Fact]
public async Task ResponseHeaders_11HeadRequestStatusCodeWithoutBody_NoContentLengthOrChunkedOrClose()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
Task<HttpResponseMessage> responseTask = SendHeadRequestAsync(address);
var context = await server.GetContextAsync();
context.Response.StatusCode = 204; // No Content
context.Dispose();
HttpResponseMessage response = await responseTask;
response.EnsureSuccessStatusCode();
Assert.Equal(2, response.Headers.Count());
Assert.False(response.Headers.TransferEncodingChunked.HasValue);
Assert.True(response.Headers.Date.HasValue);
Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString());
Assert.False(response.Content.Headers.Contains("Content-Length"));
Assert.Equal(0, response.Content.Headers.Count());
}
}
[Fact] [Fact]
public async Task ResponseHeaders_ServerSendsSingleValueKnownHeaders_Success() public async Task ResponseHeaders_ServerSendsSingleValueKnownHeaders_Success()
{ {
@ -127,43 +265,6 @@ namespace Microsoft.Net.Http.Server
Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection")); Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
} }
} }
/* TODO:
[Fact]
public async Task ResponseHeaders_SendsHttp10_Gets11Close()
{
using (Utilities.CreateHttpServer(env =>
{
env["owin.ResponseProtocol"] = "HTTP/1.0";
return Task.FromResult(0);
}))
{
HttpResponseMessage response = await SendRequestAsync(Address);
response.EnsureSuccessStatusCode();
Assert.Equal(new Version(1, 1), response.Version);
Assert.True(response.Headers.ConnectionClose.Value);
Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
}
}
[Fact]
public async Task ResponseHeaders_SendsHttp10WithBody_Gets11Close()
{
using (Utilities.CreateHttpServer(env =>
{
env["owin.ResponseProtocol"] = "HTTP/1.0";
return env.Get<Stream>("owin.ResponseBody").WriteAsync(new byte[10], 0, 10);
}))
{
HttpResponseMessage response = await SendRequestAsync(Address);
response.EnsureSuccessStatusCode();
Assert.Equal(new Version(1, 1), response.Version);
Assert.False(response.Headers.TransferEncodingChunked.HasValue);
Assert.False(response.Content.Headers.Contains("Content-Length"));
Assert.True(response.Headers.ConnectionClose.Value);
Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
}
}
*/
[Fact] [Fact]
public async Task ResponseHeaders_HTTP10Request_Gets11Close() public async Task ResponseHeaders_HTTP10Request_Gets11Close()
@ -171,26 +272,21 @@ namespace Microsoft.Net.Http.Server
string address; string address;
using (var server = Utilities.CreateHttpServer(out address)) using (var server = Utilities.CreateHttpServer(out address))
{ {
using (HttpClient client = new HttpClient()) Task<HttpResponseMessage> responseTask = SendRequestAsync(address, usehttp11: false);
{
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, address);
request.Version = new Version(1, 0);
Task<HttpResponseMessage> responseTask = client.SendAsync(request);
var context = await server.GetContextAsync(); var context = await server.GetContextAsync();
context.Dispose(); context.Dispose();
HttpResponseMessage response = await responseTask; HttpResponseMessage response = await responseTask;
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
Assert.Equal(new Version(1, 1), response.Version); Assert.Equal(new Version(1, 1), response.Version);
Assert.True(response.Headers.ConnectionClose.Value); Assert.True(response.Headers.ConnectionClose.Value);
Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection")); Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
}
} }
} }
[Fact] [Fact]
public async Task ResponseHeaders_HTTP10Request_RemovesChunkedHeader() public async Task ResponseHeaders_HTTP10Request_AllowsManualChunking()
{ {
string address; string address;
using (var server = Utilities.CreateHttpServer(out address)) using (var server = Utilities.CreateHttpServer(out address))
@ -204,20 +300,41 @@ namespace Microsoft.Net.Http.Server
var context = await server.GetContextAsync(); var context = await server.GetContextAsync();
var responseHeaders = context.Response.Headers; var responseHeaders = context.Response.Headers;
responseHeaders["Transfer-Encoding"] = "chunked"; responseHeaders["Transfer-Encoding"] = "chunked";
await context.Response.Body.WriteAsync(new byte[10], 0, 10); var responseBytes = Encoding.ASCII.GetBytes("10\r\nManually Chunked\r\n0\r\n\r\n");
await context.Response.Body.WriteAsync(responseBytes, 0, responseBytes.Length);
context.Dispose(); context.Dispose();
HttpResponseMessage response = await responseTask; HttpResponseMessage response = await responseTask;
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
Assert.Equal(new Version(1, 1), response.Version); Assert.Equal(new Version(1, 1), response.Version);
Assert.False(response.Headers.TransferEncodingChunked.HasValue); Assert.True(response.Headers.TransferEncodingChunked.Value);
Assert.False(response.Content.Headers.Contains("Content-Length")); Assert.False(response.Content.Headers.Contains("Content-Length"));
Assert.True(response.Headers.ConnectionClose.Value); Assert.True(response.Headers.ConnectionClose.Value);
Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection")); Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
Assert.Equal("Manually Chunked", await response.Content.ReadAsStringAsync());
} }
} }
} }
[Fact]
public async Task ResponseHeaders_HTTP10KeepAliveRequest_Gets11Close()
{
string address;
using (var server = Utilities.CreateHttpServer(out address))
{
// Http.Sys does not support 1.0 keep-alives.
Task<HttpResponseMessage> responseTask = SendRequestAsync(address, usehttp11: false, sendKeepAlive: true);
var context = await server.GetContextAsync();
context.Dispose();
HttpResponseMessage response = await responseTask;
response.EnsureSuccessStatusCode();
Assert.Equal(new Version(1, 1), response.Version);
Assert.True(response.Headers.ConnectionClose.Value);
}
}
[Fact] [Fact]
public async Task Headers_FlushSendsHeaders_Success() public async Task Headers_FlushSendsHeaders_Success()
{ {
@ -290,11 +407,33 @@ namespace Microsoft.Net.Http.Server
} }
} }
private async Task<HttpResponseMessage> SendRequestAsync(string uri) private async Task<HttpResponseMessage> SendRequestAsync(string uri, bool usehttp11 = true, bool sendKeepAlive = false)
{ {
using (HttpClient client = new HttpClient()) using (HttpClient client = new HttpClient())
{ {
return await client.GetAsync(uri); var request = new HttpRequestMessage(HttpMethod.Get, uri);
if (!usehttp11)
{
request.Version = new Version(1, 0);
}
if (sendKeepAlive)
{
request.Headers.Add("Connection", "Keep-Alive");
}
return await client.SendAsync(request);
}
}
private async Task<HttpResponseMessage> SendHeadRequestAsync(string uri, bool usehttp11 = true)
{
using (HttpClient client = new HttpClient())
{
var request = new HttpRequestMessage(HttpMethod.Head, uri);
if (!usehttp11)
{
request.Version = new Version(1, 0);
}
return await client.SendAsync(request);
} }
} }
} }

View File

@ -84,7 +84,7 @@ namespace Microsoft.Net.Http.Server
} }
[Fact] [Fact]
public async Task ResponseSendFile_Chunked_Chunked() public async Task ResponseSendFile_Unspecified_Chunked()
{ {
string address; string address;
using (var server = Utilities.CreateHttpServer(out address)) using (var server = Utilities.CreateHttpServer(out address))
@ -92,7 +92,6 @@ namespace Microsoft.Net.Http.Server
Task<HttpResponseMessage> responseTask = SendRequestAsync(address); Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync(); var context = await server.GetContextAsync();
context.Response.Headers["Transfer-EncodinG"] = "CHUNKED";
await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
context.Dispose(); context.Dispose();
@ -106,7 +105,7 @@ namespace Microsoft.Net.Http.Server
} }
[Fact] [Fact]
public async Task ResponseSendFile_MultipleChunks_Chunked() public async Task ResponseSendFile_MultipleWrites_Chunked()
{ {
string address; string address;
using (var server = Utilities.CreateHttpServer(out address)) using (var server = Utilities.CreateHttpServer(out address))
@ -114,7 +113,6 @@ namespace Microsoft.Net.Http.Server
Task<HttpResponseMessage> responseTask = SendRequestAsync(address); Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
var context = await server.GetContextAsync(); var context = await server.GetContextAsync();
context.Response.Headers["Transfer-EncodinG"] = "CHUNKED";
await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
context.Dispose(); context.Dispose();
@ -129,7 +127,7 @@ namespace Microsoft.Net.Http.Server
} }
[Fact] [Fact]
public async Task ResponseSendFile_ChunkedHalfOfFile_Chunked() public async Task ResponseSendFile_HalfOfFile_Chunked()
{ {
string address; string address;
using (var server = Utilities.CreateHttpServer(out address)) using (var server = Utilities.CreateHttpServer(out address))
@ -150,7 +148,7 @@ namespace Microsoft.Net.Http.Server
} }
[Fact] [Fact]
public async Task ResponseSendFile_ChunkedOffsetOutOfRange_Throws() public async Task ResponseSendFile_OffsetOutOfRange_Throws()
{ {
string address; string address;
using (var server = Utilities.CreateHttpServer(out address)) using (var server = Utilities.CreateHttpServer(out address))
@ -167,7 +165,7 @@ namespace Microsoft.Net.Http.Server
} }
[Fact] [Fact]
public async Task ResponseSendFile_ChunkedCountOutOfRange_Throws() public async Task ResponseSendFile_CountOutOfRange_Throws()
{ {
string address; string address;
using (var server = Utilities.CreateHttpServer(out address)) using (var server = Utilities.CreateHttpServer(out address))
@ -184,7 +182,7 @@ namespace Microsoft.Net.Http.Server
} }
[Fact] [Fact]
public async Task ResponseSendFile_ChunkedCount0_Chunked() public async Task ResponseSendFile_Count0_Chunked()
{ {
string address; string address;
using (var server = Utilities.CreateHttpServer(out address)) using (var server = Utilities.CreateHttpServer(out address))