Fix IOutputCache empty-body bug (#1702): bump feature revision in InvokeFeatures.Set#2364
Open
GarrettBeatty wants to merge 2 commits into
Open
Fix IOutputCache empty-body bug (#1702): bump feature revision in InvokeFeatures.Set#2364GarrettBeatty wants to merge 2 commits into
GarrettBeatty wants to merge 2 commits into
Conversation
InvokeFeatures.Set<TFeature> was updating the feature dictionary without incrementing _containerRevision. ASP.NET Core's FeatureReferences cache uses Collection.Revision to detect feature swaps; without the bump, cached feature references stay stale after middleware (e.g. OutputCache, ResponseCompression) replaces a feature via HttpContext.Response.Body = wrapper. The result was that response writes bypassed the wrapper entirely, so OutputCache cached zero-byte bodies and subsequent cached responses returned empty. Resolves #1702. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 12, 2026
Collaborator
Author
|
@normj tbh i dont fully understand this fix or this area of the code, but from testing at least it seems to fix the issue. I am not really sure on the full implications of this change |
Collaborator
Author
|
I can add a unit test if the change looks good |
Regression tests for issue #1702: verify that Set<TFeature> bumps the feature collection revision so ASP.NET Core's FeatureReferences cache sees feature swaps, and that Set<TFeature>(null) removes the entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
IOutputCache(and any other middleware that wraps the response body) caches/sees zero bytes when an ASP.NET Core app is hosted in Lambda.Internal/InvokeFeatures.cs:Set<TFeature>now delegates to the indexer so_containerRevisionis bumped on every mutation.The bug
When middleware does
HttpContext.Response.Body = wrapperStream, ASP.NET Core'sDefaultHttpResponse.Body.setreplaces the activeIHttpResponseBodyFeatureviaFeatures.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(value, oldFeature))(source).ASP.NET Core caches feature lookups via
FeatureReferences<T>.Fetch, which only re-reads the dictionary whenCollection.Revisionchanges (source).Our
InvokeFeatures.Set<TFeature>was bypassing the indexer:So after
Response.Body = OutputCacheStream:IHttpResponseBodyFeatureis updated to the newStreamResponseBodyFeaturewrappingOutputCacheStream._containerRevisionis not bumped.IHttpResponseBodyFeaturereference (and thePipeWriterit produced) still point at the original feature, which writes directly to the underlyingMemoryStream.WriteAsJsonAsync(and friends) →response.BodyWriter→ cached writer → bytes flow into theMemoryStreamand never visitOutputCacheStream.OutputCacheMiddleware.FinalizeCacheBodyAsyncreadscontext.OutputCacheStream.GetCachedResponseBody()→ empty → caches an empty entry.The fix
The indexer setter (just above in the same file) was already correct — it increments
_containerRevisionon every change and supports null-removal per theIFeatureCollectioncontract. Delegating to it makes both code paths behave identically. No other change is needed; the bug is invisible until middleware swaps a feature mid-request.This change also helps any other middleware that wraps the body via
Response.Body =/Response.BodyWriter =(e.g.ResponseCompression).How we tested
End-to-end on real AWS Lambda + API Gateway HTTP API v2:
Reproduced the bug on
dev(unfixed): deployed a minimal app —services.AddOutputCache()+app.UseOutputCache()+MapGet(\"/api/values\", () => new[]{\"value1\",\"value2\"}). First request returnedContent-Length: 19, body[\"value1\",\"value2\"]. Every subsequent request returnedContent-Length: 0, empty body, withageheader confirming it was served from the cache.Verified PR #2150 does not fix it. That PR adds
responseFeatures.Body.Position = 0Lafter marshalling. Deployed it; bug still repros. A diag endpoint confirmed the underlyingMemoryStreamhadlen=19, pos=0after the request — the position reset worked, but it was on the wrong stream. The OutputCache buffer was always empty because writes never reachedOutputCacheStream.Verified the real fix works. Built a NuGet from this branch, redeployed the same sample. Result:
All four requests return the full body. Requests 2-4 carry the
ageheader, proving they're served from the OutputCache (and that the cache contains real bytes, not zero-length).Test plan
devAmazon.Lambda.AspNetCoreServer.Testsuite passesNotes
Position = 0Lline addresses a symptom on the wrong stream and has no effect on the OutputCache scenario.APIGatewayProxyFunction,APIGatewayHttpApiV2ProxyFunction,ApplicationLoadBalancerFunction) because they all shareInvokeFeatures.🤖 Generated with Claude Code