Skip to content

fix(consent): stop passing a malformed storefrontRootDomain#3750

Open
z0n wants to merge 1 commit into
Shopify:mainfrom
z0n:fix/checkout-consent-transfer
Open

fix(consent): stop passing a malformed storefrontRootDomain#3750
z0n wants to merge 1 commit into
Shopify:mainfrom
z0n:fix/checkout-consent-transfer

Conversation

@z0n
Copy link
Copy Markdown
Contributor

@z0n z0n commented Apr 28, 2026

WHY are these changes introduced?

useCustomerPrivacy has been emitting consent-sync requests to malformed URLs like https://.example.com/api/unstable/graphql.json ever since #3649 (@shopify/hydrogen@2026.4.0). On affected storefronts, every setTrackingConsent call from CookieBot or any other consent tool fails with Failed 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:

  1. storefrontRootDomain was 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."

  2. 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 by connect-src CSP and causes the consent caller to surface Failed 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 CSP connect-src and 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 omits storefrontRootDomain when hasSfapiProxy is true, letting the Customer Privacy API fall back to window.location.host so that the additional request routes through the same proxy as the primary one.

WHAT is this pull request doing?

In useCustomerPrivacy:

  • Drops the leading dot from storefrontRootDomain so the value matches the public Customer Privacy API contract.
  • Omits storefrontRootDomain entirely when hasSfapiProxy is true (Oxygen / typical 2026.4+ deployments), so consent-tracking-api falls back to window.location.host. Both consent fetches now route through Hydrogen's mandatory same-origin SFAPI proxy.
  • Adds hasSfapiProxy to the consent config useMemo deps.
  • Adds regression tests covering the wrapped setTrackingConsent payload for proxy-enabled and proxy-disabled cases, plus an "across all modes" assertion that the leading-dot shape is never re-introduced.
  • Refreshes generated_docs_data*.json (the embedded useCustomerPrivacy source).

Compatibility

Deployment Before After
Oxygen, typical (shop.example.comcheckout.example.com) malformed .example.com URL → Failed to fetch both consent fetches route through the proxy → success
Oxygen, apex storefront (example.comcheckout.example.com) malformed .example.com URL both fetches route through the proxy
Multi-market with no shared root (shop.foo.atcheckout.foo.de) already undefined (no common ancestor) unchanged
Local dev (localhost:3000checkout.example.com) already undefined unchanged
sameDomainForStorefrontApi: false (explicit opt-out) malformed .example.com URL bare hostname (matches docs); the additional fetch may still 404 if the registrable root has no SFAPI, but the URL is no longer malformed
Hydrogen pre-2026.4 consumers N/A — backendConsentEnabled not set N/A — legacy cookie-domain path unaffected (RFC 6265 normalizes leading dots)

HOW to test your changes?

Unit tests:

pnpm --filter @shopify/hydrogen test -- --run useCustomerPrivacy

Manual repro on an affected storefront (any merchant on @shopify/hydrogen >= 2026.4.0 with a checkout subdomain that shares a registrable root with the storefront):

  1. Open the storefront in a browser and accept consent in your consent tool (CookieBot, OneTrust, etc.).
  2. Inspect the network tab and the console.

Before this PR: a request to https://.<root>/api/unstable/graphql.json is logged in the console as a connect-src CSP violation, and setTrackingConsent'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.json appear, both succeed, and the consent callback fires without an error.

Checklist

  • I've read the Contributing Guidelines
  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've added a changeset if this PR contains user-facing or functional changes. Test changes or internal-only config changes do not require a changeset.
  • I've added tests to cover my changes
  • I've added or updated the documentation

Made with Cursor

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>
@z0n
Copy link
Copy Markdown
Contributor Author

z0n commented Apr 28, 2026

Notes: Thanks to Cursor for analyzing and fixing this 😄

I had this issue in my Hydrogen store where my PUBLIC_CHECKOUT_DOMAIN was set correctly but the consent calls were going to .example.org (while my PUBLIC_CHECKOUT_DOMAIN was something like checkout.example.org) and the Shopify storefront domain something like shopify.example.org.

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.

@z0n
Copy link
Copy Markdown
Contributor Author

z0n commented Apr 28, 2026

/snapit

@github-actions
Copy link
Copy Markdown
Contributor

Only users with write permission to the repository can run /snapit

@z0n
Copy link
Copy Markdown
Contributor Author

z0n commented Apr 29, 2026

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 shopify.example.org and a checkout on checkout.example.org.

With this fix, Hydrogen will now call https://shopify.example.org/api/unstable/graphql.json which is fine, however the Set-Cookie header I get in the response will be for the wrong domain. Instead of example.org, I will get the "default" Shopify store domain (e.g. saidf-ds.myshopify.com for the cookie. Not sure why that is, I noticed in my Oxygen settings that the read-only PUBLIC_STORE_DOMAIN is set to that saidf-ds.myshopify.com domain so that might be part of the problem. I guess I need to open a Shopify support ticket for that as it's not part of Hydrogen anymore.

Fun fact: If I run the request against the checkout domain (checkout.example.org), I do get the cookie for the example.org domain.

@z0n z0n marked this pull request as ready for review April 29, 2026 10:00
@z0n z0n requested a review from a team as a code owner April 29, 2026 10:00
@z0n
Copy link
Copy Markdown
Contributor Author

z0n commented May 5, 2026

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 .example.org and a consent sync error is triggered).

Might be a good idea to have this checked by an experienced Shopify 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