Skip to content

Fix OutputCache caching truncated responses for aborted requests - #66954

Closed
Bellambharath wants to merge 1 commit into
dotnet:mainfrom
Bellambharath:fix/output-cache-truncated-aborted-response
Closed

Fix OutputCache caching truncated responses for aborted requests#66954
Bellambharath wants to merge 1 commit into
dotnet:mainfrom
Bellambharath:fix/output-cache-truncated-aborted-response

Conversation

@Bellambharath

@Bellambharath Bellambharath commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Fixes #66877

Problem

OutputCacheMiddleware can cache an empty or truncated response when the request that populates the cache is canceled. The broken entry is then served to all subsequent clients until it expires.

This happens because the existing cacheability checks have two blind spots:

The Content-Length check is bypassed when the header is absent (e.g. ResponseCompression removes it).

A decorator stream layered above OutputCacheStream (e.g. GZipStream) can throw OperationCanceledException before the write reaches OutputCacheStream, so BufferingEnabled stays true even though the body is incomplete.

The locking path (SetLocking(true)) has an additional issue: concurrent requests waiting on the in-flight response are served directly from the OutputCacheEntry returned by ExecuteResponseAsync, bypassing the normal cacheability checks entirely.

Fix

Two targeted changes to OutputCacheMiddleware.cs:

  1. FinalizeCacheBodyAsync — skip storing if RequestAborted is canceled:
// Do not cache a response whose originating request was aborted.
if (context.HttpContext.RequestAborted.IsCancellationRequested)
{
    _logger.ResponseNotCached();
    return;
}

This is the right place because it is the single choke-point before every StoreAsync call. RequestAborted being canceled at this point is a strong, observable signal that the response body is incomplete — it is the token passed to WriteAsync in virtually every IActionResult implementation, and callers are expected to call httpContext.Abort() after catching OperationCanceledException mid-write.

  1. ExecuteResponseAsync (locking path) — return null if the request was aborted after _next completes:
if (httpContext.RequestAborted.IsCancellationRequested)
{
    _logger.ResponseNotCached();
    return null;
}

Returning null here causes TryServeCachedResponseAsync to short-circuit, so no truncated entry is handed off to waiting concurrent requests.

Testing

The existing test FinalizeCacheBody_Cache_IfContentLengthAbsent and the locking tests continue to pass unchanged. The repro from the issue (OutputCache + ResponseCompression, client cancels after 1 s, subsequent request should get full payload) is the end-to-end verification scenario.

When a request is canceled (RequestAborted), OutputCacheMiddleware
could store an empty or truncated response in the cache if:

1. No Content-Length header is present (e.g. removed by ResponseCompression)
2. A decorator stream above OutputCacheStream throws before the write
   reaches OutputCacheStream, so BufferingEnabled stays true

Fix:
- In FinalizeCacheBodyAsync, skip storing if RequestAborted is canceled
- In ExecuteResponseAsync (locking path), return null if the request was
  aborted so that waiting concurrent requests are not served a truncated
  response via TryServeCachedResponseAsync

Fixes dotnet#66877
@github-actions github-actions Bot added the needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically label Jun 1, 2026
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Jun 1, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Thanks for your PR, @Bellambharath. Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@gfoidl gfoidl added area-middleware Includes: URL rewrite, redirect, response cache/compression, session, and other general middlewares and removed needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically labels Jun 1, 2026

@BrennanConroy BrennanConroy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a test or 2 here. Aborting the request in the middle of writing a response and seeing that future requests don't get the truncated response, and making sure the aborted request doesn't re-run the request pipeline.

if (httpContext.RequestAborted.IsCancellationRequested)
{
_logger.ResponseNotCached();
return null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be setting executed = true; here so the aborted request doesn't try to serve a cached response or re-run the request pipeline.

@BrennanConroy BrennanConroy added the pr: pending author input For automation. Specifically separate from Needs: Author Feedback label Jun 30, 2026
@BrennanConroy

Copy link
Copy Markdown
Member

@Bellambharath are you interested in continuing with this contribution?

@DeagleGross

Copy link
Copy Markdown
Member

Thanks for contribution @Bellambharath; we have an active PR solving same issue here: #68683.
Closing this one

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-middleware Includes: URL rewrite, redirect, response cache/compression, session, and other general middlewares community-contribution Indicates that the PR has been added by a community member pr: pending author input For automation. Specifically separate from Needs: Author Feedback

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OutputCache caches truncated responses for aborted requests

4 participants