Skip to content

Commit 46ea97d

Browse files
committed
Ensure default response headers are only set if empty
This change moves it to be on the `Response.OnStarting` call rather than at the beginning of the request. As part of this, a bug in `HttpRequest.AppendHeader` was fixed.
1 parent 9921217 commit 46ea97d

File tree

3 files changed

+70
-6
lines changed

3 files changed

+70
-6
lines changed

src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SetDefaultResponseHeadersMiddleware.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ internal class SetDefaultResponseHeadersMiddleware
1414
{
1515
private readonly RequestDelegate _next;
1616

17-
private readonly (string Header, string Value)[] _defaultHeaders = new (string, string)[]
17+
private static readonly (string Header, string Value)[] _defaultHeaders = new (string, string)[]
1818
{
1919
(HeaderNames.CacheControl, CacheControlHeaderValue.PrivateString),
2020
(HeaderNames.ContentType, "text/html")
@@ -24,13 +24,20 @@ internal class SetDefaultResponseHeadersMiddleware
2424

2525
public Task InvokeAsync(HttpContext context)
2626
{
27-
foreach (var (header, value) in _defaultHeaders)
27+
context.Response.OnStarting(static state =>
2828
{
29-
if (!context.Response.Headers.ContainsKey(header))
29+
var context = (HttpContext)state;
30+
31+
foreach (var (header, value) in _defaultHeaders)
3032
{
31-
context.Response.Headers[header] = value;
33+
if (!context.Response.Headers.ContainsKey(header))
34+
{
35+
context.Response.Headers[header] = value;
36+
}
3237
}
33-
}
38+
39+
return Task.CompletedTask;
40+
}, context);
3441

3542
return _next(context);
3643
}

src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ public void AppendHeader(string name, string value)
173173
{
174174
if (_response.Headers.TryGetValue(name, out var existing))
175175
{
176-
_response.Headers.Add(name, StringValues.Concat(existing, value));
176+
_response.Headers[name] = StringValues.Concat(existing, value);
177177
}
178178
else
179179
{

test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/SetDefaultResponseHeadersMiddlewareTests.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Threading.Tasks;
56
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Http.Features;
68
using Xunit;
79

810
namespace Microsoft.AspNetCore.SystemWebAdapters.Tests;
@@ -14,11 +16,14 @@ public async Task NoHeaders()
1416
{
1517
// Arrange
1618
var context = new DefaultHttpContext();
19+
var startupFeature = new StartupCallbackFeature();
20+
context.Features.Set<IHttpResponseFeature>(startupFeature);
1721
var next = Task (HttpContextCore context) => Task.CompletedTask;
1822
var middleware = new SetDefaultResponseHeadersMiddleware(new(next));
1923

2024
// Act
2125
await middleware.InvokeAsync(context);
26+
await startupFeature.RunAsync();
2227

2328
// Assert
2429
Assert.Equal(2, context.Response.Headers.Count);
@@ -33,15 +38,67 @@ public async Task Existing(string name, string value)
3338
{
3439
// Arrange
3540
var context = new DefaultHttpContext();
41+
var startupFeature = new StartupCallbackFeature();
42+
context.Features.Set<IHttpResponseFeature>(startupFeature);
43+
3644
context.Response.Headers[name] = value;
3745

3846
var next = Task (HttpContextCore context) => Task.CompletedTask;
3947
var middleware = new SetDefaultResponseHeadersMiddleware(new(next));
4048

4149
// Act
4250
await middleware.InvokeAsync(context);
51+
await startupFeature.RunAsync();
52+
53+
// Assert
54+
Assert.Equal(value, context.Response.Headers[name]);
55+
}
56+
57+
[Theory]
58+
[InlineData("Content-Type", "some-content-type")]
59+
[InlineData("Cache-Control", "cache-value")]
60+
public async Task AddedInLaterMiddleware(string name, string value)
61+
{
62+
// Arrange
63+
var context = new DefaultHttpContext();
64+
var startupFeature = new StartupCallbackFeature();
65+
context.Features.Set<IHttpResponseFeature>(startupFeature);
66+
67+
var next = Task (HttpContextCore context) =>
68+
{
69+
var adapter = context.GetAdapter();
70+
adapter.Response.AppendHeader(name, value);
71+
72+
return Task.CompletedTask;
73+
};
74+
var middleware = new SetDefaultResponseHeadersMiddleware(new(next));
75+
76+
// Act
77+
await middleware.InvokeAsync(context);
78+
await startupFeature.RunAsync();
4379

4480
// Assert
4581
Assert.Equal(value, context.Response.Headers[name]);
4682
}
83+
84+
// The default feature does not include a function `OnStarting` method
85+
private class StartupCallbackFeature : HttpResponseFeature
86+
{
87+
private Func<Task>? _callback;
88+
89+
public override void OnStarting(Func<object, Task> callback, object state)
90+
{
91+
_callback = () => callback(state);
92+
}
93+
94+
public Task RunAsync()
95+
{
96+
if (_callback is null)
97+
{
98+
throw new InvalidOperationException();
99+
}
100+
101+
return _callback();
102+
}
103+
}
47104
}

0 commit comments

Comments
 (0)