Skip to content

Commit ab35454

Browse files
Fix IOutputCache empty-body bug (#1702): bump feature revision in InvokeFeatures.Set (#2364)
1 parent 4c9eac9 commit ab35454

3 files changed

Lines changed: 68 additions & 4 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"Projects": [
3+
{
4+
"Name": "Amazon.Lambda.AspNetCoreServer",
5+
"Type": "Patch",
6+
"ChangelogMessages": [
7+
"Fix InvokeFeatures.Set<TFeature> to bump the feature collection revision so middleware that wraps the response body (e.g. OutputCache, ResponseCompression) is properly visible to ASP.NET Core's FeatureReferences cache. Resolves https://github.com/aws/aws-lambda-dotnet/issues/1702 where IOutputCache stored empty response bodies."
8+
]
9+
}
10+
]
11+
}

Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/InvokeFeatures.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,11 @@ public IEnumerator<KeyValuePair<Type, object>> GetEnumerator()
117117

118118
public void Set<TFeature>(TFeature instance)
119119
{
120-
if (instance == null)
121-
return;
122-
123-
_features[typeof(TFeature)] = instance;
120+
// Delegate to the indexer so _containerRevision is bumped, otherwise
121+
// ASP.NET Core's FeatureReferences cache will return stale feature
122+
// references after middleware (e.g. OutputCache, ResponseCompression)
123+
// wraps the response body via HttpContext.Response.Body = wrapper.
124+
this[typeof(TFeature)] = instance;
124125
}
125126

126127
IEnumerator IEnumerable.GetEnumerator()

Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/UtilitiesTest.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,57 @@ public void EnsureStatusCodeStartsAtIs200()
2929
var feature = new InvokeFeatures() as IHttpResponseFeature;
3030
Assert.Equal(200, feature.StatusCode);
3131
}
32+
33+
// Regression test for https://github.com/aws/aws-lambda-dotnet/issues/1702.
34+
// ASP.NET Core's FeatureReferences cache uses Revision to detect when a
35+
// feature has been swapped (e.g. OutputCache/ResponseCompression replacing
36+
// IHttpResponseBodyFeature to wrap the response body). If Set<TFeature>
37+
// does not bump the revision, cached references stay stale and writes
38+
// bypass the wrapper.
39+
[Fact]
40+
public void SetFeatureBumpsRevision()
41+
{
42+
IFeatureCollection features = new InvokeFeatures();
43+
var initialRevision = features.Revision;
44+
45+
features.Set<IHttpResponseBodyFeature>(new TestResponseBodyFeature());
46+
47+
Assert.NotEqual(initialRevision, features.Revision);
48+
}
49+
50+
[Fact]
51+
public void SetFeatureStoresAndRetrievesInstance()
52+
{
53+
IFeatureCollection features = new InvokeFeatures();
54+
var replacement = new TestResponseBodyFeature();
55+
56+
features.Set<IHttpResponseBodyFeature>(replacement);
57+
58+
Assert.Same(replacement, features.Get<IHttpResponseBodyFeature>());
59+
}
60+
61+
[Fact]
62+
public void SetFeatureNullRemovesEntryAndBumpsRevision()
63+
{
64+
IFeatureCollection features = new InvokeFeatures();
65+
// InvokeFeatures seeds itself as the IHttpResponseBodyFeature in its constructor.
66+
Assert.NotNull(features.Get<IHttpResponseBodyFeature>());
67+
var revisionBeforeRemove = features.Revision;
68+
69+
features.Set<IHttpResponseBodyFeature>(null);
70+
71+
Assert.Null(features.Get<IHttpResponseBodyFeature>());
72+
Assert.NotEqual(revisionBeforeRemove, features.Revision);
73+
}
74+
75+
private sealed class TestResponseBodyFeature : IHttpResponseBodyFeature
76+
{
77+
public System.IO.Stream Stream => System.IO.Stream.Null;
78+
public System.IO.Pipelines.PipeWriter Writer => System.IO.Pipelines.PipeWriter.Create(System.IO.Stream.Null);
79+
public System.Threading.Tasks.Task CompleteAsync() => System.Threading.Tasks.Task.CompletedTask;
80+
public void DisableBuffering() { }
81+
public System.Threading.Tasks.Task SendFileAsync(string path, long offset, long? count, System.Threading.CancellationToken cancellationToken) => System.Threading.Tasks.Task.CompletedTask;
82+
public System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken) => System.Threading.Tasks.Task.CompletedTask;
83+
}
3284
}
3385
}

0 commit comments

Comments
 (0)