Skip to content

Commit 3f4e232

Browse files
author
Cesar Blum Silveira
committed
Throw when setting Frame.StatusCode or Frame.ReasonPhrase after response has already started (#805).
1 parent cb284b9 commit 3f4e232

File tree

5 files changed

+193
-54
lines changed

5 files changed

+193
-54
lines changed

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

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,42 @@ public string HttpVersion
121121
public IHeaderDictionary RequestHeaders { get; set; }
122122
public Stream RequestBody { get; set; }
123123

124-
public int StatusCode { get; set; }
125-
public string ReasonPhrase { get; set; }
124+
private int _statusCode;
125+
public int StatusCode
126+
{
127+
get
128+
{
129+
return _statusCode;
130+
}
131+
set
132+
{
133+
if (HasResponseStarted)
134+
{
135+
throw new InvalidOperationException("Status code cannot be set, response has already started.");
136+
}
137+
138+
_statusCode = value;
139+
}
140+
}
141+
142+
private string _reasonPhrase;
143+
public string ReasonPhrase
144+
{
145+
get
146+
{
147+
return _reasonPhrase;
148+
}
149+
set
150+
{
151+
if (HasResponseStarted)
152+
{
153+
throw new InvalidOperationException("Reason phrase cannot be set, response had already started.");
154+
}
155+
156+
_reasonPhrase = value;
157+
}
158+
}
159+
126160
public IHeaderDictionary ResponseHeaders { get; set; }
127161
public Stream ResponseBody { get; set; }
128162

@@ -579,6 +613,13 @@ protected Task ProduceEnd()
579613
{
580614
if (_requestRejected || _applicationException != null)
581615
{
616+
if (HasResponseStarted)
617+
{
618+
// We can no longer change the response, so we simply close the connection.
619+
_requestProcessingStopping = true;
620+
return TaskUtilities.CompletedTask;
621+
}
622+
582623
if (_requestRejected)
583624
{
584625
// 400 Bad Request
@@ -591,30 +632,21 @@ protected Task ProduceEnd()
591632
StatusCode = 500;
592633
}
593634

594-
if (HasResponseStarted)
595-
{
596-
// We can no longer respond with a 500, so we simply close the connection.
597-
_requestProcessingStopping = true;
598-
return TaskUtilities.CompletedTask;
599-
}
600-
else
601-
{
602-
ReasonPhrase = null;
603-
604-
var responseHeaders = _frameHeaders.ResponseHeaders;
605-
responseHeaders.Reset();
606-
var dateHeaderValues = DateHeaderValueManager.GetDateHeaderValues();
635+
ReasonPhrase = null;
607636

608-
responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
609-
responseHeaders.SetRawContentLength("0", _bytesContentLengthZero);
637+
var responseHeaders = _frameHeaders.ResponseHeaders;
638+
responseHeaders.Reset();
639+
var dateHeaderValues = DateHeaderValueManager.GetDateHeaderValues();
610640

611-
if (ServerOptions.AddServerHeader)
612-
{
613-
responseHeaders.SetRawServer(Constants.ServerName, Headers.BytesServer);
614-
}
641+
responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
642+
responseHeaders.SetRawContentLength("0", _bytesContentLengthZero);
615643

616-
ResponseHeaders = responseHeaders;
644+
if (ServerOptions.AddServerHeader)
645+
{
646+
responseHeaders.SetRawServer(Constants.ServerName, Headers.BytesServer);
617647
}
648+
649+
ResponseHeaders = responseHeaders;
618650
}
619651

620652
if (!HasResponseStarted)

test/Microsoft.AspNetCore.Server.KestrelTests/FrameFacts.cs

Lines changed: 0 additions & 32 deletions
This file was deleted.

test/Microsoft.AspNetCore.Server.KestrelTests/FrameResponseHeadersTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,5 +125,47 @@ public void AddingControlCharactersToHeadersThrows(string key, string value)
125125
((IDictionary<string, StringValues>)responseHeaders).Add(key, value);
126126
});
127127
}
128+
129+
[Fact]
130+
public void ThrowsWhenAddingHeaderAfterReadOnlyIsSet()
131+
{
132+
var headers = new FrameResponseHeaders();
133+
headers.SetReadOnly();
134+
135+
Assert.Throws<InvalidOperationException>(() => ((IDictionary<string, StringValues>)headers).Add("my-header", new[] { "value" }));
136+
}
137+
138+
[Fact]
139+
public void ThrowsWhenChangingHeaderAfterReadOnlyIsSet()
140+
{
141+
var headers = new FrameResponseHeaders();
142+
var dictionary = (IDictionary<string, StringValues>)headers;
143+
dictionary.Add("my-header", new[] { "value" });
144+
headers.SetReadOnly();
145+
146+
Assert.Throws<InvalidOperationException>(() => dictionary["my-header"] = "other-value");
147+
}
148+
149+
[Fact]
150+
public void ThrowsWhenRemovingHeaderAfterReadOnlyIsSet()
151+
{
152+
var headers = new FrameResponseHeaders();
153+
var dictionary = (IDictionary<string, StringValues>)headers;
154+
dictionary.Add("my-header", new[] { "value" });
155+
headers.SetReadOnly();
156+
157+
Assert.Throws<InvalidOperationException>(() => dictionary.Remove("my-header"));
158+
}
159+
160+
[Fact]
161+
public void ThrowsWhenClearingHeadersAfterReadOnlyIsSet()
162+
{
163+
var headers = new FrameResponseHeaders();
164+
var dictionary = (IDictionary<string, StringValues>)headers;
165+
dictionary.Add("my-header", new[] { "value" });
166+
headers.SetReadOnly();
167+
168+
Assert.Throws<InvalidOperationException>(() => dictionary.Clear());
169+
}
128170
}
129171
}

test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
using System;
55
using System.Linq;
66
using System.Text;
7+
using Microsoft.AspNetCore.Http.Features;
78
using Microsoft.AspNetCore.Server.Kestrel;
89
using Microsoft.AspNetCore.Server.Kestrel.Http;
910
using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
11+
using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers;
1012
using Xunit;
1113

1214
namespace Microsoft.AspNetCore.Server.KestrelTests
@@ -50,5 +52,68 @@ public void EmptyHeaderValuesCanBeParsed(string rawHeaders, int numHeaders)
5052
Assert.True(scan.IsEnd);
5153
}
5254
}
55+
56+
[Fact]
57+
public void ResetResetsScheme()
58+
{
59+
// Arrange
60+
var connectionContext = new ConnectionContext()
61+
{
62+
DateHeaderValueManager = new DateHeaderValueManager(),
63+
ServerAddress = ServerAddress.FromUrl("http://localhost:5000")
64+
};
65+
var frame = new Frame<object>(application: null, context: connectionContext);
66+
frame.Scheme = "https";
67+
68+
// Act
69+
frame.Reset();
70+
71+
// Assert
72+
Assert.Equal("http", ((IFeatureCollection)frame).Get<IHttpRequestFeature>().Scheme);
73+
}
74+
75+
[Fact]
76+
public void ThrowsWhenStatusCodeIsSetAfterResponseStarted()
77+
{
78+
// Arrange
79+
var connectionContext = new ConnectionContext()
80+
{
81+
DateHeaderValueManager = new DateHeaderValueManager(),
82+
ServerAddress = ServerAddress.FromUrl("http://localhost:5000"),
83+
ServerOptions = new KestrelServerOptions(),
84+
SocketOutput = new MockSocketOuptut()
85+
};
86+
var frame = new Frame<object>(application: null, context: connectionContext);
87+
frame.InitializeHeaders();
88+
89+
// Act
90+
frame.Write(new ArraySegment<byte>(new byte[1]));
91+
92+
// Assert
93+
Assert.True(frame.HasResponseStarted);
94+
Assert.Throws<InvalidOperationException>(() => ((IHttpResponseFeature)frame).StatusCode = 404);
95+
}
96+
97+
[Fact]
98+
public void ThrowsWhenReasonPhraseIsSetAfterResponseStarted()
99+
{
100+
// Arrange
101+
var connectionContext = new ConnectionContext()
102+
{
103+
DateHeaderValueManager = new DateHeaderValueManager(),
104+
ServerAddress = ServerAddress.FromUrl("http://localhost:5000"),
105+
ServerOptions = new KestrelServerOptions(),
106+
SocketOutput = new MockSocketOuptut()
107+
};
108+
var frame = new Frame<object>(application: null, context: connectionContext);
109+
frame.InitializeHeaders();
110+
111+
// Act
112+
frame.Write(new ArraySegment<byte>(new byte[1]));
113+
114+
// Assert
115+
Assert.True(frame.HasResponseStarted);
116+
Assert.Throws<InvalidOperationException>(() => ((IHttpResponseFeature)frame).ReasonPhrase = "Reason phrase");
117+
}
53118
}
54119
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Server.Kestrel.Http;
8+
using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
9+
10+
namespace Microsoft.AspNetCore.Server.KestrelTests.TestHelpers
11+
{
12+
public class MockSocketOuptut : ISocketOutput
13+
{
14+
public void ProducingComplete(MemoryPoolIterator end)
15+
{
16+
}
17+
18+
public MemoryPoolIterator ProducingStart()
19+
{
20+
return new MemoryPoolIterator();
21+
}
22+
23+
public void Write(ArraySegment<byte> buffer, bool chunk = false)
24+
{
25+
}
26+
27+
public Task WriteAsync(ArraySegment<byte> buffer, bool chunk = false, CancellationToken cancellationToken = default(CancellationToken))
28+
{
29+
return Task.FromResult(0);
30+
}
31+
}
32+
}

0 commit comments

Comments
 (0)