Support multi-value response headers.

This commit is contained in:
Chris Ross 2014-03-12 08:28:45 -07:00
parent fd510db13c
commit 55ad9ab17a
4 changed files with 182 additions and 115 deletions

View File

@ -109,7 +109,7 @@ namespace Microsoft.AspNet.Server.WebListener
internal static extern uint HttpReceiveHttpRequest(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_REQUEST* pRequestBuffer, uint requestBufferLength, uint* pBytesReturned, SafeNativeOverlapped pOverlapped);
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
internal static extern uint HttpSendHttpResponse(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_RESPONSE* pHttpResponse, void* pCachePolicy, uint* pBytesSent, SafeLocalFree pRequestBuffer, uint requestBufferLength, SafeNativeOverlapped pOverlapped, IntPtr pLogData);
internal static extern uint HttpSendHttpResponse(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_RESPONSE_V2* pHttpResponse, void* pCachePolicy, uint* pBytesSent, SafeLocalFree pRequestBuffer, uint requestBufferLength, SafeNativeOverlapped pOverlapped, IntPtr pLogData);
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
internal static extern uint HttpSendResponseEntityBody(SafeHandle requestQueueHandle, ulong requestId, uint flags, ushort entityChunkCount, HTTP_DATA_CHUNK* pEntityChunks, uint* pBytesSent, SafeLocalFree pRequestBuffer, uint requestBufferLength, SafeNativeOverlapped pOverlapped, IntPtr pLogData);
@ -495,7 +495,7 @@ namespace Microsoft.AspNet.Server.WebListener
{
internal HTTP_RESPONSE_INFO_TYPE Type;
internal uint Length;
internal void* pInfo;
internal HTTP_MULTIPLE_KNOWN_HEADERS* pInfo;
}
[StructLayout(LayoutKind.Sequential)]
@ -509,10 +509,31 @@ namespace Microsoft.AspNet.Server.WebListener
internal HTTP_RESPONSE_HEADERS Headers;
internal ushort EntityChunkCount;
internal HTTP_DATA_CHUNK* pEntityChunks;
}
[StructLayout(LayoutKind.Sequential)]
internal struct HTTP_RESPONSE_V2
{
internal HTTP_RESPONSE Response_V1;
internal ushort ResponseInfoCount;
internal HTTP_RESPONSE_INFO* pResponseInfo;
}
internal enum HTTP_RESPONSE_INFO_FLAGS : uint
{
None = 0,
PreserveOrder = 1,
}
[StructLayout(LayoutKind.Sequential)]
internal struct HTTP_MULTIPLE_KNOWN_HEADERS
{
internal HTTP_RESPONSE_HEADER_ID.Enum HeaderId;
internal HTTP_RESPONSE_INFO_FLAGS Flags;
internal ushort KnownHeaderCount;
internal HTTP_KNOWN_HEADER* KnownHeaders;
}
[StructLayout(LayoutKind.Sequential)]
internal struct HTTP_REQUEST_AUTH_INFO
{

View File

@ -767,26 +767,26 @@ namespace Microsoft.AspNet.Server.WebListener
private unsafe void SendError(ulong requestId, HttpStatusCode httpStatusCode)
{
UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE httpResponse = new UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE();
httpResponse.Version = new UnsafeNclNativeMethods.HttpApi.HTTP_VERSION();
httpResponse.Version.MajorVersion = (ushort)1;
httpResponse.Version.MinorVersion = (ushort)1;
httpResponse.StatusCode = (ushort)httpStatusCode;
UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2 httpResponse = new UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2();
httpResponse.Response_V1.Version = new UnsafeNclNativeMethods.HttpApi.HTTP_VERSION();
httpResponse.Response_V1.Version.MajorVersion = (ushort)1;
httpResponse.Response_V1.Version.MinorVersion = (ushort)1;
httpResponse.Response_V1.StatusCode = (ushort)httpStatusCode;
string statusDescription = HttpReasonPhrase.Get(httpStatusCode);
uint dataWritten = 0;
uint statusCode;
byte[] byteReason = HeaderEncoding.GetBytes(statusDescription);
fixed (byte* pReason = byteReason)
{
httpResponse.pReason = (sbyte*)pReason;
httpResponse.ReasonLength = (ushort)byteReason.Length;
httpResponse.Response_V1.pReason = (sbyte*)pReason;
httpResponse.Response_V1.ReasonLength = (ushort)byteReason.Length;
byte[] byteContentLength = new byte[] { (byte)'0' };
fixed (byte* pContentLength = byteContentLength)
{
(&httpResponse.Headers.KnownHeaders)[(int)HttpSysResponseHeader.ContentLength].pRawValue = (sbyte*)pContentLength;
(&httpResponse.Headers.KnownHeaders)[(int)HttpSysResponseHeader.ContentLength].RawValueLength = (ushort)byteContentLength.Length;
httpResponse.Headers.UnknownHeaderCount = 0;
(&httpResponse.Response_V1.Headers.KnownHeaders)[(int)HttpSysResponseHeader.ContentLength].pRawValue = (sbyte*)pContentLength;
(&httpResponse.Response_V1.Headers.KnownHeaders)[(int)HttpSysResponseHeader.ContentLength].RawValueLength = (ushort)byteContentLength.Length;
httpResponse.Response_V1.Headers.UnknownHeaderCount = 0;
statusCode =
UnsafeNclNativeMethods.HttpApi.HttpSendHttpResponse(

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNet.Server.WebListener
private ResponseStream _nativeStream;
private long _contentLength;
private BoundaryType _boundaryType;
private UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE _nativeResponse;
private UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2 _nativeResponse;
private IList<Tuple<Action<object>, object>> _onSendingHeadersActions;
private RequestContext _requestContext;
@ -32,12 +32,12 @@ namespace Microsoft.AspNet.Server.WebListener
{
// TODO: Verbose log
_requestContext = httpContext;
_nativeResponse = new UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE();
_nativeResponse = new UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2();
_headers = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase);
_boundaryType = BoundaryType.None;
_nativeResponse.StatusCode = (ushort)HttpStatusCode.OK;
_nativeResponse.Version.MajorVersion = 1;
_nativeResponse.Version.MinorVersion = 1;
_nativeResponse.Response_V1.StatusCode = (ushort)HttpStatusCode.OK;
_nativeResponse.Response_V1.Version.MajorVersion = 1;
_nativeResponse.Response_V1.Version.MinorVersion = 1;
_responseState = ResponseState.Created;
_onSendingHeadersActions = new List<Tuple<Action<object>, object>>();
}
@ -68,7 +68,7 @@ namespace Microsoft.AspNet.Server.WebListener
public int StatusCode
{
get { return _nativeResponse.StatusCode; }
get { return _nativeResponse.Response_V1.StatusCode; }
set
{
if (value <= 100 || 999 < value)
@ -76,7 +76,7 @@ namespace Microsoft.AspNet.Server.WebListener
throw new ArgumentOutOfRangeException("value", value, string.Format(Resources.Exception_InvalidStatusCode, value));
}
CheckResponseStarted();
_nativeResponse.StatusCode = (ushort)value;
_nativeResponse.Response_V1.StatusCode = (ushort)value;
}
}
@ -296,7 +296,7 @@ namespace Microsoft.AspNet.Server.WebListener
// TODO: Verbose log headers
_responseState = ResponseState.SentHeaders;
string reasonPhrase = GetReasonPhrase(_nativeResponse.StatusCode);
string reasonPhrase = GetReasonPhrase(_nativeResponse.Response_V1.StatusCode);
/*
if (m_BoundaryType==BoundaryType.Raw) {
@ -305,23 +305,23 @@ namespace Microsoft.AspNet.Server.WebListener
*/
uint statusCode;
uint bytesSent;
List<GCHandle> pinnedHeaders = SerializeHeaders(ref _nativeResponse.Headers, isOpaqueUpgrade);
List<GCHandle> pinnedHeaders = SerializeHeaders();
try
{
if (pDataChunk != null)
{
_nativeResponse.EntityChunkCount = 1;
_nativeResponse.pEntityChunks = pDataChunk;
_nativeResponse.Response_V1.EntityChunkCount = 1;
_nativeResponse.Response_V1.pEntityChunks = pDataChunk;
}
else if (asyncResult != null && asyncResult.DataChunks != null)
{
_nativeResponse.EntityChunkCount = asyncResult.DataChunkCount;
_nativeResponse.pEntityChunks = asyncResult.DataChunks;
_nativeResponse.Response_V1.EntityChunkCount = asyncResult.DataChunkCount;
_nativeResponse.Response_V1.pEntityChunks = asyncResult.DataChunks;
}
else
{
_nativeResponse.EntityChunkCount = 0;
_nativeResponse.pEntityChunks = null;
_nativeResponse.Response_V1.EntityChunkCount = 0;
_nativeResponse.Response_V1.pEntityChunks = null;
}
if (reasonPhrase.Length > 0)
@ -329,10 +329,10 @@ namespace Microsoft.AspNet.Server.WebListener
byte[] reasonPhraseBytes = new byte[HeaderEncoding.GetByteCount(reasonPhrase)];
fixed (byte* pReasonPhrase = reasonPhraseBytes)
{
_nativeResponse.ReasonLength = (ushort)reasonPhraseBytes.Length;
_nativeResponse.Response_V1.ReasonLength = (ushort)reasonPhraseBytes.Length;
HeaderEncoding.GetBytes(reasonPhrase, 0, reasonPhraseBytes.Length, reasonPhraseBytes, 0);
_nativeResponse.pReason = (sbyte*)pReasonPhrase;
fixed (UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE* pResponse = &_nativeResponse)
_nativeResponse.Response_V1.pReason = (sbyte*)pReasonPhrase;
fixed (UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2* pResponse = &_nativeResponse)
{
statusCode =
UnsafeNclNativeMethods.HttpApi.HttpSendHttpResponse(
@ -359,7 +359,7 @@ namespace Microsoft.AspNet.Server.WebListener
}
else
{
fixed (UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE* pResponse = &_nativeResponse)
fixed (UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2* pResponse = &_nativeResponse)
{
statusCode =
UnsafeNclNativeMethods.HttpApi.HttpSendHttpResponse(
@ -414,8 +414,8 @@ namespace Microsoft.AspNet.Server.WebListener
// Check the response headers to determine the correct keep alive and boundary type.
Version responseVersion = GetProtocolVersion();
_nativeResponse.Version.MajorVersion = (ushort)responseVersion.Major;
_nativeResponse.Version.MinorVersion = (ushort)responseVersion.Minor;
_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);
@ -531,10 +531,10 @@ namespace Microsoft.AspNet.Server.WebListener
return flags;
}
private List<GCHandle> SerializeHeaders(ref UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADERS headers,
bool isOpaqueUpgrade)
private List<GCHandle> SerializeHeaders()
{
UnsafeNclNativeMethods.HttpApi.HTTP_UNKNOWN_HEADER[] unknownHeaders = null;
UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO[] knownHeaderInfo = null;
List<GCHandle> pinnedHeaders;
GCHandle gcHandle;
/*
@ -553,124 +553,78 @@ namespace Microsoft.AspNet.Server.WebListener
byte[] bytes = null;
pinnedHeaders = new List<GCHandle>();
//---------------------------------------------------
// DTS Issue: 609383:
// The Set-Cookie headers are being merged into one.
// There are two issues here.
// 1. When Set-Cookie headers are set through SetCookie method on the ListenerResponse,
// there is code in the SetCookie method and the methods it calls to flatten the Set-Cookie
// values. This blindly concatenates the cookies with a comma delimiter. There could be
// a cookie value that contains comma, but we don't escape it with %XX value
//
// As an alternative users can add the Set-Cookie header through the AddHeader method
// like ListenerResponse.Headers.Add("name", "value")
// That way they can add multiple headers - AND They can format the value like they want it.
//
// 2. Now that the header collection contains multiple Set-Cookie name, value pairs
// you would think the problem would go away. However here is an interesting thing.
// For NameValueCollection, when you add
// "Set-Cookie", "value1"
// "Set-Cookie", "value2"
// The NameValueCollection.Count == 1. Because there is only one key
// NameValueCollection.Get("Set-Cookie") would conveniently take these two values
// concatenate them with a comma like
// value1,value2.
// In order to get individual values, you need to use
// string[] values = NameValueCollection.GetValues("Set-Cookie");
//
// -------------------------------------------------------------
// So here is the proposed fix here.
// We must first to loop through all the NameValueCollection keys
// and if the name is a unknown header, we must compute the number of
// values it has. Then, we should allocate that many unknown header array
// elements.
//
// Note that a part of the fix here is to treat Set-Cookie as an unknown header
//
//
//-----------------------------------------------------------
int numUnknownHeaders = 0;
int numKnownMultiHeaders = 0;
foreach (KeyValuePair<string, string[]> headerPair in Headers)
{
if (headerPair.Value.Length == 0)
{
// TODO: Have the collection exclude empty headers.
continue;
}
// See if this is an unknown header
lookup = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerPair.Key);
// TODO: WWW-Authentiate header
// TODO: HTTP_RESPONSE_V2 has a HTTP_MULTIPLE_KNOWN_HEADERS option where you can supply multiple values.
// TODO: Consider any 'known' header that has multiple values as 'unknown'?
// Treat Set-Cookie as well as Connection header in opaque mode as unknown
if (lookup == (int)HttpSysResponseHeader.SetCookie ||
(isOpaqueUpgrade && lookup == (int)HttpSysResponseHeader.Connection))
{
lookup = -1;
}
if (lookup == -1)
{
numUnknownHeaders += headerPair.Value.Length;
}
else if (headerPair.Value.Length > 1)
{
numKnownMultiHeaders++;
}
// else known single-value header.
}
try
{
fixed (UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER* pKnownHeaders = &headers.KnownHeaders)
fixed (UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER* pKnownHeaders = &_nativeResponse.Response_V1.Headers.KnownHeaders)
{
foreach (KeyValuePair<string, string[]> headerPair in Headers)
{
headerName = headerPair.Key;
lookup = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerName);
if (lookup == (int)HttpSysResponseHeader.SetCookie ||
(isOpaqueUpgrade && lookup == (int)HttpSysResponseHeader.Connection))
if (headerPair.Value.Length == 0)
{
lookup = -1;
// TODO: Have the collection exclude empty headers.
continue;
}
headerName = headerPair.Key;
string[] headerValues = headerPair.Value;
lookup = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerName);
if (lookup == -1)
{
if (unknownHeaders == null)
{
//----------------------------------------
// *** This following comment is no longer true ***
// we waste some memory here (up to 32*41=1312 bytes) but we gain speed
// unknownHeaders = new UnsafeNclNativeMethods.HttpApi.HTTP_UNKNOWN_HEADER[Headers.Count-index];
//--------------------------------------------
unknownHeaders = new UnsafeNclNativeMethods.HttpApi.HTTP_UNKNOWN_HEADER[numUnknownHeaders];
gcHandle = GCHandle.Alloc(unknownHeaders, GCHandleType.Pinned);
pinnedHeaders.Add(gcHandle);
headers.pUnknownHeaders = (UnsafeNclNativeMethods.HttpApi.HTTP_UNKNOWN_HEADER*)gcHandle.AddrOfPinnedObject();
_nativeResponse.Response_V1.Headers.pUnknownHeaders = (UnsafeNclNativeMethods.HttpApi.HTTP_UNKNOWN_HEADER*)gcHandle.AddrOfPinnedObject();
}
//----------------------------------------
// FOR UNKNOWN HEADERS
// ALLOW MULTIPLE HEADERS to be added
//---------------------------------------
string[] headerValues = headerPair.Value;
for (int headerValueIndex = 0; headerValueIndex < headerValues.Length; headerValueIndex++)
{
// Add Name
bytes = new byte[HeaderEncoding.GetByteCount(headerName)];
unknownHeaders[headers.UnknownHeaderCount].NameLength = (ushort)bytes.Length;
unknownHeaders[_nativeResponse.Response_V1.Headers.UnknownHeaderCount].NameLength = (ushort)bytes.Length;
HeaderEncoding.GetBytes(headerName, 0, bytes.Length, bytes, 0);
gcHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
pinnedHeaders.Add(gcHandle);
unknownHeaders[headers.UnknownHeaderCount].pName = (sbyte*)gcHandle.AddrOfPinnedObject();
unknownHeaders[_nativeResponse.Response_V1.Headers.UnknownHeaderCount].pName = (sbyte*)gcHandle.AddrOfPinnedObject();
// Add Value
headerValue = headerValues[headerValueIndex];
bytes = new byte[HeaderEncoding.GetByteCount(headerValue)];
unknownHeaders[headers.UnknownHeaderCount].RawValueLength = (ushort)bytes.Length;
unknownHeaders[_nativeResponse.Response_V1.Headers.UnknownHeaderCount].RawValueLength = (ushort)bytes.Length;
HeaderEncoding.GetBytes(headerValue, 0, bytes.Length, bytes, 0);
gcHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
pinnedHeaders.Add(gcHandle);
unknownHeaders[headers.UnknownHeaderCount].pRawValue = (sbyte*)gcHandle.AddrOfPinnedObject();
headers.UnknownHeaderCount++;
unknownHeaders[_nativeResponse.Response_V1.Headers.UnknownHeaderCount].pRawValue = (sbyte*)gcHandle.AddrOfPinnedObject();
_nativeResponse.Response_V1.Headers.UnknownHeaderCount++;
}
}
else
else if (headerPair.Value.Length == 1)
{
string[] headerValues = headerPair.Value;
headerValue = headerValues.Length == 1 ? headerValues[0] : string.Join(", ", headerValues);
headerValue = headerValues[0];
if (headerValue != null)
{
bytes = new byte[HeaderEncoding.GetByteCount(headerValue)];
@ -681,6 +635,49 @@ namespace Microsoft.AspNet.Server.WebListener
pKnownHeaders[lookup].pRawValue = (sbyte*)gcHandle.AddrOfPinnedObject();
}
}
else
{
if (knownHeaderInfo == null)
{
knownHeaderInfo = new UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO[numKnownMultiHeaders];
gcHandle = GCHandle.Alloc(knownHeaderInfo, GCHandleType.Pinned);
pinnedHeaders.Add(gcHandle);
_nativeResponse.pResponseInfo = (UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO*)gcHandle.AddrOfPinnedObject();
}
knownHeaderInfo[_nativeResponse.ResponseInfoCount].Type = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO_TYPE.HttpResponseInfoTypeMultipleKnownHeaders;
knownHeaderInfo[_nativeResponse.ResponseInfoCount].Length = (uint)Marshal.SizeOf(typeof(UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS));
UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS header = new UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS();
header.HeaderId = (UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADER_ID.Enum)lookup;
header.Flags = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO_FLAGS.PreserveOrder; // TODO: The docs say this is for www-auth only.
UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER[] nativeHeaderValues = new UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER[headerValues.Length];
gcHandle = GCHandle.Alloc(nativeHeaderValues, GCHandleType.Pinned);
pinnedHeaders.Add(gcHandle);
header.KnownHeaders = (UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER*)gcHandle.AddrOfPinnedObject();
for (int headerValueIndex = 0; headerValueIndex < headerValues.Length; headerValueIndex++)
{
// Add Value
headerValue = headerValues[headerValueIndex];
bytes = new byte[HeaderEncoding.GetByteCount(headerValue)];
nativeHeaderValues[header.KnownHeaderCount].RawValueLength = (ushort)bytes.Length;
HeaderEncoding.GetBytes(headerValue, 0, bytes.Length, bytes, 0);
gcHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
pinnedHeaders.Add(gcHandle);
nativeHeaderValues[header.KnownHeaderCount].pRawValue = (sbyte*)gcHandle.AddrOfPinnedObject();
header.KnownHeaderCount++;
}
// This type is a struct, not an object, so pinning it causes a boxed copy to be created. We can't do that until after all the fields are set.
gcHandle = GCHandle.Alloc(header, GCHandleType.Pinned);
pinnedHeaders.Add(gcHandle);
knownHeaderInfo[_nativeResponse.ResponseInfoCount].pInfo = (UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS*)gcHandle.AddrOfPinnedObject();
_nativeResponse.ResponseInfoCount++;
}
}
}
}

View File

@ -18,6 +18,7 @@ using Xunit;
namespace Microsoft.AspNet.Server.WebListener.Test
{
using AppFunc = Func<object, Task>;
using System.Net;
public class ResponseHeaderTests
{
@ -42,6 +43,54 @@ namespace Microsoft.AspNet.Server.WebListener.Test
}
}
[Fact]
public async Task ResponseHeaders_ServerSendsSingleValueKnownHeaders_Success()
{
using (Utilities.CreateHttpServer(env =>
{
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
var responseInfo = httpContext.GetFeature<IHttpResponseInformation>();
var responseHeaders = responseInfo.Headers;
responseHeaders["WWW-Authenticate"] = new string[] { "custom1" };
return Task.FromResult(0);
}))
{
// HttpClient would merge the headers no matter what
WebRequest request = WebRequest.Create(Address);
HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync();
Assert.Equal(4, response.Headers.Count);
Assert.Null(response.Headers["Transfer-Encoding"]);
Assert.Equal(0, response.ContentLength);
Assert.NotNull(response.Headers["Date"]);
Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers["Server"]);
Assert.Equal(new string[] { "custom1" }, response.Headers.GetValues("WWW-Authenticate"));
}
}
[Fact]
public async Task ResponseHeaders_ServerSendsMultiValueKnownHeaders_Success()
{
using (Utilities.CreateHttpServer(env =>
{
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
var responseInfo = httpContext.GetFeature<IHttpResponseInformation>();
var responseHeaders = responseInfo.Headers;
responseHeaders["WWW-Authenticate"] = new string[] { "custom1, and custom2", "custom3" };
return Task.FromResult(0);
}))
{
// HttpClient would merge the headers no matter what
WebRequest request = WebRequest.Create(Address);
HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync();
Assert.Equal(4, response.Headers.Count);
Assert.Null(response.Headers["Transfer-Encoding"]);
Assert.Equal(0, response.ContentLength);
Assert.NotNull(response.Headers["Date"]);
Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers["Server"]);
Assert.Equal(new string[] { "custom1, and custom2", "custom3" }, response.Headers.GetValues("WWW-Authenticate"));
}
}
[Fact]
public async Task ResponseHeaders_ServerSendsCustomHeaders_Success()
{
@ -54,15 +103,15 @@ namespace Microsoft.AspNet.Server.WebListener.Test
return Task.FromResult(0);
}))
{
HttpResponseMessage response = await SendRequestAsync(Address);
response.EnsureSuccessStatusCode();
Assert.Equal(3, 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());
// HttpClient would merge the headers no matter what
WebRequest request = WebRequest.Create(Address);
HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync();
Assert.Equal(4, response.Headers.Count);
Assert.Null(response.Headers["Transfer-Encoding"]);
Assert.Equal(0, response.ContentLength);
Assert.NotNull(response.Headers["Date"]);
Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers["Server"]);
Assert.Equal(new string[] { "custom1, and custom2", "custom3" }, response.Headers.GetValues("Custom-Header1"));
Assert.Equal(1, response.Content.Headers.Count());
Assert.Equal(0, response.Content.Headers.ContentLength);
}
}