diff --git a/src/Microsoft.AspNet.Server.WebListener/NativeInterop/UnsafeNativeMethods.cs b/src/Microsoft.AspNet.Server.WebListener/NativeInterop/UnsafeNativeMethods.cs index 0eb890cf8b..5591fd2068 100644 --- a/src/Microsoft.AspNet.Server.WebListener/NativeInterop/UnsafeNativeMethods.cs +++ b/src/Microsoft.AspNet.Server.WebListener/NativeInterop/UnsafeNativeMethods.cs @@ -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 { diff --git a/src/Microsoft.AspNet.Server.WebListener/OwinWebListener.cs b/src/Microsoft.AspNet.Server.WebListener/OwinWebListener.cs index dbe4fe4944..904f7a032e 100644 --- a/src/Microsoft.AspNet.Server.WebListener/OwinWebListener.cs +++ b/src/Microsoft.AspNet.Server.WebListener/OwinWebListener.cs @@ -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( diff --git a/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/Response.cs b/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/Response.cs index 4988fd0e30..1db2216f9c 100644 --- a/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/Response.cs +++ b/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/Response.cs @@ -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, 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(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, 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 pinnedHeaders = SerializeHeaders(ref _nativeResponse.Headers, isOpaqueUpgrade); + List 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 SerializeHeaders(ref UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADERS headers, - bool isOpaqueUpgrade) + private List SerializeHeaders() { UnsafeNclNativeMethods.HttpApi.HTTP_UNKNOWN_HEADER[] unknownHeaders = null; + UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO[] knownHeaderInfo = null; List pinnedHeaders; GCHandle gcHandle; /* @@ -553,124 +553,78 @@ namespace Microsoft.AspNet.Server.WebListener byte[] bytes = null; pinnedHeaders = new List(); - //--------------------------------------------------- - // 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 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 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++; + } } } } diff --git a/test/Microsoft.AspNet.Server.WebListener.Test/ResponseHeaderTests.cs b/test/Microsoft.AspNet.Server.WebListener.Test/ResponseHeaderTests.cs index 25f8b29e62..59ce482ed5 100644 --- a/test/Microsoft.AspNet.Server.WebListener.Test/ResponseHeaderTests.cs +++ b/test/Microsoft.AspNet.Server.WebListener.Test/ResponseHeaderTests.cs @@ -18,6 +18,7 @@ using Xunit; namespace Microsoft.AspNet.Server.WebListener.Test { using AppFunc = Func; + 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(); + 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(); + 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); } }