From 2681e8b3d138eb23f5c5aa8d1cdfb0dc6b895642 Mon Sep 17 00:00:00 2001 From: Chris R Date: Fri, 15 May 2015 14:55:54 -0700 Subject: [PATCH] #112, #113 Sort out default response modes, allow manual chunking. --- src/Microsoft.Net.Http.Server/Constants.cs | 3 + .../RequestProcessing/BoundaryType.cs | 4 +- .../RequestProcessing/Request.cs | 7 +- .../RequestProcessing/Response.cs | 198 +++++--------- .../RequestProcessing/ResponseStream.cs | 11 +- .../ResponseBodyTests.cs | 23 +- .../ResponseHeaderTests.cs | 53 +--- .../ResponseSendFileTests.cs | 14 +- .../ResponseBodyTests.cs | 51 ++-- .../ResponseHeaderTests.cs | 251 ++++++++++++++---- .../ResponseSendFileTests.cs | 14 +- 11 files changed, 320 insertions(+), 309 deletions(-) diff --git a/src/Microsoft.Net.Http.Server/Constants.cs b/src/Microsoft.Net.Http.Server/Constants.cs index e3336f6bea..4991637282 100644 --- a/src/Microsoft.Net.Http.Server/Constants.cs +++ b/src/Microsoft.Net.Http.Server/Constants.cs @@ -29,6 +29,9 @@ namespace Microsoft.Net.Http.Server { internal const string HttpScheme = "http"; 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 static Version V1_0 = new Version(1, 0); diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/BoundaryType.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/BoundaryType.cs index b45b24b658..f2b4e78568 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/BoundaryType.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/BoundaryType.cs @@ -28,6 +28,8 @@ namespace Microsoft.Net.Http.Server None = 0, Chunked = 1, // Transfer-Encoding: chunked 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, } } diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/Request.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/Request.cs index 9519037ca8..668cc6dfea 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/Request.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/Request.cs @@ -263,6 +263,11 @@ namespace Microsoft.Net.Http.Server get { return _httpMethod; } } + public bool IsHeadMethod + { + get { return string.Equals(_httpMethod, "HEAD", StringComparison.OrdinalIgnoreCase); } + } + public Stream Body { get @@ -413,7 +418,7 @@ namespace Microsoft.Net.Http.Server { get { - return Headers.Get(HttpKnownHeaderNames.ContentLength); + return Headers.Get(HttpKnownHeaderNames.ContentType); } } diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs index 3d19df6781..d0cf0c98a7 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs @@ -38,13 +38,13 @@ namespace Microsoft.Net.Http.Server { public sealed unsafe class Response { - private static readonly string[] ZeroContentLength = new[] { "0" }; + private static readonly string[] ZeroContentLength = new[] { Constants.Zero }; private ResponseState _responseState; private HeaderCollection _headers; private string _reasonPhrase; private ResponseStream _nativeStream; - private long _contentLength; + private long _expectedBodyLength; private BoundaryType _boundaryType; private UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2 _nativeResponse; private IList, object>> _onSendingHeadersActions; @@ -166,11 +166,11 @@ namespace Microsoft.Net.Http.Server get { return _headers; } } - internal long CalculatedLength + internal long ExpectedBodyLength { get { - return _contentLength; + return _expectedBodyLength; } } @@ -184,7 +184,7 @@ namespace Microsoft.Net.Http.Server if (!string.IsNullOrWhiteSpace(contentLengthString)) { contentLengthString = contentLengthString.Trim(); - if (string.Equals("0", contentLengthString, StringComparison.Ordinal)) + if (string.Equals(Constants.Zero, contentLengthString, StringComparison.Ordinal)) { return 0; } @@ -225,7 +225,7 @@ namespace Microsoft.Net.Http.Server { get { - return Headers.Get(HttpKnownHeaderNames.ContentLength); + return Headers.Get(HttpKnownHeaderNames.ContentType); } 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(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 internal void Dispose() { @@ -476,121 +438,87 @@ namespace Microsoft.Net.Http.Server 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."); _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. - Version responseVersion = GetProtocolVersion(); - _nativeResponse.Response_V1.Version.MajorVersion = (ushort)responseVersion.Major; - _nativeResponse.Response_V1.Version.MinorVersion = (ushort)responseVersion.Minor; - bool keepAlive = responseVersion >= Constants.V1_1; - string connectionString = Headers.Get(HttpKnownHeaderNames.Connection); - string keepAliveString = Headers.Get(HttpKnownHeaderNames.KeepAlive); - bool closeSet = false; - bool keepAliveSet = false; + // Gather everything from the request that affects the response: + var requestVersion = Request.ProtocolVersion; + var requestConnectionString = Request.Headers.Get(HttpKnownHeaderNames.Connection); + var isHeadRequest = Request.IsHeadMethod; + var requestCloseSet = Matches(Constants.Close, requestConnectionString); - 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; - closeSet = true; - } - else if (!string.IsNullOrWhiteSpace(keepAliveString) && string.Equals("true", keepAliveString.Trim(), StringComparison.OrdinalIgnoreCase)) - { - keepAlive = true; - keepAliveSet = true; + keepConnectionAlive = false; } - // Content-Length takes priority - long? contentLength = ContentLength; - string transferEncodingString = Headers.Get(HttpKnownHeaderNames.TransferEncoding); - - if (responseVersion == Constants.V1_0 && !string.IsNullOrEmpty(transferEncodingString) - && string.Equals("chunked", transferEncodingString.Trim(), StringComparison.OrdinalIgnoreCase)) + // Determine the body format. If the user asks to do something, let them, otherwise choose a good default for the scenario. + if (responseContentLength.HasValue) { - // A 1.0 client can't process chunked responses. - Headers.Remove(HttpKnownHeaderNames.TransferEncoding); - transferEncodingString = null; - } - - if (contentLength.HasValue) - { - _contentLength = contentLength.Value; _boundaryType = BoundaryType.ContentLength; - if (_contentLength == 0) - { - flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE; - } + // ComputeLeftToWrite checks for HEAD requests when setting _leftToWrite + _expectedBodyLength = responseContentLength.Value; } - else if (!string.IsNullOrWhiteSpace(transferEncodingString) - && string.Equals("chunked", transferEncodingString.Trim(), StringComparison.OrdinalIgnoreCase)) + else if (responseChunkedSet) + { + // 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; - } - 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; + Headers[HttpKnownHeaderNames.TransferEncoding] = Constants.Chunked; } else { - // Then fall back to Connection:Close transparent mode. - _boundaryType = BoundaryType.None; - flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE; // seems like HTTP_SEND_RESPONSE_FLAG_MORE_DATA but this hangs the app; - 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; - } + // The length cannot be determined, so we must close the connection + keepConnectionAlive = false; + _boundaryType = BoundaryType.Close; } - // Also, Keep-Alive vs Connection Close - if (!keepAlive) + // Managed connection lifetime + 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"); - } - 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"; + Headers.Append(HttpKnownHeaderNames.Connection, Constants.Close); } + flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT; } + return flags; } + private static bool Matches(string knownValue, string input) + { + return string.Equals(knownValue, input?.Trim(), StringComparison.OrdinalIgnoreCase); + } + private List SerializeHeaders(bool isOpaqueUpgrade) { Headers.Sent = true; // Prohibit further modifications. diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs index feec8feb6f..2ce897c414 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs @@ -231,7 +231,7 @@ namespace Microsoft.Net.Http.Server } else if (_requestContext.Response.BoundaryType == BoundaryType.ContentLength) { - _leftToWrite = _requestContext.Response.CalculatedLength; + _leftToWrite = _requestContext.Response.ExpectedBodyLength; } else { @@ -764,7 +764,7 @@ namespace Microsoft.Net.Http.Server if (_leftToWrite > 0 && !_inOpaqueMode) { _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."); return; } @@ -775,9 +775,12 @@ namespace Microsoft.Net.Http.Server } 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; } diff --git a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseBodyTests.cs b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseBodyTests.cs index 38d91a330a..7f77ff1880 100644 --- a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseBodyTests.cs +++ b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseBodyTests.cs @@ -99,28 +99,7 @@ namespace Microsoft.AspNet.Server.WebListener 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("owin.ResponseBody").Write(new byte[10], 0, 10); - return env.Get("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 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] public async Task ResponseBody_WriteContentLengthNoneWritten_Throws() { diff --git a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseHeaderTests.cs b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseHeaderTests.cs index bf73c78f94..5d7cc5234a 100644 --- a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseHeaderTests.cs +++ b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseHeaderTests.cs @@ -16,9 +16,11 @@ // permissions and limitations under the License. using System; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.Http.Features; @@ -134,6 +136,7 @@ namespace Microsoft.AspNet.Server.WebListener var responseInfo = httpContext.GetFeature(); var responseHeaders = responseInfo.Headers; 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); })) { @@ -141,47 +144,12 @@ namespace Microsoft.AspNet.Server.WebListener response.EnsureSuccessStatusCode(); Assert.True(response.Headers.ConnectionClose.Value); 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("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")); + IEnumerable values; + var result = response.Content.Headers.TryGetValues("Content-Length", out values); + Assert.False(result); } } - */ [Fact] public async Task ResponseHeaders_HTTP10Request_Gets11Close() @@ -201,12 +169,13 @@ namespace Microsoft.AspNet.Server.WebListener Assert.Equal(new Version(1, 1), response.Version); Assert.True(response.Headers.ConnectionClose.Value); Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection")); + Assert.False(response.Headers.TransferEncodingChunked.HasValue); } } } [Fact] - public async Task ResponseHeaders_HTTP10Request_RemovesChunkedHeader() + public async Task ResponseHeaders_HTTP10RequestWithChunkedHeader_ManualChunking() { string address; using (Utilities.CreateHttpServer(out address, env => @@ -215,7 +184,8 @@ namespace Microsoft.AspNet.Server.WebListener var responseInfo = httpContext.GetFeature(); var responseHeaders = responseInfo.Headers; 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()) @@ -225,10 +195,11 @@ namespace Microsoft.AspNet.Server.WebListener HttpResponseMessage response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); 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.True(response.Headers.ConnectionClose.Value); Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection")); + Assert.Equal("Manually Chunked", await response.Content.ReadAsStringAsync()); } } } diff --git a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseSendFileTests.cs b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseSendFileTests.cs index 58fbf286b5..4814537415 100644 --- a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseSendFileTests.cs +++ b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/ResponseSendFileTests.cs @@ -162,14 +162,13 @@ namespace Microsoft.AspNet.Server.WebListener } [Fact] - public async Task ResponseSendFile_Chunked_Chunked() + public async Task ResponseSendFile_Unspecified_Chunked() { string address; using (Utilities.CreateHttpServer(out address, env => { var httpContext = new DefaultHttpContext((IFeatureCollection)env); var sendFile = httpContext.GetFeature(); - httpContext.Response.Headers["Transfer-EncodinG"] = "CHUNKED"; return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); })) { @@ -183,14 +182,13 @@ namespace Microsoft.AspNet.Server.WebListener } [Fact] - public async Task ResponseSendFile_MultipleChunks_Chunked() + public async Task ResponseSendFile_MultipleWrites_Chunked() { string address; using (Utilities.CreateHttpServer(out address, env => { var httpContext = new DefaultHttpContext((IFeatureCollection)env); var sendFile = httpContext.GetFeature(); - httpContext.Response.Headers["Transfer-EncodinG"] = "CHUNKED"; sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None).Wait(); return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); })) @@ -205,7 +203,7 @@ namespace Microsoft.AspNet.Server.WebListener } [Fact] - public async Task ResponseSendFile_ChunkedHalfOfFile_Chunked() + public async Task ResponseSendFile_HalfOfFile_Chunked() { string address; using (Utilities.CreateHttpServer(out address, env => @@ -225,7 +223,7 @@ namespace Microsoft.AspNet.Server.WebListener } [Fact] - public async Task ResponseSendFile_ChunkedOffsetOutOfRange_Throws() + public async Task ResponseSendFile_OffsetOutOfRange_Throws() { string address; using (Utilities.CreateHttpServer(out address, env => @@ -241,7 +239,7 @@ namespace Microsoft.AspNet.Server.WebListener } [Fact] - public async Task ResponseSendFile_ChunkedCountOutOfRange_Throws() + public async Task ResponseSendFile_CountOutOfRange_Throws() { string address; using (Utilities.CreateHttpServer(out address, env => @@ -257,7 +255,7 @@ namespace Microsoft.AspNet.Server.WebListener } [Fact] - public async Task ResponseSendFile_ChunkedCount0_Chunked() + public async Task ResponseSendFile_Count0_Chunked() { string address; using (Utilities.CreateHttpServer(out address, env => diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs index b76bda6db0..779b7080fb 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseBodyTests.cs @@ -5,6 +5,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 Xunit; @@ -37,7 +38,7 @@ namespace Microsoft.Net.Http.Server } [Fact] - public async Task ResponseBody_WriteChunked_Chunked() + public async Task ResponseBody_WriteChunked_ManuallyChunked() { string address; using (var server = Utilities.CreateHttpServer(out address)) @@ -45,11 +46,10 @@ namespace Microsoft.Net.Http.Server Task responseTask = SendRequestAsync(address); var context = await server.GetContextAsync(); - context.Request.Headers["transfeR-Encoding"] = " CHunked "; + context.Response.Headers["transfeR-Encoding"] = " CHunked "; Stream stream = context.Response.Body; - stream.EndWrite(stream.BeginWrite(new byte[10], 0, 10, null, null)); - stream.Write(new byte[10], 0, 10); - await stream.WriteAsync(new byte[10], 0, 10); + var responseBytes = Encoding.ASCII.GetBytes("10\r\nManually Chunked\r\n0\r\n\r\n"); + await stream.WriteAsync(responseBytes, 0, responseBytes.Length); context.Dispose(); HttpResponseMessage response = await responseTask; @@ -58,7 +58,7 @@ namespace Microsoft.Net.Http.Server IEnumerable ignored; Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length"); Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked"); - Assert.Equal(new byte[30], await response.Content.ReadAsByteArrayAsync()); + Assert.Equal("Manually Chunked", await response.Content.ReadAsStringAsync()); } } @@ -88,43 +88,28 @@ namespace Microsoft.Net.Http.Server Assert.Equal(new byte[30], await response.Content.ReadAsByteArrayAsync()); } } - /* TODO: response protocol + [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"; - env.Get("owin.ResponseBody").Write(new byte[10], 0, 10); - return env.Get("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 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 responseTask = SendRequestAsync(Address); + Task responseTask = SendRequestAsync(address); 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(); await Assert.ThrowsAsync(() => responseTask); } } - */ + [Fact] public async Task ResponseBody_WriteContentLengthNotEnoughWritten_Throws() { diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseHeaderTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseHeaderTests.cs index 2fc83a613e..f16bc98142 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseHeaderTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseHeaderTests.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Net; using System.Net.Http; +using System.Text; using System.Threading.Tasks; using Xunit; @@ -12,7 +13,7 @@ namespace Microsoft.Net.Http.Server public class ResponseHeaderTests { [Fact] - public async Task ResponseHeaders_ServerSendsDefaultHeaders_Success() + public async Task ResponseHeaders_11Request_ServerSendsDefaultHeaders() { string 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 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 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 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 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 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 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] public async Task ResponseHeaders_ServerSendsSingleValueKnownHeaders_Success() { @@ -127,43 +265,6 @@ namespace Microsoft.Net.Http.Server 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("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] public async Task ResponseHeaders_HTTP10Request_Gets11Close() @@ -171,26 +272,21 @@ namespace Microsoft.Net.Http.Server string address; using (var server = Utilities.CreateHttpServer(out address)) { - using (HttpClient client = new HttpClient()) - { - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, address); - request.Version = new Version(1, 0); - Task responseTask = client.SendAsync(request); + Task responseTask = SendRequestAsync(address, usehttp11: false); - var context = await server.GetContextAsync(); - context.Dispose(); + 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); - Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection")); - } + HttpResponseMessage response = await responseTask; + 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_HTTP10Request_RemovesChunkedHeader() + public async Task ResponseHeaders_HTTP10Request_AllowsManualChunking() { string address; using (var server = Utilities.CreateHttpServer(out address)) @@ -204,20 +300,41 @@ namespace Microsoft.Net.Http.Server var context = await server.GetContextAsync(); var responseHeaders = context.Response.Headers; 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(); HttpResponseMessage response = await responseTask; response.EnsureSuccessStatusCode(); 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.True(response.Headers.ConnectionClose.Value); 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 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] public async Task Headers_FlushSendsHeaders_Success() { @@ -290,11 +407,33 @@ namespace Microsoft.Net.Http.Server } } - private async Task SendRequestAsync(string uri) + private async Task SendRequestAsync(string uri, bool usehttp11 = true, bool sendKeepAlive = false) { 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 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); } } } diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs index f13af46cf1..b81fd6d514 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseSendFileTests.cs @@ -84,7 +84,7 @@ namespace Microsoft.Net.Http.Server } [Fact] - public async Task ResponseSendFile_Chunked_Chunked() + public async Task ResponseSendFile_Unspecified_Chunked() { string address; using (var server = Utilities.CreateHttpServer(out address)) @@ -92,7 +92,6 @@ namespace Microsoft.Net.Http.Server Task responseTask = SendRequestAsync(address); var context = await server.GetContextAsync(); - context.Response.Headers["Transfer-EncodinG"] = "CHUNKED"; await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None); context.Dispose(); @@ -106,7 +105,7 @@ namespace Microsoft.Net.Http.Server } [Fact] - public async Task ResponseSendFile_MultipleChunks_Chunked() + public async Task ResponseSendFile_MultipleWrites_Chunked() { string address; using (var server = Utilities.CreateHttpServer(out address)) @@ -114,7 +113,6 @@ namespace Microsoft.Net.Http.Server Task responseTask = SendRequestAsync(address); 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(); @@ -129,7 +127,7 @@ namespace Microsoft.Net.Http.Server } [Fact] - public async Task ResponseSendFile_ChunkedHalfOfFile_Chunked() + public async Task ResponseSendFile_HalfOfFile_Chunked() { string address; using (var server = Utilities.CreateHttpServer(out address)) @@ -150,7 +148,7 @@ namespace Microsoft.Net.Http.Server } [Fact] - public async Task ResponseSendFile_ChunkedOffsetOutOfRange_Throws() + public async Task ResponseSendFile_OffsetOutOfRange_Throws() { string address; using (var server = Utilities.CreateHttpServer(out address)) @@ -167,7 +165,7 @@ namespace Microsoft.Net.Http.Server } [Fact] - public async Task ResponseSendFile_ChunkedCountOutOfRange_Throws() + public async Task ResponseSendFile_CountOutOfRange_Throws() { string address; using (var server = Utilities.CreateHttpServer(out address)) @@ -184,7 +182,7 @@ namespace Microsoft.Net.Http.Server } [Fact] - public async Task ResponseSendFile_ChunkedCount0_Chunked() + public async Task ResponseSendFile_Count0_Chunked() { string address; using (var server = Utilities.CreateHttpServer(out address))