Skip to content

fix(Router): add x-forwarded-host to cache key for domain-based routing#6384

Open
ax-at wants to merge 1 commit intoanomalyco:devfrom
ax-at:bugfix/router-redirection-issue-fix
Open

fix(Router): add x-forwarded-host to cache key for domain-based routing#6384
ax-at wants to merge 1 commit intoanomalyco:devfrom
ax-at:bugfix/router-redirection-issue-fix

Conversation

@ax-at
Copy link

@ax-at ax-at commented Feb 4, 2026

Summary

This PR fixes a critical caching bug in sst.aws.Router where CloudFront serves cached content from one subdomain to requests for another subdomain. The fix adds x-forwarded-host to the CloudFront cache key to ensure each subdomain has separate cache entries.

Fixes: #6383

Technical Explanation

The Problem

The Router's cache policy (lazy routes mode) only includes x-open-next-cache-key in the cache key:

File: platform/src/components/aws/router.ts (lines 1452-1457)

headersConfig: {
  headerBehavior: "whitelist",
  headers: {
    items: ["x-open-next-cache-key"],  // ❌ Missing domain header
  },
},

This causes CloudFront to cache responses without considering the domain:

Cache Key: {uri="/", query="", x-open-next-cache-key="..."}
❌ Missing: domain/host information

When a user visits light.development.example.com after visiting dark.development.example.com, CloudFront finds a cache hit and returns the cached dark app content without executing the CloudFront Function.

The Solution

File: platform/src/components/aws/router.ts

Change:

Add x-forwarded-host to the cache key:

headersConfig: {
  headerBehavior: "whitelist",
  headers: {
    items: ["x-open-next-cache-key", "x-forwarded-host"],  // Add x-forwarded-host
  },
},

This creates domain-specific cache entries:

Cache Key for dark.development.example.com: 
  {uri="/", query="", x-open-next-cache-key="...", x-forwarded-host="dark.development.example.com"}

Cache Key for light.development.example.com:
  {uri="/", query="", x-open-next-cache-key="...", x-forwarded-host="light.development.example.com"}

Now each subdomain has separate cache entries, and CloudFront serves the correct content.

Why x-forwarded-host Instead of host?

Critical Design Decision: We use x-forwarded-host instead of the standard host header to avoid breaking Lambda URL OAC (Origin Access Control) authentication.

How CloudFront Cache Policy Works

When a header is added to the cache policy whitelist, CloudFront does TWO things:

  1. Includes it in the cache key (desired behavior)
  2. Forwards it to the origin (can cause issues)

The Lambda URL OAC Problem

Lambda URLs with OAC use AWS SigV4 authentication, which requires:

Host: <lambda-url-domain>.lambda-url.<region>.on.aws

If we whitelist the host header in the cache policy:

  1. Browser sends: Host: dark.development.example.com
  2. CloudFront includes host in cache key ✅
  3. CloudFront forwards Host: dark.development.example.com to origin ❌
  4. Lambda URL OAC expects: Host: <lambda-url>.lambda-url.ap-south-1.on.aws
  5. Signature validation fails → 403 AccessDeniedException

Why x-forwarded-host Works

The CloudFront Function already sets x-forwarded-host (see platform/src/components/aws/router.ts, lines 2085-2086):

function setUrlOrigin(urlHost, override) {
  event.request.headers["x-forwarded-host"] = event.request.headers.host;
  // ... sets origin dynamically
}

By using x-forwarded-host:

  1. CloudFront Function sets: x-forwarded-host: dark.development.example.com
  2. Cache key includes: x-forwarded-host
  3. Origin receives: Host: <lambda-url>... (unchanged) ✅
  4. Origin also receives: x-forwarded-host: dark.development.example.com (preserved)
  5. Lambda URL OAC validates correctly ✅
  6. Next.js can access original domain via x-forwarded-host

Request Flow Diagram

sequenceDiagram
    participant Browser
    participant CloudFront
    participant CFFunction as CloudFront_Function
    participant LambdaURL as Lambda_URL_with_OAC

    Browser->>CloudFront: GET dark.development.example.com/
    Note over CloudFront: Cache key includes x-forwarded-host
    CloudFront->>CFFunction: viewer-request event<br/>Host: dark.development.example.com
    Note over CFFunction: Sets x-forwarded-host = dark.development.example.com
    CFFunction->>CFFunction: Match route in KV Store
    CFFunction->>CloudFront: Update origin to Lambda URL
    CloudFront->>LambdaURL: Forward request<br/>Host: xxx.lambda-url.ap-south-1.on.aws<br/>x-forwarded-host: dark.development.example.com
    Note over LambdaURL: OAC validates with Lambda URL host ✅
    Note over LambdaURL: App knows original domain from x-forwarded-host
    LambdaURL-->>CloudFront: Response
    Note over CloudFront: Cache with x-forwarded-host in key
    CloudFront-->>Browser: Response
Loading

Testing Performed

Test Setup

  1. Using the reproduction example's code, created two distinct Next.js apps (dark-app, light-app) with visibly different content
  2. Configured Router with wildcard domain aliases

Test Scenarios

Without Fix

  1. Navigate to dark.development.example.com → Dark app loads ✅
  2. Navigate to light.development.example.com → Dark app loads ❌ (cached)
  3. Clear cache, navigate to light.development.example.com → Light app loads ✅
  4. Navigate to dark.development.example.com → Light app loads ❌ (cached)

With Fix

  1. Navigate to dark.development.example.com → Dark app loads ✅
  2. Navigate to light.development.example.com → Light app loads ✅
  3. Navigate back to dark.development.example.com → Dark app loads ✅
  4. Tested PR preview domains: app-1-pr-123.development.example.com
  5. Verified Lambda URL OAC still works ✅
  6. Tested cache invalidation and redeployment ✅

curl Validation

# Test dark subdomain
curl -I https://dark.development.example.com/ 
# Returns: X-Cache: Miss from cloudfront (first request)
# Returns: X-Cache: Hit from cloudfront (subsequent requests)
# Content: Dark app ✅

# Test light subdomain
curl -I https://light.development.example.com/
# Returns: X-Cache: Miss from cloudfront (first request for this domain)
# Content: Light app ✅ (not cached dark app)

Breaking Changes

None. This change is additive and only affects how CloudFront caches responses for domain-based routing. Existing deployments continue to work (albeit with the caching bug).

Copy link
Collaborator

@vimtor vimtor left a comment

Choose a reason for hiding this comment

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

thanks for your contribution @ax-at

(loved the detailed description)

proof of it working
Before After
Image Image

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.

2 participants