You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The JS Agent team now lets the origin own the agent's cache lifetimes. This proxy was overwriting them. It clamped max-age to at most 3600 and injected s-maxage=60 whenever the origin sent none, so CloudFront discarded the agent every minute, and any origin lifetime above an hour reached browsers truncated. It also forwarded the upstream Cache-Tag, which carries a hash derived from the customer's API key.
Verified against Fingerprint staging (procdn.fpjs.sh) through four endpoints in a single run, each with its own API key so every endpoint's first request reaches the origin:
This PR
main
Bare path
/fpjs/web/v4/<apiKey>
/fpjs/web/v4/<apiKey>
.js path
/fpjs/web/v4/<apiKey>/iife.min.js
/fpjs/web/v4/<apiKey>/iife.min.js
Neither build treats the two path shapes differently: same statuses, same cache behavior, same header handling. Only the origin's own max-age differs, because each endpoint uses its own API key. The table reports one row per step, bare path first, .js path second.
Test results
Step
Expected
main
This PR
1. First request
200, no s-maxage, no age, cache-tag
❌ 200 MISS, s-maxage=60, cache-tag forwarded
⚠️ 200 MISS, max-age=3669 / 3761, no s-maxage, no age, no cache-tag*
⚠️ 200 HIT, no s-maxage, no cache-tag, age: 10 / 7**
3. After the edge entry expired
200, no s-maxage, age: 0, no cache-tag
❌ 200 MISS, s-maxage=60
✅ 200 MISS, re-fetched, max-age=3669 / 3644, no cache-tag
4a. If-None-Match, answered from cache
304, no s-maxage, age: 0, no cache-tag
❌ 304 HIT, s-maxage=60, age: 1
⚠️ 304 HIT, no s-maxage, age: 3 / 2**
4b. If-None-Match, revalidated at the origin***
304
✅ 304 MISS, but still s-maxage=60
✅ 304 MISS, age: 0
* main forwarded the upstream purge tag on steps 1 and 2, keyed to the customer: cache-tag: procdn,procdn-apiKey-62dd1936… on one endpoint, …7dba44ea… on the other. It stops appearing from step 3 because procdn's own Cloudflare cache was warm by then and no longer emitted it. This PR returns no cache-tag on any step.
** Age is the one expectation this change does not meet, on either build. This function cannot fix it, see the follow-up section. The values differ across endpoints because of the run order. Each step hits the four endpoints in sequence, so the endpoint requested first has the oldest entry by the time the next step comes round. At step 4b this PR reports age: 0 where main sends no Age at all; RFC 9111 treats an absent Age as zero, so the two are equivalent.
*** Not in the ticket's plan. The ticket's step 4 is answered by the CloudFront cache, so the function never sees an upstream 304. 4b lets the edge entry expire first, which pushes the conditional request through to the origin. Both builds handle it.
The max-age cap was truncating real values
A second run isolates the cap. All four endpoints shared one API key, so the origin value was identical everywhere. main returned exactly max-age=3600 on all ten of its responses while the origin sent between 3613 and 3715 in the same minute. That cut up to 115 seconds off every agent response. This PR returns what the origin sent.
The fresh-key run above only hints at it. main's bare path got max-age=3478, under the cap and so untouched, while its .js path landed on exactly 3600 against sibling keys serving 3644 to 3761. Suggestive, not conclusive, since 3600 is also a plausible origin value.
s-maxage and the edge TTL
main injected s-maxage=60 into all ten of its responses, which is what CloudFront uses for its edge TTL. On this PR the TTL comes from the cache policy's 180s instead. Step 3 cannot separate the two, since a 190s pause expires both and both re-fetched once. The difference is in how often that happens under real traffic: every 60s on main, every 180s here.
What changed
Area
Before
After
Cache-Control to clients
max-age clamped to at most 3600, truncating the 3613 to 3715 the origin sent during the shared-key run. s-maxage set to 60 when the origin sent none
Origin value as it is
Edge TTL
60s, from the injected s-maxage
180s, from the CloudFront cache policy
cache-tag
Forwarded, with an API-key-derived hash
Stripped on every response
age
CloudFront's own value
CloudFront's own value, unchanged
If-None-Match
304 from cache and 304 revalidated at the origin, both correct
Unchanged
Code:
Deleted proxy/utils/cache-control.ts. The origin Cache-Control passes through byte for byte.
Dropped the overrideCacheControl parameter from updateResponseHeaders and the isJavascript flag that fed it. The agent response needs no special case.
Added cache-tag to BLACKLISTED_HEADERS.
Why
The origin decides cache lifetimes. The proxy no longer replaces them with static values.
cache-tag is the upstream CDN's purge handle. A browser has no use for it.
An upstream 304 has no body. Step 4b checks the function returns it rather than failing, since nothing exercised that path before.
Why cache-tag is stripped from the first response too
The ticket expects cache-tag on a miss and nothing on a hit. A CloudFront proxy cannot do that, and main shows why. Step 2 is a CloudFront HIT and still carries the tag. The Lambda@Edge function runs on origin-request, so it only runs on a cache miss. It fetches the agent itself and returns the response, CloudFront stores that, and every later hit is replayed from the store without the function running. The header is either always present or never present. Stripping it on every response keeps the API-key hash out of client responses.
Follow-up: the edge TTL stays at 180s
The cache policy in fingerprintjs/fingerprint-cloudfront-proxy-integration (v2.0.0) sets default_ttl = max_ttl = 180, and CloudFront clamps origin TTLs to it. Removing the s-maxage=60 injection therefore raised the real edge TTL from 60s to 180s, not to the origin's ~1h. Keeping it there, for now.
A browser loses whatever the CloudFront entry's age was when it fetched, once, off its own window. At max_ttl = 180 that is at most 180s of ~3500s, averaging ~90s, so under 3%. At max_ttl = 3600 the average loss becomes roughly half the window and the unlucky half of users revalidate on nearly every page load.
Pinning Age: 0 does not buy the headroom to raise it. An aws_cloudfront_response_headers_policy can set it, but it changes nothing a browser computes. CloudFront replays the stored Date, frozen at the moment it stored the entry, so RFC 9111'smax(apparent_age, corrected_age_value) resolves to the Date-derived value regardless of Age. Measured with the policy: a hit at 10:48:42 serving date: 10:46:27 and age: 0, so a browser reads 135s, not 0.
Fixing Date needs code, since the correct value is "now" and a policy only writes constants. That means a viewer-response function, which the edge function restrictions permit alongside our origin-request one. Worth doing only if someone wants max_ttl raised.
Also ruled out: passing the origin's Age: 0 through. Removing age from BLACKLISTED_HEADERS was deployed and tested, and CloudFront overwrites it with its own count. age stays blacklisted.
🚀 Following releases will be created using changesets from this PR:
@fingerprint/aws-cloudfront-proxy@2.2.1-rc.0
Patch Changes
Upgrade AWS SDK Clients to latest version (^3.1144.0). (51192a4)
Pass the origin's Cache-Control for the agent through as it is. The proxy no longer caps max-age at 3600 or injects s-maxage=60, so the browser cache lifetime follows the origin. CloudFront derives its edge TTL from the origin directives subject to the cache policy's limits, and uses the policy default when the origin sends no cache lifetime. Strip the upstream Cache-Tag header, which carries the upstream CDN's purge tag and an API-key-derived hash. (6dc6eef)
This URI matches the configured V3 agent route (fpjs_agent_download_path = greiodsfkljlds), which is checked before the V4 catch-all in app.ts:26-51. As a result, this test no longer exercises the V4 agent path and duplicates the V3 test instead; use the suite's requestUri so the cache pass-through remains covered through the V4 handler.
This issue also appears on line 140 of the same file.
This branch has not been deployed
No deployments
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
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.
Honor origin agent cache headers
The JS Agent team now lets the origin own the agent's cache lifetimes. This proxy was overwriting them. It clamped
max-ageto at most 3600 and injecteds-maxage=60whenever the origin sent none, so CloudFront discarded the agent every minute, and any origin lifetime above an hour reached browsers truncated. It also forwarded the upstreamCache-Tag, which carries a hash derived from the customer's API key.Verified against Fingerprint staging (
procdn.fpjs.sh) through four endpoints in a single run, each with its own API key so every endpoint's first request reaches the origin:main/fpjs/web/v4/<apiKey>/fpjs/web/v4/<apiKey>.jspath/fpjs/web/v4/<apiKey>/iife.min.js/fpjs/web/v4/<apiKey>/iife.min.jsNeither build treats the two path shapes differently: same statuses, same cache behavior, same header handling. Only the origin's own
max-agediffers, because each endpoint uses its own API key. The table reports one row per step, bare path first,.jspath second.Test results
mains-maxage, noage,cache-tags-maxage=60,cache-tagforwardedmax-age=3669/3761, nos-maxage, noage, nocache-tag*s-maxage,age: 0, nocache-tags-maxage=60,age: 3/2,cache-tagstill forwardeds-maxage, nocache-tag,age: 10/7**s-maxage,age: 0, nocache-tags-maxage=60max-age=3669/3644, nocache-tagIf-None-Match, answered from caches-maxage,age: 0, nocache-tags-maxage=60,age: 1s-maxage,age: 3/2**If-None-Match, revalidated at the origin***s-maxage=60age: 0mainforwarded the upstream purge tag on steps 1 and 2, keyed to the customer:cache-tag: procdn,procdn-apiKey-62dd1936…on one endpoint,…7dba44ea…on the other. It stops appearing from step 3 because procdn's own Cloudflare cache was warm by then and no longer emitted it. This PR returns nocache-tagon any step.Ageis the one expectation this change does not meet, on either build. This function cannot fix it, see the follow-up section. The values differ across endpoints because of the run order. Each step hits the four endpoints in sequence, so the endpoint requested first has the oldest entry by the time the next step comes round. At step 4b this PR reportsage: 0wheremainsends noAgeat all; RFC 9111 treats an absentAgeas zero, so the two are equivalent.The
max-agecap was truncating real valuesA second run isolates the cap. All four endpoints shared one API key, so the origin value was identical everywhere.
mainreturned exactlymax-age=3600on all ten of its responses while the origin sent between 3613 and 3715 in the same minute. That cut up to 115 seconds off every agent response. This PR returns what the origin sent.The fresh-key run above only hints at it.
main's bare path gotmax-age=3478, under the cap and so untouched, while its.jspath landed on exactly3600against sibling keys serving 3644 to 3761. Suggestive, not conclusive, since 3600 is also a plausible origin value.s-maxageand the edge TTLmaininjecteds-maxage=60into all ten of its responses, which is what CloudFront uses for its edge TTL. On this PR the TTL comes from the cache policy's 180s instead. Step 3 cannot separate the two, since a 190s pause expires both and both re-fetched once. The difference is in how often that happens under real traffic: every 60s onmain, every 180s here.What changed
Cache-Controlto clientsmax-ageclamped to at most 3600, truncating the 3613 to 3715 the origin sent during the shared-key run.s-maxageset to 60 when the origin sent nones-maxagecache-tagageIf-None-MatchCode:
proxy/utils/cache-control.ts. The originCache-Controlpasses through byte for byte.overrideCacheControlparameter fromupdateResponseHeadersand theisJavascriptflag that fed it. The agent response needs no special case.cache-tagtoBLACKLISTED_HEADERS.Why
cache-tagis the upstream CDN's purge handle. A browser has no use for it.Why
cache-tagis stripped from the first response tooThe ticket expects
cache-tagon a miss and nothing on a hit. A CloudFront proxy cannot do that, andmainshows why. Step 2 is a CloudFront HIT and still carries the tag. The Lambda@Edge function runs onorigin-request, so it only runs on a cache miss. It fetches the agent itself and returns the response, CloudFront stores that, and every later hit is replayed from the store without the function running. The header is either always present or never present. Stripping it on every response keeps the API-key hash out of client responses.Follow-up: the edge TTL stays at 180s
The cache policy in
fingerprintjs/fingerprint-cloudfront-proxy-integration(v2.0.0) setsdefault_ttl = max_ttl = 180, and CloudFront clamps origin TTLs to it. Removing thes-maxage=60injection therefore raised the real edge TTL from 60s to 180s, not to the origin's ~1h. Keeping it there, for now.A browser loses whatever the CloudFront entry's age was when it fetched, once, off its own window. At
max_ttl = 180that is at most 180s of ~3500s, averaging ~90s, so under 3%. Atmax_ttl = 3600the average loss becomes roughly half the window and the unlucky half of users revalidate on nearly every page load.Pinning
Age: 0does not buy the headroom to raise it. Anaws_cloudfront_response_headers_policycan set it, but it changes nothing a browser computes. CloudFront replays the storedDate, frozen at the moment it stored the entry, so RFC 9111'smax(apparent_age, corrected_age_value)resolves to theDate-derived value regardless ofAge. Measured with the policy: a hit at 10:48:42 servingdate: 10:46:27andage: 0, so a browser reads 135s, not 0.Fixing
Dateneeds code, since the correct value is "now" and a policy only writes constants. That means aviewer-responsefunction, which the edge function restrictions permit alongside ourorigin-requestone. Worth doing only if someone wantsmax_ttlraised.Also ruled out: passing the origin's
Age: 0through. RemovingagefromBLACKLISTED_HEADERSwas deployed and tested, and CloudFront overwrites it with its own count.agestays blacklisted.