diff --git a/.changeset/quiet-toys-admire.md b/.changeset/quiet-toys-admire.md new file mode 100644 index 0000000000..598838ac0a --- /dev/null +++ b/.changeset/quiet-toys-admire.md @@ -0,0 +1,5 @@ +--- +'@shopify/theme': patch +--- + +Ignore API collect endpoint in `shopify theme dev` and `shopify app dev` commands diff --git a/packages/theme/src/cli/utilities/theme-environment/proxy.test.ts b/packages/theme/src/cli/utilities/theme-environment/proxy.test.ts index 5775b1d5f7..cc409271a7 100644 --- a/packages/theme/src/cli/utilities/theme-environment/proxy.test.ts +++ b/packages/theme/src/cli/utilities/theme-environment/proxy.test.ts @@ -1,9 +1,16 @@ -import {canProxyRequest, getProxyStorefrontHeaders, injectCdnProxy, patchRenderingResponse} from './proxy.js' +import { + canProxyRequest, + getProxyHandler, + getProxyStorefrontHeaders, + injectCdnProxy, + patchRenderingResponse, +} from './proxy.js' import {describe, test, expect} from 'vitest' import {createEvent} from 'h3' import {IncomingMessage, ServerResponse} from 'node:http' import {Socket} from 'node:net' import type {DevServerContext} from './types.js' +import type {Theme} from '@shopify/cli-kit/node/themes/types' function createH3Event(method = 'GET', path = '/', headers = {}) { const req = new IncomingMessage(new Socket()) @@ -120,6 +127,50 @@ describe('dev proxy', () => { `"; rel=\\"preconnect\\", ; rel=\\"preconnect\\"; crossorigin,; as=\\"style\\"; rel=\\"preload\\""`, ) }) + + test('proxies beacon endpoint URLs to local server', () => { + const content = html` + + +
+ + ` + + expect(injectCdnProxy(content, ctx)).toMatchInlineSnapshot(` + " + + +
+ + " + `) + }) + + test('proxies beacon endpoint URLs with single quotes', () => { + const content = `
` + + expect(injectCdnProxy(content, ctx)).toMatchInlineSnapshot( + `"
"`, + ) + }) + + test('proxies multiple beacon endpoint URLs in the same content', () => { + const content = html` + +
+ + + ` + + expect(injectCdnProxy(content, ctx)).toMatchInlineSnapshot(` + " + +
+ + + " + `) + }) }) describe('patchRenderingResponse', () => { @@ -338,4 +389,33 @@ describe('dev proxy', () => { expect(canProxyRequest(event)).toBeTruthy() }) }) + + describe('getProxyHandler', () => { + const mockTheme = {} as Theme + + test.each([ + '/.well-known', + '/shopify/monorail', + '/mini-profiler-resources', + '/web-pixels-manager', + '/web-pixels@', + '/wpm', + '/services/', + '/api/event/collect', + '/api/collect', + '/cdn-cgi/challenge-platform', + ])('should return null for ignored endpoint: %s', async (endpoint) => { + const event = createH3Event('GET', endpoint) + const handler = getProxyHandler(mockTheme, ctx) + const result = await handler(event) + expect(result).toBeNull() + }) + + test('should return null for ignored endpoint with additional path segments', async () => { + const event = createH3Event('GET', '/api/event/collect/some/path') + const handler = getProxyHandler({} as Theme, ctx) + const result = await handler(event) + expect(result).toBeNull() + }) + }) }) diff --git a/packages/theme/src/cli/utilities/theme-environment/proxy.ts b/packages/theme/src/cli/utilities/theme-environment/proxy.ts index d228837ef6..d82dd8bdac 100644 --- a/packages/theme/src/cli/utilities/theme-environment/proxy.ts +++ b/packages/theme/src/cli/utilities/theme-environment/proxy.ts @@ -14,6 +14,7 @@ import type {DevServerContext} from './types.js' export const VANITY_CDN_PREFIX = '/cdn/' export const EXTENSION_CDN_PREFIX = '/ext/cdn/' +const API_COLLECT_PATH = '/api/collect' const CART_PATTERN = /^\/cart\// const CHECKOUT_PATTERN = /^\/checkouts\/(?!internal\/)/ const ACCOUNT_PATTERN = /^\/account(\/login\/multipass(\/[^/]+)?|\/logout)?\/?$/ @@ -28,7 +29,8 @@ const IGNORED_ENDPOINTS = [ '/web-pixels@', '/wpm', '/services/', - '/api/collect', + '/api/event/collect', + API_COLLECT_PATH, // Cloudflare's turnstile challenge #6416 '/cdn-cgi/challenge-platform', ] @@ -125,6 +127,11 @@ function getStoreFqdnForRegEx(ctx: DevServerContext) { export function injectCdnProxy(originalContent: string, ctx: DevServerContext) { let content = originalContent + // -- The beacon endpoint is patched in injectCdnProxy to be proxied and ignored locally + const beaconEndpointREStr = `(data-shs-beacon-endpoint=["'])https://${getStoreFqdnForRegEx(ctx)}${API_COLLECT_PATH}` + const beaconEndpointRE = new RegExp(beaconEndpointREStr, 'g') + content = content.replace(beaconEndpointRE, `$1${API_COLLECT_PATH}`) + // -- Redirect all usages to the vanity CDN to the local server: const vanityCdnRE = new RegExp(`(https?:)?//${getStoreFqdnForRegEx(ctx)}${VANITY_CDN_PREFIX}`, 'g') content = content.replace(vanityCdnRE, VANITY_CDN_PREFIX)