Skip to content

cloudfront: normalize Accept into a two-value cache key at the edge - #97

Open
devin-ai-integration[bot] wants to merge 5 commits into
mainfrom
devin/1786211417-cloudfront-accept-cache-key
Open

cloudfront: normalize Accept into a two-value cache key at the edge#97
devin-ai-integration[bot] wants to merge 5 commits into
mainfrom
devin/1786211417-cloudfront-accept-cache-key

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

ravion.com now negotiates its representation at the origin (Accept: text/markdown → Markdown, browser Accept → HTML). CloudFront breaks that: its cache key contains only what the cache policy lists, and it does not split on the origin's Vary: Accept, so a crawler's GET /pricing and a browser's collide on one entry. The AWS-managed policies can't be edited, and the existing custom option only accepts the ID of a policy created outside IaC — so the module has to create the policy itself.

Keying on raw Accept would shard the HTML cache across every real-world Accept string, so the viewer-request function normalizes instead: it computes one bit and the cache policy keys on that, splitting the cache exactly 2×.

viewer-request:  Accept: text/html,…,*/*;q=0.8   →  x-md: 0
                 Accept: text/markdown           →  x-md: 1
cache policy:    key = URI + query + [x-md]      (mirrors UseOriginCacheControlHeaders-QueryStrings otherwise)

The origin keeps negotiating on Accept itself, so there's no new origin contract — x-md exists only for the cache key. That does mean the chosen origin request policy has to forward Accept (the default AllViewer does), which is called out in the docs. The function always overwrites a client-supplied x-md so a viewer can't inject a value and poison either entry. Preference matching mirrors the origin: Markdown wins only when its effective quality is > 0 and > q(text/html), where the effective quality is the highest at the most specific matching level (text/markdown > text/* > */*), so */* and equal-q both resolve to HTML.

CloudFront permits one viewer-request function per behavior, and this module already attaches one to the default and every ordered behavior for redirect rules — so redirects and normalization are composed into a single generated function rather than two:

viewer_request_function_enabled = local.redirects_enabled || var.accept_header_cache_key_creation_enabled
viewer_request_function_code    = templatefile("functions/viewer_request.js", {
  accept_header_normalization_code = enabled ? file("functions/accept_header_normalization.js") : ""
  redirect_rules_json              = jsonencode(var.redirect_rules)
})

aws_cloudfront_function.redirect.viewer_request therefore comes with a moved block, and the generated physical function name is deliberately left unchanged so distributions that already use redirects aren't replaced.

The Terraform input accept_header_cache_key_creation_enabled defaults to false, so direct module callers are unchanged; in the definition it is the new recommended cache_policy choice and its default, so new stacks get it. Existing stacks keep their stored value. Setting it together with an explicit default_cache_behavior.cache_policy_id is a validation error rather than a silent win for one of them. release.version 1.0.1 → 1.1.0 (form-default changes have precedent as non-major here, e.g. c98552d flipping allowed methods to read-only).

Link to Devin session: https://app.devin.ai/sessions/bba2fb3d755c445b86e8fad62cf428b8
Requested by: @flybayer

Co-Authored-By: brandon <brandon@flightcontrol.dev>
@flybayer flybayer self-assigned this Aug 8, 2026
@flybayer
flybayer self-requested a review August 8, 2026 17:50
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Ravion Module Publish Plan

Dry run only. No Ravion API mutations were made.

Module Current Version New Version Description
rvn-cloudfront 1.0.1 1.1.0 Make normalized Markdown negotiation the recommended CloudFront cache policy.

Diffs

rvn-cloudfront 1.0.1 -> 1.1.0

--- remote
+++ compiled
       - description: GET, HEAD
         label: Read only
         value: read
-  - default: 4cc15a8a-d715-48a4-82b8-cc0b614638fe
-    description: Controls edge caching for the default behavior. The default respects your app's Cache-Control headers, so nothing is cached unless the app asks for it.
+  - default: managed_accept
+    description: Controls edge caching for the default behavior. The recommended option respects your app's Cache-Control headers and keeps HTML and Markdown in separate cache entries.
     id: cache_policy
     label: Cache policy
     required: true
     type: string
     values:
+      - description: Caches only responses with Cache-Control headers from your app. Query strings and a normalized Markdown preference are part of the cache key.
+        label: UseOriginCacheControlHeaders-QueryStrings with normalized Markdown negotiation (recommended)
+        value: managed_accept
       - description: Caches only responses with Cache-Control headers from your app. Query strings are part of the cache key.
-        label: UseOriginCacheControlHeaders-QueryStrings (recommended)
+        label: UseOriginCacheControlHeaders-QueryStrings
         value: 4cc15a8a-d715-48a4-82b8-cc0b614638fe
       - description: Caches only responses with Cache-Control headers from your app. Query strings are not part of the cache key.
         label: UseOriginCacheControlHeaders
@@
 
   The default configuration is safe for dynamic apps: responses are cached at the edge only when your app returns Cache-Control headers, and the full viewer request, including the Host header, cookies, and query strings, is forwarded to the origin. You get TLS termination close to users, HTTP/2 and HTTP/3, connection reuse to the origin, optional WAF, and per-path caching for static assets.
 
-  Terraform source: [ravionhq/modules/cdn/cloudfront](https://github.com/ravionhq/modules/tree/rvn-cloudfront@1.0.1/cdn/cloudfront)
+  Terraform source: [ravionhq/modules/cdn/cloudfront](https://github.com/ravionhq/modules/tree/rvn-cloudfront@1.1.0/cdn/cloudfront)
 
   ## Use cases
 
@@
 
   Redirect rules run at viewer request time before CloudFront checks its cache or contacts an origin. Rules are evaluated in order and the first match wins. Sources and destinations accept absolute HTTPS URLs or host-agnostic paths. Use `:name` to capture one path segment and a final `:name*` to capture the remaining path, then place those named values anywhere in the destination. Query preservation is optional and disabled by default.
 
-  The managed redirect function is attached to the default cache behavior and every custom cache behavior so redirects cover every request path. CloudFront allows only one viewer-request edge association per behavior, so redirect rules cannot be combined with a viewer-request CloudFront Function or Lambda@Edge association supplied through Advanced Terraform variables.
+  The managed viewer-request function is attached to the default cache behavior and every custom cache behavior. It handles redirects when configured and can normalize Markdown negotiation into a two-value cache key when the managed cache policy is selected. CloudFront allows only one viewer-request edge association per behavior, so these features cannot be combined with a viewer-request CloudFront Function or Lambda@Edge association supplied through Advanced Terraform variables.
 
   The module prevents a rule from redirecting back into its own source pattern. It cannot detect cycles spanning multiple independently matching rules, so review rule ordering and destinations when defining bidirectional or multi-domain redirects.
 
@@
 
   | Setting | Default | Why |
   | --- | --- | --- |
-  | Cache policy | UseOriginCacheControlHeaders-QueryStrings | Caches only responses where the app sends Cache-Control headers |
+  | Cache policy | UseOriginCacheControlHeaders-QueryStrings with normalized Markdown negotiation | Caches only responses where the app sends Cache-Control headers and keeps HTML and Markdown in separate cache entries |
   | Origin request policy | AllViewer | Forwards Host, cookies, and query strings so app sessions and ALB routing work |
   | Require signed URLs | false | Allows public viewer access unless enabled with trusted key groups |
   | Allowed methods | Read only | Only GET and HEAD reach the origin; choose Read and OPTIONS or All methods to accept form posts and API writes |
   | Viewer protocol policy | Redirect to HTTPS | Viewers are upgraded to HTTPS automatically |
 
-  With the default cache policy, nothing is cached until the app opts in by returning Cache-Control headers such as public, max-age=300. Responses without caching headers always go to the origin. Choose CachingDisabled to turn off edge caching entirely, or CachingOptimized to cache aggressively regardless of origin headers. Pick Custom policy ID to use a cache or origin request policy you created in your own account.
+  With the recommended cache policy, nothing is cached until the app opts in by returning Cache-Control headers such as public, max-age=300. Responses without caching headers always go to the origin. The normalized Markdown negotiation option creates a module-managed policy and viewer-request function that keep HTML and Markdown in two separate cache entries. The selected origin request policy must forward the Accept header, or the origin cannot negotiate Markdown and may return HTML for every request. Choose the plain UseOriginCacheControlHeaders-QueryStrings policy when the origin does not serve Markdown, CachingDisabled to turn off edge caching entirely, or CachingOptimized to cache aggressively regardless of origin headers. Pick Custom policy ID to use a cache or origin request policy you created in your own account.
 
   Response headers policy is optional and adds headers such as the SecurityHeadersPolicy set or CORS headers to every response.
 
@@
   | Redirect rules | No | [] | Ordered URL-pattern redirects returned before contacting an origin |
   | Viewer protocol policy | Yes | Redirect to HTTPS | How viewers connect to CloudFront |
   | Allowed methods | Yes | Read only | GET and HEAD only; switch to Read and OPTIONS or All methods for apps that write |
-  | Cache policy | Yes | UseOriginCacheControlHeaders-QueryStrings | AWS managed policies by name, or a custom policy ID |
+  | Cache policy | Yes | UseOriginCacheControlHeaders-QueryStrings with normalized Markdown negotiation | AWS managed policies by name, a module-managed normalized Markdown policy, or a custom policy ID |
   | Origin request policy | Yes | AllViewer | AWS managed policies by name, or a custom policy ID |
   | Require signed URLs | No | false | Requires signed URLs or signed cookies on the default behavior |
   | Trusted key group IDs | When signed URLs are enabled | [] | Existing CloudFront key groups trusted for signatures |
@@
         base_path: cdn/cloudfront
         branch: main
         execution_environment_id: << module.input.execution_environment_id >>
-        ref: rvn-cloudfront@1.0.1
+        ref: rvn-cloudfront@1.1.0
         repo: https://github.com/ravionhq/modules
         stack_id: <<stack.id>>
         terraform_variables:
           ...overrides: << module.input.advanced_terraform_variables >>
+          accept_header_cache_key_creation_enabled: << module.input.cache_policy == "managed_accept" >>
           additional_metrics_enabled: << module.input.additional_metrics_enabled >>
           default_cache_behavior:
             allowed_methods: '<< module.input.allowed_methods == "all" ? ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] : module.input.allowed_methods == "read_options" ? ["GET", "HEAD", "OPTIONS"] : ["GET", "HEAD"] >>'
-            cache_policy_id: '<< module.input.cache_policy == "custom" ? module.input.cache_policy_custom_id : module.input.cache_policy >>'
+            cache_policy_id: '<< module.input.cache_policy == "managed_accept" ? nil : module.input.cache_policy == "custom" ? module.input.cache_policy_custom_id : module.input.cache_policy >>'
             origin_request_policy_id: '<< module.input.origin_request_policy == "custom" ? module.input.origin_request_policy_custom_id : module.input.origin_request_policy == "none" ? nil : module.input.origin_request_policy >>'
             response_headers_policy_id: '<< module.input.response_headers_policy == "custom" ? module.input.response_headers_policy_custom_id : (module.input.response_headers_policy || nil) >>'
             target_origin_id: primary

Co-Authored-By: brandon <brandon@flightcontrol.dev>
@devin-ai-integration devin-ai-integration Bot changed the title cloudfront: optional module-managed cache policy that keys on Accept cloudfront: normalize Accept into a two-value cache key at the edge Aug 8, 2026
Co-Authored-By: brandon <brandon@flightcontrol.dev>
@flybayer
flybayer requested review from mabadir and removed request for flybayer August 8, 2026 18:26
Comment thread cdn/cloudfront/rvn-cloudfront-definition.yml Outdated
devin-ai-integration Bot and others added 2 commits August 8, 2026 18:28
Co-Authored-By: brandon <brandon@flightcontrol.dev>
Co-Authored-By: brandon <brandon@flightcontrol.dev>
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.

1 participant