Skip to content

Commit 06f52b8

Browse files
committed
DevDiv 388456 -- WebHost should not use Transfer-Encoding chunked when content length is known
1 parent f351672 commit 06f52b8

File tree

3 files changed

+81
-13
lines changed

3 files changed

+81
-13
lines changed

src/System.Web.Http.WebHost/HttpControllerHandler.cs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -278,12 +278,19 @@ internal static Task ConvertResponse(HttpContextBase httpContextBase, HttpRespon
278278

279279
if (response.Content != null)
280280
{
281-
CopyHeaders(response.Content.Headers, httpContextBase);
282-
283-
// Select output buffering by the kind of content
281+
// Select output buffering based on the kind of HttpContent.
282+
// This is done before CopyHeaders because the ContentLength
283+
// property getter will evaluate the content length and set
284+
// the Content-Length header if it has not already been set.
285+
// Doing this before CopyHeaders ensures the headers contain a
286+
// valid Content-Length before they are copied to HttpContextBase.
287+
// Unless HttpContextBase headers contain a positive Content-Length,
288+
// the Transfer-Encoding for streamed output will be chunked.
284289
isBuffered = IsOutputBufferingNecessary(response.Content);
285290
httpResponseBase.BufferOutput = isBuffered;
286291

292+
CopyHeaders(response.Content.Headers, httpContextBase);
293+
287294
responseTask = response.Content.CopyToAsync(httpResponseBase.OutputStream);
288295
}
289296
else
@@ -328,24 +335,24 @@ internal static Task ConvertResponse(HttpContextBase httpContextBase, HttpRespon
328335
});
329336
}
330337

331-
private static bool IsOutputBufferingNecessary(HttpContent httpContent)
338+
/// <summary>
339+
/// Determines whether the given <see cref="HttpContent"/> should use a buffered response.
340+
/// </summary>
341+
/// <param name="httpContent">The <see cref="HttpContent"/> of the response.</param>
342+
/// <returns>A value of <c>true</c> indicates buffering should be used, otherwise a streamed response should be used.</returns>
343+
internal static bool IsOutputBufferingNecessary(HttpContent httpContent)
332344
{
333-
// Never buffer StreamContent. Either it's already buffered or
334-
// is of indeterminate length. Neither calls for us to buffer.
335-
if (httpContent == null || httpContent is StreamContent)
336-
{
337-
return false;
338-
}
345+
Contract.Assert(httpContent != null);
339346

340-
// Any content that knows its ContentLength is assumed to have
341-
// buffered it already.
347+
// Any HttpContent that knows its length is presumably already buffered internally.
342348
long? contentLength = httpContent.Headers.ContentLength;
343349
if (contentLength.HasValue && contentLength.Value >= 0)
344350
{
345351
return false;
346352
}
347353

348-
return true;
354+
// Content length is null or -1 (meaning not known). Buffer any HttpContent except StreamContent.
355+
return !(httpContent is StreamContent);
349356
}
350357

351358
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller becomes owner")]

test/System.Web.Http.WebHost.Test/HttpControllerHandlerTest.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
using System.Collections;
2+
using System.Collections.Generic;
23
using System.IO;
4+
using System.Linq;
35
using System.Net;
46
using System.Net.Http;
7+
using System.Net.Http.Formatting;
58
using System.Net.Http.Headers;
69
using System.Reflection;
10+
using System.Text;
711
using System.Threading.Tasks;
12+
using Microsoft.TestCommon;
813
using Moq;
914
using Xunit;
15+
using Xunit.Extensions;
1016
using Assert = Microsoft.TestCommon.AssertEx;
1117

1218
namespace System.Web.Http.WebHost
@@ -133,5 +139,56 @@ public void SuppressFormsAuthenticationRedirect_RequireSuppressRedirect() {
133139
}
134140
}
135141

142+
[Theory]
143+
[PropertyData("OutputBufferingTestData")]
144+
public void IsOutputBufferingNecessary_Returns_Correct_Value(HttpContent content, bool isBuffered)
145+
{
146+
// Arrange & Act & Assert
147+
Assert.Equal(isBuffered, HttpControllerHandler.IsOutputBufferingNecessary(content));
148+
}
149+
150+
[Theory]
151+
[PropertyData("OutputBufferingTestData")]
152+
public void IsOutputBufferingNecessary_Causes_Content_Length_Header_To_Be_Set(HttpContent content, bool isBuffered)
153+
{
154+
// Arrange & Act
155+
HttpControllerHandler.IsOutputBufferingNecessary(content);
156+
IEnumerable<string> contentLengthEnumerable;
157+
bool isContentLengthInHeaders = content.Headers.TryGetValues("Content-Length", out contentLengthEnumerable);
158+
string[] contentLengthStrings = isContentLengthInHeaders ? contentLengthEnumerable.ToArray() : new string[0];
159+
long? contentLength = content.Headers.ContentLength;
160+
161+
// Assert
162+
if (contentLength.HasValue && contentLength.Value >= 0)
163+
{
164+
// Setting the header is HttpContentHeader's responsibility, but we assert
165+
// it has happened because it is IsOutputBufferingNecessary's responsibility
166+
// to cause that to happen. HttpControllerHandler relies on this.
167+
Assert.True(isContentLengthInHeaders);
168+
Assert.Equal(contentLength.Value, long.Parse(contentLengthStrings[0]));
169+
}
170+
}
171+
172+
public static IEnumerable<object[]> OutputBufferingTestData
173+
{
174+
get
175+
{
176+
string testString = "testString";
177+
Mock<Stream> mockStream = new Mock<Stream>() { CallBase = true };
178+
return new TheoryDataSet<HttpContent, bool>()
179+
{
180+
// Known length HttpContents other than OC should not buffer
181+
{ new StringContent(testString), false },
182+
{ new ByteArrayContent(Encoding.UTF8.GetBytes(testString)), false },
183+
{ new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(testString))), false},
184+
185+
// StreamContent (unknown length) should not buffer
186+
{ new StreamContent(mockStream.Object), false},
187+
188+
// ObjectContent (unknown length) should buffer
189+
{ new ObjectContent<string>(testString, new XmlMediaTypeFormatter()), true }
190+
};
191+
}
192+
}
136193
}
137194
}

test/System.Web.Http.WebHost.Test/System.Web.Http.WebHost.Test.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@
7272
<None Include="packages.config" />
7373
</ItemGroup>
7474
<ItemGroup>
75+
<ProjectReference Include="..\..\src\System.Net.Http.Formatting\System.Net.Http.Formatting.csproj">
76+
<Project>{668E9021-CE84-49D9-98FB-DF125A9FCDB0}</Project>
77+
<Name>System.Net.Http.Formatting</Name>
78+
</ProjectReference>
7579
<ProjectReference Include="..\..\src\System.Web.Http.WebHost\System.Web.Http.WebHost.csproj">
7680
<Project>{A0187BC2-8325-4BB2-8697-7F955CF4173E}</Project>
7781
<Name>System.Web.Http.WebHost</Name>

0 commit comments

Comments
 (0)