Always flush headers on first response write (#1202).
This commit is contained in:
parent
0fbbd9e165
commit
757952d4d3
|
|
@ -520,6 +520,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
|
|
||||||
public void Write(ArraySegment<byte> data)
|
public void Write(ArraySegment<byte> data)
|
||||||
{
|
{
|
||||||
|
// For the first write, ensure headers are flushed if Write(Chunked)isn't called.
|
||||||
|
var firstWrite = !HasResponseStarted;
|
||||||
|
|
||||||
VerifyAndUpdateWrite(data.Count);
|
VerifyAndUpdateWrite(data.Count);
|
||||||
ProduceStartAndFireOnStarting().GetAwaiter().GetResult();
|
ProduceStartAndFireOnStarting().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
|
@ -529,6 +532,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
{
|
{
|
||||||
if (data.Count == 0)
|
if (data.Count == 0)
|
||||||
{
|
{
|
||||||
|
if (firstWrite)
|
||||||
|
{
|
||||||
|
Flush();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
WriteChunked(data);
|
WriteChunked(data);
|
||||||
|
|
@ -541,6 +548,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
HandleNonBodyResponseWrite();
|
HandleNonBodyResponseWrite();
|
||||||
|
|
||||||
|
if (firstWrite)
|
||||||
|
{
|
||||||
|
Flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -581,14 +593,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
|
|
||||||
await ProduceStartAndFireOnStarting();
|
await ProduceStartAndFireOnStarting();
|
||||||
|
|
||||||
|
// WriteAsyncAwaited is only called for the first write to the body.
|
||||||
|
// Ensure headers are flushed if Write(Chunked)Async isn't called.
|
||||||
if (_canHaveBody)
|
if (_canHaveBody)
|
||||||
{
|
{
|
||||||
if (_autoChunk)
|
if (_autoChunk)
|
||||||
{
|
{
|
||||||
if (data.Count == 0)
|
if (data.Count == 0)
|
||||||
{
|
{
|
||||||
|
await FlushAsync(cancellationToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await WriteChunkedAsync(data, cancellationToken);
|
await WriteChunkedAsync(data, cancellationToken);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -599,7 +615,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
HandleNonBodyResponseWrite();
|
HandleNonBodyResponseWrite();
|
||||||
return;
|
await FlushAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
|
@ -767,11 +768,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
{
|
{
|
||||||
await connection.SendEnd(
|
await connection.Send(
|
||||||
"HEAD / HTTP/1.1",
|
"HEAD / HTTP/1.1",
|
||||||
"",
|
"",
|
||||||
"");
|
"");
|
||||||
await connection.ReceiveEnd(
|
await connection.Receive(
|
||||||
"HTTP/1.1 200 OK",
|
"HTTP/1.1 200 OK",
|
||||||
$"Date: {server.Context.DateHeaderValue}",
|
$"Date: {server.Context.DateHeaderValue}",
|
||||||
"Content-Length: 42",
|
"Content-Length: 42",
|
||||||
|
|
@ -782,26 +783,95 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HeadResponseCanContainContentLengthHeaderButBodyNotWritten()
|
public async Task HeadResponseBodyNotWrittenWithAsyncWrite()
|
||||||
{
|
{
|
||||||
|
var flushed = new SemaphoreSlim(0, 1);
|
||||||
|
|
||||||
using (var server = new TestServer(async httpContext =>
|
using (var server = new TestServer(async httpContext =>
|
||||||
{
|
{
|
||||||
httpContext.Response.ContentLength = 12;
|
httpContext.Response.ContentLength = 12;
|
||||||
await httpContext.Response.WriteAsync("hello, world");
|
await httpContext.Response.WriteAsync("hello, world");
|
||||||
|
await flushed.WaitAsync();
|
||||||
|
}, new TestServiceContext()))
|
||||||
|
{
|
||||||
|
using (var connection = server.CreateConnection())
|
||||||
|
{
|
||||||
|
await connection.Send(
|
||||||
|
"HEAD / HTTP/1.1",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
await connection.Receive(
|
||||||
|
"HTTP/1.1 200 OK",
|
||||||
|
$"Date: {server.Context.DateHeaderValue}",
|
||||||
|
"Content-Length: 12",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
|
||||||
|
flushed.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HeadResponseBodyNotWrittenWithSyncWrite()
|
||||||
|
{
|
||||||
|
var flushed = new SemaphoreSlim(0, 1);
|
||||||
|
|
||||||
|
using (var server = new TestServer(httpContext =>
|
||||||
|
{
|
||||||
|
httpContext.Response.ContentLength = 12;
|
||||||
|
httpContext.Response.Body.Write(Encoding.ASCII.GetBytes("hello, world"), 0, 12);
|
||||||
|
flushed.Wait();
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
}, new TestServiceContext()))
|
||||||
|
{
|
||||||
|
using (var connection = server.CreateConnection())
|
||||||
|
{
|
||||||
|
await connection.Send(
|
||||||
|
"HEAD / HTTP/1.1",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
await connection.Receive(
|
||||||
|
"HTTP/1.1 200 OK",
|
||||||
|
$"Date: {server.Context.DateHeaderValue}",
|
||||||
|
"Content-Length: 12",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
|
||||||
|
flushed.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ZeroLengthWritesFlushHeaders()
|
||||||
|
{
|
||||||
|
var flushed = new SemaphoreSlim(0, 1);
|
||||||
|
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
httpContext.Response.ContentLength = 12;
|
||||||
|
await httpContext.Response.WriteAsync("");
|
||||||
|
flushed.Wait();
|
||||||
|
await httpContext.Response.WriteAsync("hello, world");
|
||||||
}, new TestServiceContext()))
|
}, new TestServiceContext()))
|
||||||
{
|
{
|
||||||
using (var connection = server.CreateConnection())
|
using (var connection = server.CreateConnection())
|
||||||
{
|
{
|
||||||
await connection.SendEnd(
|
await connection.SendEnd(
|
||||||
"HEAD / HTTP/1.1",
|
"GET / HTTP/1.1",
|
||||||
"",
|
"",
|
||||||
"");
|
"");
|
||||||
await connection.ReceiveEnd(
|
await connection.Receive(
|
||||||
"HTTP/1.1 200 OK",
|
"HTTP/1.1 200 OK",
|
||||||
$"Date: {server.Context.DateHeaderValue}",
|
$"Date: {server.Context.DateHeaderValue}",
|
||||||
"Content-Length: 12",
|
"Content-Length: 12",
|
||||||
"",
|
"",
|
||||||
"");
|
"");
|
||||||
|
|
||||||
|
flushed.Release();
|
||||||
|
|
||||||
|
await connection.ReceiveEnd("hello, world");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,48 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ConnectionFilterData))]
|
||||||
|
public async Task ZeroLengthWritesFlushHeaders(TestServiceContext testContext)
|
||||||
|
{
|
||||||
|
var flushed = new SemaphoreSlim(0, 1);
|
||||||
|
|
||||||
|
using (var server = new TestServer(async httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
await response.WriteAsync("");
|
||||||
|
|
||||||
|
await flushed.WaitAsync();
|
||||||
|
|
||||||
|
await response.WriteAsync("Hello World!");
|
||||||
|
}, testContext))
|
||||||
|
{
|
||||||
|
using (var connection = server.CreateConnection())
|
||||||
|
{
|
||||||
|
await connection.SendEnd(
|
||||||
|
"GET / HTTP/1.1",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
|
||||||
|
await connection.Receive(
|
||||||
|
"HTTP/1.1 200 OK",
|
||||||
|
$"Date: {testContext.DateHeaderValue}",
|
||||||
|
"Transfer-Encoding: chunked",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
|
||||||
|
flushed.Release();
|
||||||
|
|
||||||
|
await connection.ReceiveEnd(
|
||||||
|
"c",
|
||||||
|
"Hello World!",
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[MemberData(nameof(ConnectionFilterData))]
|
[MemberData(nameof(ConnectionFilterData))]
|
||||||
public async Task EmptyResponseBodyHandledCorrectlyWithZeroLengthWrite(TestServiceContext testContext)
|
public async Task EmptyResponseBodyHandledCorrectlyWithZeroLengthWrite(TestServiceContext testContext)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue