Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

Commit cb1a40e

Browse files
committed
Don't emit TE header or body for non-body responses
1 parent f2fa8b5 commit cb1a40e

File tree

9 files changed

+423
-73
lines changed

9 files changed

+423
-73
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
6+
7+
namespace Microsoft.AspNetCore.Server.Kestrel
8+
{
9+
public sealed class BadHttpResponseException : InvalidOperationException
10+
{
11+
private BadHttpResponseException(string message)
12+
: base(message)
13+
{
14+
15+
}
16+
17+
internal static BadHttpResponseException GetException(ResponseRejectionReasons reason)
18+
{
19+
BadHttpResponseException ex;
20+
switch (reason)
21+
{
22+
case ResponseRejectionReasons.HeadersReadonlyResponseStarted:
23+
ex = new BadHttpResponseException("Headers are read-only, response has already started.");
24+
break;
25+
default:
26+
ex = new BadHttpResponseException("Bad response.");
27+
break;
28+
}
29+
30+
return ex;
31+
}
32+
33+
internal static BadHttpResponseException GetException(ResponseRejectionReasons reason, string value)
34+
{
35+
BadHttpResponseException ex;
36+
switch (reason)
37+
{
38+
case ResponseRejectionReasons.ValueCannotBeSetResponseStarted:
39+
ex = new BadHttpResponseException(value + " cannot be set, response had already started.");
40+
break;
41+
case ResponseRejectionReasons.TransferEncodingSetOnNonBodyResponse:
42+
ex = new BadHttpResponseException($"Transfer-Encoding set on a {value} non-body request.");
43+
break;
44+
case ResponseRejectionReasons.WriteToNonBodyResponse:
45+
ex = new BadHttpResponseException($"Write to non-body {value} response.");
46+
break;
47+
default:
48+
ex = new BadHttpResponseException("Bad response.");
49+
break;
50+
}
51+
52+
return ex;
53+
54+
}
55+
}
56+
}

src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs

Lines changed: 134 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public abstract partial class Frame : ConnectionContext, IFrameControl
5959

6060
protected RequestProcessingStatus _requestProcessingStatus;
6161
protected bool _keepAlive;
62+
private bool _canHaveBody;
6263
private bool _autoChunk;
6364
protected Exception _applicationException;
6465

@@ -131,7 +132,7 @@ public int StatusCode
131132
{
132133
if (HasResponseStarted)
133134
{
134-
ThrowResponseAlreadyStartedException(nameof(StatusCode));
135+
throw BadHttpResponseException.GetException(ResponseRejectionReasons.ValueCannotBeSetResponseStarted, nameof(StatusCode));
135136
}
136137

137138
_statusCode = value;
@@ -149,7 +150,7 @@ public string ReasonPhrase
149150
{
150151
if (HasResponseStarted)
151152
{
152-
ThrowResponseAlreadyStartedException(nameof(ReasonPhrase));
153+
throw BadHttpResponseException.GetException(ResponseRejectionReasons.ValueCannotBeSetResponseStarted, nameof(ReasonPhrase));
153154
}
154155

155156
_reasonPhrase = value;
@@ -471,17 +472,24 @@ public void Write(ArraySegment<byte> data)
471472
{
472473
ProduceStartAndFireOnStarting().GetAwaiter().GetResult();
473474

474-
if (_autoChunk)
475+
if (_canHaveBody)
475476
{
476-
if (data.Count == 0)
477+
if (_autoChunk)
478+
{
479+
if (data.Count == 0)
480+
{
481+
return;
482+
}
483+
WriteChunked(data);
484+
}
485+
else
477486
{
478-
return;
487+
SocketOutput.Write(data);
479488
}
480-
WriteChunked(data);
481489
}
482490
else
483491
{
484-
SocketOutput.Write(data);
492+
HandleNonBodyResponseWrite(data.Count);
485493
}
486494
}
487495

@@ -492,36 +500,53 @@ public Task WriteAsync(ArraySegment<byte> data, CancellationToken cancellationTo
492500
return WriteAsyncAwaited(data, cancellationToken);
493501
}
494502

495-
if (_autoChunk)
503+
if (_canHaveBody)
496504
{
497-
if (data.Count == 0)
505+
if (_autoChunk)
498506
{
499-
return TaskUtilities.CompletedTask;
507+
if (data.Count == 0)
508+
{
509+
return TaskUtilities.CompletedTask;
510+
}
511+
return WriteChunkedAsync(data, cancellationToken);
512+
}
513+
else
514+
{
515+
return SocketOutput.WriteAsync(data, cancellationToken: cancellationToken);
500516
}
501-
return WriteChunkedAsync(data, cancellationToken);
502517
}
503518
else
504519
{
505-
return SocketOutput.WriteAsync(data, cancellationToken: cancellationToken);
520+
HandleNonBodyResponseWrite(data.Count);
521+
return TaskUtilities.CompletedTask;
506522
}
507523
}
508524

509525
public async Task WriteAsyncAwaited(ArraySegment<byte> data, CancellationToken cancellationToken)
510526
{
511527
await ProduceStartAndFireOnStarting();
512528

513-
if (_autoChunk)
529+
if (_canHaveBody)
514530
{
515-
if (data.Count == 0)
531+
if (_autoChunk)
532+
{
533+
if (data.Count == 0)
534+
{
535+
return;
536+
}
537+
await WriteChunkedAsync(data, cancellationToken);
538+
}
539+
else
516540
{
517-
return;
541+
await SocketOutput.WriteAsync(data, cancellationToken: cancellationToken);
518542
}
519-
await WriteChunkedAsync(data, cancellationToken);
520543
}
521544
else
522545
{
523-
await SocketOutput.WriteAsync(data, cancellationToken: cancellationToken);
546+
HandleNonBodyResponseWrite(data.Count);
547+
return;
524548
}
549+
525550
}
526551

527552
private void WriteChunked(ArraySegment<byte> data)
@@ -636,28 +661,14 @@ protected Task ProduceEnd()
636661

637662
if (_requestRejected)
638663
{
639-
// 400 Bad Request
640-
StatusCode = 400;
641664
_keepAlive = false;
665+
// 400 Bad Request
666+
ErrorResetHeadersToDefaults(statusCode: 400);
642667
}
643668
else
644669
{
645670
// 500 Internal Server Error
646-
StatusCode = 500;
647-
}
648-
649-
ReasonPhrase = null;
650-
651-
var responseHeaders = FrameResponseHeaders;
652-
responseHeaders.Reset();
653-
var dateHeaderValues = DateHeaderValueManager.GetDateHeaderValues();
654-
655-
responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
656-
responseHeaders.SetRawContentLength("0", _bytesContentLengthZero);
657-
658-
if (ServerOptions.AddServerHeader)
659-
{
660-
responseHeaders.SetRawServer(Constants.ServerName, _bytesServer);
671+
ErrorResetHeadersToDefaults(statusCode: 500);
661672
}
662673
}
663674

@@ -711,10 +722,12 @@ private void CreateResponseHeader(
711722
bool appCompleted)
712723
{
713724
var responseHeaders = FrameResponseHeaders;
714-
responseHeaders.SetReadOnly();
715725

716726
var hasConnection = responseHeaders.HasConnection;
717727

728+
// Set whether response can have body
729+
_canHaveBody = StatusCanHaveBody(StatusCode) && Method != "HEAD";
730+
718731
var end = SocketOutput.ProducingStart();
719732
if (_keepAlive && hasConnection)
720733
{
@@ -728,39 +741,48 @@ private void CreateResponseHeader(
728741
}
729742
}
730743

731-
if (_keepAlive && !responseHeaders.HasTransferEncoding && !responseHeaders.HasContentLength)
744+
if (_canHaveBody)
732745
{
733-
if (appCompleted)
746+
if (_keepAlive && !responseHeaders.HasTransferEncoding && !responseHeaders.HasContentLength)
734747
{
735-
// Don't set the Content-Length or Transfer-Encoding headers
736-
// automatically for HEAD requests or 101, 204, 205, 304 responses.
737-
if (Method != "HEAD" && StatusCanHaveBody(StatusCode))
748+
if (appCompleted)
738749
{
739750
// Since the app has completed and we are only now generating
740751
// the headers we can safely set the Content-Length to 0.
741752
responseHeaders.SetRawContentLength("0", _bytesContentLengthZero);
742753
}
743-
}
744-
else
745-
{
746-
// Note for future reference: never change this to set _autoChunk to true on HTTP/1.0
747-
// connections, even if we were to infer the client supports it because an HTTP/1.0 request
748-
// was received that used chunked encoding. Sending a chunked response to an HTTP/1.0
749-
// client would break compliance with RFC 7230 (section 3.3.1):
750-
//
751-
// A server MUST NOT send a response containing Transfer-Encoding unless the corresponding
752-
// request indicates HTTP/1.1 (or later).
753-
if (_httpVersion == Http.HttpVersion.Http11)
754-
{
755-
_autoChunk = true;
756-
responseHeaders.SetRawTransferEncoding("chunked", _bytesTransferEncodingChunked);
757-
}
758754
else
759755
{
760-
_keepAlive = false;
756+
// Note for future reference: never change this to set _autoChunk to true on HTTP/1.0
757+
// connections, even if we were to infer the client supports it because an HTTP/1.0 request
758+
// was received that used chunked encoding. Sending a chunked response to an HTTP/1.0
759+
// client would break compliance with RFC 7230 (section 3.3.1):
760+
//
761+
// A server MUST NOT send a response containing Transfer-Encoding unless the corresponding
762+
// request indicates HTTP/1.1 (or later).
763+
if (_httpVersion == Http.HttpVersion.Http11)
764+
{
765+
_autoChunk = true;
766+
responseHeaders.SetRawTransferEncoding("chunked", _bytesTransferEncodingChunked);
767+
}
768+
else
769+
{
770+
_keepAlive = false;
771+
}
761772
}
762773
}
763774
}
775+
else
776+
{
777+
// Don't set the Content-Length or Transfer-Encoding headers
778+
// automatically for HEAD requests or 101, 204, 205, 304 responses.
779+
if (responseHeaders.HasTransferEncoding)
780+
{
781+
RejectNonBodyTransferEncodingResponse(appCompleted);
782+
}
783+
}
784+
785+
responseHeaders.SetReadOnly();
764786

765787
if (!_keepAlive && !hasConnection && _httpVersion != Http.HttpVersion.Http10)
766788
{
@@ -1214,12 +1236,66 @@ public bool StatusCanHaveBody(int statusCode)
12141236
statusCode != 304;
12151237
}
12161238

1217-
private void ThrowResponseAlreadyStartedException(string value)
1239+
private void RejectNonBodyTransferEncodingResponse(bool appCompleted)
1240+
{
1241+
var ex = BadHttpResponseException.GetException(ResponseRejectionReasons.TransferEncodingSetOnNonBodyResponse, StatusCode.ToString());
1242+
if (!appCompleted)
1243+
{
1244+
// Back out of header creation surface exeception in user code
1245+
_requestProcessingStatus = RequestProcessingStatus.RequestStarted;
1246+
throw ex;
1247+
}
1248+
else
1249+
{
1250+
ReportApplicationError(ex);
1251+
// 500 Internal Server Error
1252+
ErrorResetHeadersToDefaults(statusCode: 500);
1253+
}
1254+
}
1255+
1256+
private void ErrorResetHeadersToDefaults(int statusCode)
1257+
{
1258+
StatusCode = statusCode;
1259+
ReasonPhrase = null;
1260+
1261+
var responseHeaders = FrameResponseHeaders;
1262+
responseHeaders.Reset();
1263+
var dateHeaderValues = DateHeaderValueManager.GetDateHeaderValues();
1264+
1265+
responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
1266+
responseHeaders.SetRawContentLength("0", _bytesContentLengthZero);
1267+
1268+
if (ServerOptions.AddServerHeader)
1269+
{
1270+
responseHeaders.SetRawServer(Constants.ServerName, _bytesServer);
1271+
}
1272+
}
1273+
1274+
public void HandleNonBodyResponseWrite(int count)
12181275
{
1219-
throw new InvalidOperationException(value + " cannot be set, response had already started.");
1276+
if (Method == "HEAD")
1277+
{
1278+
// Don't write to body for HEAD requests.
1279+
if (Log.IsEnabled(LogLevel.Debug))
1280+
{
1281+
Log.ConnectionHeadResponseBodyWrite(ConnectionId, count);
1282+
}
1283+
}
1284+
else
1285+
{
1286+
// Throw Exception for 101, 204, 205, 304 responses.
1287+
throw BadHttpResponseException.GetException(ResponseRejectionReasons.WriteToNonBodyResponse, StatusCode.ToString());
1288+
}
12201289
}
12211290

12221291
private void ThrowResponseAbortedException()
1292+
{
1293+
throw new ObjectDisposedException(
1294+
"The response has been aborted due to an unhandled application exception.",
1295+
_applicationException);
1296+
}
1297+
1298+
public void RejectRequest(string message)
12231299
{
12241300
throw new ObjectDisposedException(
12251301
"The response has been aborted due to an unhandled application exception.",

0 commit comments

Comments
 (0)