#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
Родитель 748a6e1090
Коммит 2681e8b3d1
11 изменённых файлов: 320 добавлений и 309 удалений

Просмотреть файл

@ -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);

Просмотреть файл

@ -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,
}
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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<Tuple<Action<object>, 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<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
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<GCHandle> SerializeHeaders(bool isOpaqueUpgrade)
{
Headers.Sent = true; // Prohibit further modifications.

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -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<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]
public async Task ResponseBody_WriteContentLengthNoneWritten_Throws()
{

Просмотреть файл

@ -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<IHttpResponseFeature>();
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<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"));
IEnumerable<string> 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<IHttpResponseFeature>();
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());
}
}
}

Просмотреть файл

@ -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<IHttpSendFileFeature>();
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<IHttpSendFileFeature>();
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 =>

Просмотреть файл

@ -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<HttpResponseMessage> 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<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("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<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);
Task<HttpResponseMessage> 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<HttpRequestException>(() => responseTask);
}
}
*/
[Fact]
public async Task ResponseBody_WriteContentLengthNotEnoughWritten_Throws()
{

Просмотреть файл

@ -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<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]
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<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]
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<HttpResponseMessage> responseTask = client.SendAsync(request);
Task<HttpResponseMessage> 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<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]
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())
{
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);
}
}
}

Просмотреть файл

@ -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<HttpResponseMessage> 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<HttpResponseMessage> 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))