Skip to content

Correct response caching of origin-dependent and faulted responses #12733#12734

Open
yasmoradi wants to merge 1 commit into
bitfoundation:developfrom
yasmoradi:fix/response-cache-origin-and-faulted-responses
Open

Correct response caching of origin-dependent and faulted responses #12733#12734
yasmoradi wants to merge 1 commit into
bitfoundation:developfrom
yasmoradi:fix/response-cache-origin-and-faulted-responses

Conversation

@yasmoradi

@yasmoradi yasmoradi commented Jul 20, 2026

Copy link
Copy Markdown
Member

Closes #12733

1. GetExternalSignInUri is no longer shared-cacheable

The URL this action returns embeds origin = Request.GetWebAppUrl(), and GetWebAppUrl() falls back to the X-Origin request header when the origin is not in the query string. AppResponseCachePolicy varies by host, query keys and culture, so X-Origin is in neither the output cache's vary key nor a CDN's cache key, and a stored response did not depend only on the inputs it was keyed by.

The AppResponseCache attribute is simply removed. Caching buys little here, and a vary rule would not be a real fix since CDNs do not honour Vary on arbitrary headers reliably.

2. A response the output cache refuses to store no longer advertises shared caching

CacheRequestAsync writes Cache-Control before the endpoint runs. ServeResponseAsync already set AllowCacheStorage = false for non-200 responses and for responses carrying a non-culture Set-Cookie, but left that header untouched, so CDNs and browsers kept caching a response ASP.NET Core had just declined to store. A 404 from /api/v1/ProductView/Get/{id} went out with public, s-maxage=604800, leaving a stale 404 on the edge for up to seven days after the product was created.

The header is now downgraded to no-store, private alongside AllowCacheStorage.

The response.HasStarted is false guard matters: ServeResponseAsync also runs for streamed responses, whose headers are already flushed and read-only — without it the middleware throws InvalidOperationException: Headers are read-only, response has already started. That path needs no downgrade anyway, since enabling shared caching is exactly what suppresses streaming pre-rendering (see HttpRequestExtensions.IsStreamPrerenderingSuppressed).

Also included

IdentityController.WebAuthn assertion options:

  • The cache key was built as new string(challenge.Select(b => (char)b)), putting NUL and control characters into the Redis key. It is now base64url encoded.
  • The options were deliberately left in the cache until they expired, because the two-factor round trip verifies the same client response a second time. They are now evicted once the flow reaches its final step, so a WebAuthn challenge stops being reusable for the remainder of its 3 minute window. SignIn throws on an invalid two-factor code, so a failed attempt still leaves the options in place for a retry.

Verification

  • dotnet build on Boilerplate.Server.Web — succeeds, no new warnings.
  • dotnet test --filter "TestCategory=Caching" — passes.
  • dotnet test --filter "TestCategory=SEO" — 3/3 pass.

Prerendering_WithOutputCaching_Should_ReturnCompleteNonStreamedHomePage is what caught the missing HasStarted guard.

Summary by CodeRabbit

  • Bug Fixes
    • External sign-in URLs are now generated per request and no longer reused from cache.
    • Output caching is better isolated by tenant and request path, and non-cacheable responses are reliably marked private and not stored.
    • WebAuthn two-factor verification now keeps intermediate state correctly and applies credential counter changes only on completion.
    • Culture prefixes in URLs are removed only when they appear at the start of the path.

@yasmoradi
yasmoradi requested a review from msynk July 20, 2026 20:55
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The Boilerplate template disables origin-dependent sign-in caching, refines WebAuthn assertion cache handling, strengthens response-cache variation and cacheability rules, updates route markup and culture-prefix removal, normalizes cache eviction tags, and limits FusionCache auto-cloning to development.

Changes

Boilerplate corrections

Layer / File(s) Summary
Correct identity cache flows
src/Templates/Boilerplate/.../IdentityController.ExternalSignIn.cs, src/Templates/Boilerplate/.../IdentityController.WebAuthn.cs
Removes caching from origin-dependent sign-in URLs, uses base64url WebAuthn cache keys, and retains assertion options and credential updates until the final two-factor step.
Correct response-cache policy
src/Templates/Boilerplate/.../AppResponseCachePolicy.cs, src/Templates/Boilerplate/.../ResponseCacheService.cs
Adds tenant variation, preserves configured TTLs, varies edge caching by X-Origin, marks non-cacheable responses NoStore and Private, and normalizes cache eviction tags.
Update routing and URI handling
src/Templates/Boilerplate/.../Routes.razor, src/Templates/Boilerplate/.../UriExtensions.cs
Replaces the router content wrapper and removes culture prefixes only from the leading URL path segment.
Scope cache cloning by environment
src/Templates/Boilerplate/.../WebApplicationBuilderExtensions.cs
Enables FusionCache auto-cloning only in development environments.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: msynk

Poem

A rabbit hops through cache-control hay,
Encoding challenges brightens the day.
Origins stay safely out of the store,
Tenants keep their own cache door.
Final steps count credentials right—
No stale responses take flight.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also changes WebAuthn caching, router markup, URI culture stripping, and auto-clone behavior, which are not part of #12733. Split the unrelated WebAuthn, routing, URI, and auto-clone changes into separate PRs, keeping this one focused on response-caching fixes.
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: correcting response caching for origin-dependent and faulted responses.
Linked Issues check ✅ Passed The changes remove caching from origin-dependent sign-in URLs and revise caching headers so declined or faulted responses are not publicly cached.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@yasmoradi yasmoradi changed the title fix(templates): correct response caching of origin-dependent and faulted responses #12733 Correct response caching of origin-dependent and faulted responses #12733 Jul 21, 2026
@yasmoradi
yasmoradi force-pushed the fix/response-cache-origin-and-faulted-responses branch from 9b21bde to 8399f54 Compare July 21, 2026 10:00
@yasmoradi
yasmoradi force-pushed the fix/response-cache-origin-and-faulted-responses branch from 8399f54 to 3bac8a8 Compare July 21, 2026 10:56
@yasmoradi

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Infrastructure/Services/ResponseCacheService.cs (1)

22-27: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Guard against empty arrays and ensure cache tags match the absolute path format.

If PurgeCache is invoked without arguments, the resulting empty array passed to the Cloudflare API ({"files":[]}) can cause a 400 Bad Request and throw an exception.

Additionally, because the output-cache tagging uses Uri.AbsolutePath (which always begins with a /), any relativePath passed without a leading slash will fail to match and won't be evicted.

Consider adding early returns and formatting safeguards to make this method foolproof.

♻️ Proposed refactor
     public async Task PurgeCache(params string[] relativePaths)
     {
+        if (relativePaths is null || relativePaths.Length == 0)
+            return;
+
         foreach (var relativePath in relativePaths)
         {
-            await outputCacheStore.EvictByTagAsync(relativePath.ToLowerInvariant(), default);
+            if (string.IsNullOrWhiteSpace(relativePath))
+                continue;
+
+            var tag = relativePath.StartsWith('/') ? relativePath.ToLowerInvariant() : $"/{relativePath}".ToLowerInvariant();
+            await outputCacheStore.EvictByTagAsync(tag, default);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Infrastructure/Services/ResponseCacheService.cs`
around lines 22 - 27, Update ResponseCacheService.PurgeCache to return
immediately when relativePaths is null or empty, avoiding an empty cache
eviction request. Before calling outputCacheStore.EvictByTagAsync, normalize
each path to match Uri.AbsolutePath by ensuring it has a leading slash, then
apply the existing lowercase normalization.
src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Shared/Infrastructure/Services/AppResponseCachePolicy.cs (1)

35-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the indexer instead of .Add to assign dictionary values.

Using .Add on the VaryByValues dictionary could throw an ArgumentException if the key already exists (e.g., if it was pre-populated by a controller attribute or if the policy inadvertently runs twice). Using the indexer is a slightly safer and more robust approach.

♻️ Proposed refactor
         if (CultureInfoManager.InvariantGlobalization is false)
         {
-            context.CacheVaryByRules.VaryByValues.Add("Culture", CultureInfo.CurrentUICulture.Name);
+            context.CacheVaryByRules.VaryByValues["Culture"] = CultureInfo.CurrentUICulture.Name;
         }

         //#if (multitenant == true)
         // An authenticated request resolves its tenant from the user's claim rather than from the host (See TenantProvider),
         // and tenant scoped entities are filtered by that tenant (See AppDbContext.ConfigureTenantAwareEntity). Without this
         // rule two users of different tenants on the same host would share a single entry, so a UserAgnostic endpoint like
         // ProductViewController would serve one tenant's rows to another.
         if (context.HttpContext.User.GetTenantId() is Guid currentTenantId)
         {
-            context.CacheVaryByRules.VaryByValues.Add("Tenant", currentTenantId.ToString());
+            context.CacheVaryByRules.VaryByValues["Tenant"] = currentTenantId.ToString();
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Shared/Infrastructure/Services/AppResponseCachePolicy.cs`
around lines 35 - 48, Replace the VaryByValues.Add calls in the response cache
policy with dictionary indexer assignments for the "Culture" and "Tenant" keys.
Update the existing assignments within the
CultureInfoManager.InvariantGlobalization and currentTenantId branches while
preserving their current values and conditions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Infrastructure/Services/ResponseCacheService.cs`:
- Around line 22-27: Update ResponseCacheService.PurgeCache to return
immediately when relativePaths is null or empty, avoiding an empty cache
eviction request. Before calling outputCacheStore.EvictByTagAsync, normalize
each path to match Uri.AbsolutePath by ensuring it has a leading slash, then
apply the existing lowercase normalization.

In
`@src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Shared/Infrastructure/Services/AppResponseCachePolicy.cs`:
- Around line 35-48: Replace the VaryByValues.Add calls in the response cache
policy with dictionary indexer assignments for the "Culture" and "Tenant" keys.
Update the existing assignments within the
CultureInfoManager.InvariantGlobalization and currentTenantId branches while
preserving their current values and conditions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 222e6c59-1c0c-4696-9c7f-0c6d55d88560

📥 Commits

Reviewing files that changed from the base of the PR and between da37d6f and 3bac8a8.

📒 Files selected for processing (7)
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Features/Identity/IdentityController.ExternalSignIn.cs
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Features/Identity/IdentityController.WebAuthn.cs
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Infrastructure/Services/ResponseCacheService.cs
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Shared/Infrastructure/Extensions/WebApplicationBuilderExtensions.cs
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Shared/Infrastructure/Services/AppResponseCachePolicy.cs
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Infrastructure/Extensions/UriExtensions.cs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Features/Identity/IdentityController.ExternalSignIn.cs
  • src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Features/Identity/IdentityController.WebAuthn.cs

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(templates): correct response caching of origin-dependent and faulted responses

1 participant