fix(consent): stop passing a malformed storefrontRootDomain#3750
Conversation
useCustomerPrivacy was prefixing storefrontRootDomain with a leading dot (`.example.com`) — a legacy cookie-domain idiom from Shopify#3309. Since Shopify#3649 set `backendConsentEnabled = true`, consent-tracking-api also uses storefrontRootDomain as a fetch hostname, building URLs like `https://.example.com/api/unstable/graphql.json`. The browser blocks those on CSP `connect-src` and `Promise.race` returns the rejection before the same-origin fetch can resolve, surfacing as a `Failed to fetch` error in setTrackingConsent callbacks (e.g. CookieBot). Drop the leading dot so the value matches the public Customer Privacy API contract (https://shopify.dev/docs/api/consent-tracking), and omit it entirely when the SFAPI proxy is enabled so consent-tracking-api falls back to `window.location.host` and routes both fetches through the proxy. Adds regression tests covering both the proxy-enabled and proxy-disabled payload shapes. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Notes: Thanks to Cursor for analyzing and fixing this 😄 I had this issue in my Hydrogen store where my I let Cursor analyze this and this seems like an appropriate fix. It also analyzed the consent tracking script (no source code unfortunately?) and confirmed the findings. |
|
/snapit |
|
Only users with write permission to the repository can run /snapit |
|
Okay, I've done some tests with this in my actual Hydrogen storefront hosted via Oxygen: This PR fixes the issue with the leading dot (which causes the GraphQL query to fail). I'm now running into a separate issue though which is not fixable so easily: As I said, I have a store that's currently running on something like With this fix, Hydrogen will now call Fun fact: If I run the request against the checkout domain ( |
|
Update: This PR is not strictly needed for my case anymore (as there's another request going to the correct URL) but I still think there's something going wrong here (as a request is going to Might be a good idea to have this checked by an experienced Shopify dev 🙂 |
WHY are these changes introduced?
useCustomerPrivacyhas been emitting consent-sync requests to malformed URLs likehttps://.example.com/api/unstable/graphql.jsonever since #3649 (@shopify/hydrogen@2026.4.0). On affected storefronts, everysetTrackingConsentcall from CookieBot or any other consent tool fails withFailed to sync consent with Shopify: Error while setting storefront API consent: Failed to fetch, even though the same-origin SFAPI proxy is healthy.Two related root causes:
storefrontRootDomainwas being passed with a leading dot ('.' + commonAncestorDomain). The public Customer Privacy API contract documents this field as a bare hostname (e.g."hydrogen.shop", no dot). The leading dot was a vestigial cookie-domain idiom from #3309 — the source comment even called it out as temporary: "Once consent-tracking-api is updated to not rely on cookies anymore, we can remove this."feat: make SF API proxy mandatory and enable backend consent mode #3649 made that update happen. It installs
window.Shopify.customerPrivacy.backendConsentEnabled = true, which switches the Customer Privacy API into a server-set-cookie mode where it issues an additional same-origin SFAPI request (the entire reason the proxy was made mandatory in the same PR). With the bare hostname now treated as the host of an HTTPS fetch, the legacy leading dot produces a malformed URL that gets blocked byconnect-srcCSP and causes the consent caller to surfaceFailed to fetch.A subtler issue: even with a well-formed value, that second request typically targets the registrable root (e.g.
example.com) which usually isn't in a merchant's CSPconnect-srcand doesn't host an SFAPI. So just dropping the dot leaves the failure in place. To fully resolve it for Hydrogen's mandatory same-origin proxy, this PR also omitsstorefrontRootDomainwhenhasSfapiProxyis true, letting the Customer Privacy API fall back towindow.location.hostso that the additional request routes through the same proxy as the primary one.WHAT is this pull request doing?
In
useCustomerPrivacy:storefrontRootDomainso the value matches the public Customer Privacy API contract.storefrontRootDomainentirely whenhasSfapiProxyis true (Oxygen / typical 2026.4+ deployments), so consent-tracking-api falls back towindow.location.host. Both consent fetches now route through Hydrogen's mandatory same-origin SFAPI proxy.hasSfapiProxyto the consent configuseMemodeps.setTrackingConsentpayload for proxy-enabled and proxy-disabled cases, plus an "across all modes" assertion that the leading-dot shape is never re-introduced.generated_docs_data*.json(the embeddeduseCustomerPrivacysource).Compatibility
shop.example.com↔checkout.example.com).example.comURL →Failed to fetchexample.com↔checkout.example.com).example.comURLshop.foo.at↔checkout.foo.de)undefined(no common ancestor)localhost:3000↔checkout.example.com)undefinedsameDomainForStorefrontApi: false(explicit opt-out).example.comURLbackendConsentEnablednot setHOW to test your changes?
Unit tests:
pnpm --filter @shopify/hydrogen test -- --run useCustomerPrivacyManual repro on an affected storefront (any merchant on
@shopify/hydrogen >= 2026.4.0with a checkout subdomain that shares a registrable root with the storefront):Before this PR: a request to
https://.<root>/api/unstable/graphql.jsonis logged in the console as aconnect-srcCSP violation, andsetTrackingConsent's callback fires with{ error: "Error while setting storefront API consent: Failed to fetch" }.After this PR: only same-origin requests to
/api/unstable/graphql.jsonappear, both succeed, and the consent callback fires without an error.Checklist
Made with Cursor