cloudfront: normalize Accept into a two-value cache key at the edge - #97
Open
devin-ai-integration[bot] wants to merge 5 commits into
Open
cloudfront: normalize Accept into a two-value cache key at the edge#97devin-ai-integration[bot] wants to merge 5 commits into
devin-ai-integration[bot] wants to merge 5 commits into
Conversation
Co-Authored-By: brandon <brandon@flightcontrol.dev>
flybayer
self-requested a review
August 8, 2026 17:50
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Ravion Module Publish PlanDry run only. No Ravion API mutations were made.
Diffsrvn-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>
Co-Authored-By: brandon <brandon@flightcontrol.dev>
flybayer
reviewed
Aug 8, 2026
Co-Authored-By: brandon <brandon@flightcontrol.dev>
Co-Authored-By: brandon <brandon@flightcontrol.dev>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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'sVary: Accept, so a crawler'sGET /pricingand a browser's collide on one entry. The AWS-managed policies can't be edited, and the existingcustomoption only accepts the ID of a policy created outside IaC — so the module has to create the policy itself.Keying on raw
Acceptwould 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×.The origin keeps negotiating on
Acceptitself, so there's no new origin contract —x-mdexists only for the cache key. That does mean the chosen origin request policy has to forwardAccept(the defaultAllViewerdoes), which is called out in the docs. The function always overwrites a client-suppliedx-mdso a viewer can't inject a value and poison either entry. Preference matching mirrors the origin: Markdown wins only when its effective quality is> 0and> 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:
aws_cloudfront_function.redirect→.viewer_requesttherefore comes with amovedblock, 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_enableddefaults tofalse, so direct module callers are unchanged; in the definition it is the new recommendedcache_policychoice and its default, so new stacks get it. Existing stacks keep their stored value. Setting it together with an explicitdefault_cache_behavior.cache_policy_idis a validation error rather than a silent win for one of them.release.version1.0.1 → 1.1.0 (form-default changes have precedent as non-major here, e.g.c98552dflipping allowed methods to read-only).Link to Devin session: https://app.devin.ai/sessions/bba2fb3d755c445b86e8fad62cf428b8
Requested by: @flybayer