Skip to content

Commit b208053

Browse files
[release/7.0] Prevent NRE when body is not set (#46613)
* Prevent NRE when body is not set Fixes #46333 * Typo * PR feedback * PR feedback --------- Co-authored-by: Sebastien Ros <[email protected]>
1 parent ffd304a commit b208053

File tree

3 files changed

+107
-24
lines changed

3 files changed

+107
-24
lines changed

src/Middleware/OutputCaching/src/OutputCacheEntry.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ internal sealed class OutputCacheEntry
2020
/// <summary>
2121
/// Gets the headers of the cache entry.
2222
/// </summary>
23-
public HeaderDictionary Headers { get; set; } = default!;
23+
public HeaderDictionary? Headers { get; set; }
2424

2525
/// <summary>
2626
/// Gets the body of the cache entry.
2727
/// </summary>
28-
public CachedResponseBody Body { get; set; } = default!;
28+
public CachedResponseBody? Body { get; set; }
2929

3030
/// <summary>
3131
/// Gets the tags of the cache entry.

src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ private async Task InvokeAwaited(HttpContext httpContext, IReadOnlyList<IOutputC
119119
// Should we store the response to this request?
120120
if (context.AllowCacheStorage)
121121
{
122-
// It is also a pre-condition to reponse locking
122+
// It is also a pre-condition to response locking
123123

124124
var executed = false;
125125

@@ -172,6 +172,14 @@ private async Task InvokeAwaited(HttpContext httpContext, IReadOnlyList<IOutputC
172172
UnshimResponseStream(context);
173173
}
174174

175+
// If the policies prevented this response from being cached we can't reuse it for other
176+
// pending requests
177+
178+
if (!context.AllowCacheStorage)
179+
{
180+
return null;
181+
}
182+
175183
return context.CachedResponse;
176184
}
177185

@@ -277,9 +285,13 @@ internal async Task<bool> TryServeCachedResponseAsync(OutputCacheContext context
277285
var response = context.HttpContext.Response;
278286
// Copy the cached status code and response headers
279287
response.StatusCode = context.CachedResponse.StatusCode;
280-
foreach (var header in context.CachedResponse.Headers)
288+
289+
if (context.CachedResponse.Headers != null)
281290
{
282-
response.Headers[header.Key] = header.Value;
291+
foreach (var header in context.CachedResponse.Headers)
292+
{
293+
response.Headers[header.Key] = header.Value;
294+
}
283295
}
284296

285297
// Note: int64 division truncates result and errors may be up to 1 second. This reduction in
@@ -289,7 +301,8 @@ internal async Task<bool> TryServeCachedResponseAsync(OutputCacheContext context
289301

290302
// Copy the cached response body
291303
var body = context.CachedResponse.Body;
292-
if (body.Length > 0)
304+
305+
if (body != null && body.Length > 0)
293306
{
294307
try
295308
{
@@ -372,12 +385,13 @@ internal void FinalizeCacheHeaders(OutputCacheContext context)
372385
{
373386
Created = context.ResponseTime!.Value,
374387
StatusCode = response.StatusCode,
375-
Headers = new HeaderDictionary(),
376388
Tags = context.Tags.ToArray()
377389
};
378390

379391
foreach (var header in headers)
380392
{
393+
context.CachedResponse.Headers ??= new();
394+
381395
if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase))
382396
{
383397
context.CachedResponse.Headers[header.Key] = header.Value;
@@ -402,6 +416,7 @@ internal async ValueTask FinalizeCacheBodyAsync(OutputCacheContext context)
402416

403417
var contentLength = context.HttpContext.Response.ContentLength;
404418
var cachedResponseBody = context.OutputCacheStream.GetCachedResponseBody();
419+
405420
if (!contentLength.HasValue || contentLength == cachedResponseBody.Length
406421
|| (cachedResponseBody.Length == 0
407422
&& HttpMethods.IsHead(context.HttpContext.Request.Method)))
@@ -410,6 +425,7 @@ internal async ValueTask FinalizeCacheBodyAsync(OutputCacheContext context)
410425
// Add a content-length if required
411426
if (!response.ContentLength.HasValue && StringValues.IsNullOrEmpty(response.Headers.TransferEncoding))
412427
{
428+
context.CachedResponse.Headers ??= new();
413429
context.CachedResponse.Headers.ContentLength = cachedResponseBody.Length;
414430
}
415431

@@ -508,13 +524,13 @@ internal bool ContentIsNotModified(OutputCacheContext context)
508524
return true;
509525
}
510526

511-
if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag])
527+
if (cachedResponseHeaders != null && !StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag])
512528
&& EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag].ToString(), out var eTag)
513-
&& EntityTagHeaderValue.TryParseList(ifNoneMatchHeader, out var ifNoneMatchEtags))
529+
&& EntityTagHeaderValue.TryParseList(ifNoneMatchHeader, out var ifNoneMatchETags))
514530
{
515-
for (var i = 0; i < ifNoneMatchEtags?.Count; i++)
531+
for (var i = 0; i < ifNoneMatchETags?.Count; i++)
516532
{
517-
var requestETag = ifNoneMatchEtags[i];
533+
var requestETag = ifNoneMatchETags[i];
518534
if (eTag.Compare(requestETag, useStrongComparison: false))
519535
{
520536
_logger.NotModifiedIfNoneMatchMatched(requestETag);
@@ -528,6 +544,11 @@ internal bool ContentIsNotModified(OutputCacheContext context)
528544
var ifModifiedSince = context.HttpContext.Request.Headers.IfModifiedSince;
529545
if (!StringValues.IsNullOrEmpty(ifModifiedSince))
530546
{
547+
if (cachedResponseHeaders == null)
548+
{
549+
return false;
550+
}
551+
531552
if (!HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.LastModified].ToString(), out var modified) &&
532553
!HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.Date].ToString(), out modified))
533554
{

src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
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.Globalization;
5+
using System.Text;
6+
using System.Text.Unicode;
47
using Microsoft.AspNetCore.Http;
58
using Microsoft.AspNetCore.Http.Features;
69
using Microsoft.AspNetCore.OutputCaching.Memory;
@@ -150,7 +153,7 @@ public void ContentIsNotModified_NotConditionalRequest_False()
150153
}
151154

152155
[Fact]
153-
public void ContentIsNotModified_IfModifiedSince_FallsbackToDateHeader()
156+
public void ContentIsNotModified_IfModifiedSince_FallsBackToDateHeader()
154157
{
155158
var utcNow = DateTimeOffset.UtcNow;
156159
var sink = new TestSink();
@@ -329,7 +332,7 @@ public void ContentIsNotModified_IfNoneMatch_MatchesAtLeastOneValue_True()
329332
}
330333

331334
[Fact]
332-
public void StartResponsegAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime()
335+
public void StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime()
333336
{
334337
var clock = new TestClock
335338
{
@@ -798,10 +801,8 @@ public async Task Locking_PreventsConcurrentRequests()
798801
// Wait for the second request to start before processing the first one
799802
task2Executing.Wait();
800803

801-
// Simluate some delay to allow for the second request to run while this one is pending
804+
// Simulate some delay to allow for the second request to run while this one is pending
802805
await Task.Delay(500);
803-
804-
c.Response.Write("Hello" + responseCounter);
805806
});
806807

807808
var context1 = TestUtils.CreateTestContext();
@@ -826,35 +827,96 @@ public async Task Locking_PreventsConcurrentRequests()
826827
Assert.Equal(1, responseCounter);
827828
}
828829

830+
[Fact]
831+
public async Task Locking_IgnoresNonCacheableResponses()
832+
{
833+
var responseCounter = 0;
834+
835+
var blocker1 = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
836+
var blocker2 = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
837+
838+
var memoryStream1 = new MemoryStream();
839+
var memoryStream2 = new MemoryStream();
840+
841+
var options = new OutputCacheOptions();
842+
options.AddBasePolicy(build => build.Cache());
843+
844+
var middleware = TestUtils.CreateTestMiddleware(options: options, next: async c =>
845+
{
846+
responseCounter++;
847+
848+
if (responseCounter == 1)
849+
{
850+
blocker1.SetResult(true);
851+
}
852+
853+
c.Response.Cookies.Append("a", "b");
854+
c.Response.Write("Hello" + responseCounter);
855+
856+
await blocker2.Task;
857+
});
858+
859+
var context1 = TestUtils.CreateTestContext();
860+
context1.HttpContext.Request.Method = "GET";
861+
context1.HttpContext.Request.Path = "/";
862+
context1.HttpContext.Response.Body = memoryStream1;
863+
864+
var context2 = TestUtils.CreateTestContext();
865+
context2.HttpContext.Request.Method = "GET";
866+
context2.HttpContext.Request.Path = "/";
867+
context2.HttpContext.Response.Body = memoryStream2;
868+
869+
var task1 = Task.Run(() => middleware.Invoke(context1.HttpContext));
870+
871+
// Wait for context1 to be processed
872+
await blocker1.Task;
873+
874+
// Start context2
875+
var task2 = Task.Run(() => middleware.Invoke(context2.HttpContext));
876+
877+
// Wait for it to be blocked by the locking feature
878+
await Task.Delay(500);
879+
880+
// Unblock context1
881+
blocker2.SetResult(true);
882+
883+
await Task.WhenAll(task1, task2);
884+
885+
Assert.Equal(2, responseCounter);
886+
887+
// Ensure that even though two requests were processed, no result was returned from cache
888+
Assert.Equal("Hello1", Encoding.UTF8.GetString(memoryStream1.ToArray()));
889+
Assert.Equal("Hello2", Encoding.UTF8.GetString(memoryStream2.ToArray()));
890+
}
891+
829892
[Fact]
830893
public async Task Locking_ExecuteAllRequestsWhenDisabled()
831894
{
832895
var responseCounter = 0;
833896

834-
var task1Executing = new ManualResetEventSlim(false);
835-
var task2Executing = new ManualResetEventSlim(false);
897+
var blocker1 = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
898+
var blocker2 = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
836899

837900
var options = new OutputCacheOptions();
838901
options.AddBasePolicy(build => build.Cache().SetLocking(false));
839902

840-
var middleware = TestUtils.CreateTestMiddleware(options: options, next: c =>
903+
var middleware = TestUtils.CreateTestMiddleware(options: options, next: async c =>
841904
{
842905
responseCounter++;
843906

844907
switch (responseCounter)
845908
{
846909
case 1:
847-
task1Executing.Set();
848-
task2Executing.Wait();
910+
blocker1.SetResult(true);
911+
await blocker2.Task;
849912
break;
850913
case 2:
851-
task1Executing.Wait();
852-
task2Executing.Set();
914+
await blocker1.Task;
915+
blocker2.SetResult(true);
853916
break;
854917
}
855918

856919
c.Response.Write("Hello" + responseCounter);
857-
return Task.CompletedTask;
858920
});
859921

860922
var context1 = TestUtils.CreateTestContext();

0 commit comments

Comments
 (0)