Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-toys-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/theme': patch
---

Ignore API collect endpoint in `shopify theme dev` and `shopify app dev` commands
Original file line number Diff line number Diff line change
@@ -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())
Expand Down Expand Up @@ -120,6 +127,50 @@ describe('dev proxy', () => {
`"<https://cdn.shopify.com>; rel=\\"preconnect\\", <https://cdn.shopify.com>; rel=\\"preconnect\\"; crossorigin,</cdn/shop/t/10/assets/component-localization-form.css?v=120620094879297847921723560016>; as=\\"style\\"; rel=\\"preload\\""`,
)
})

test('proxies beacon endpoint URLs to local server', () => {
const content = html`<html>
<head></head>
<body>
<div data-shs-beacon-endpoint="https://my-store.myshopify.com/api/collect"></div>
</body>
</html>`

expect(injectCdnProxy(content, ctx)).toMatchInlineSnapshot(`
"<html>
<head></head>
<body>
<div data-shs-beacon-endpoint=\\"/api/collect\\"></div>
</body>
</html>"
`)
})

test('proxies beacon endpoint URLs with single quotes', () => {
const content = `<div data-shs-beacon-endpoint='https://my-store.myshopify.com/api/collect'></div>`

expect(injectCdnProxy(content, ctx)).toMatchInlineSnapshot(
`"<div data-shs-beacon-endpoint='/api/collect'></div>"`,
)
})

test('proxies multiple beacon endpoint URLs in the same content', () => {
const content = html`<html>
<body>
<div data-shs-beacon-endpoint="https://my-store.myshopify.com/api/collect"></div>
<span data-shs-beacon-endpoint="https://my-store.myshopify.com/api/collect"></span>
</body>
</html>`

expect(injectCdnProxy(content, ctx)).toMatchInlineSnapshot(`
"<html>
<body>
<div data-shs-beacon-endpoint=\\"/api/collect\\"></div>
<span data-shs-beacon-endpoint=\\"/api/collect\\"></span>
</body>
</html>"
`)
})
})

describe('patchRenderingResponse', () => {
Expand Down Expand Up @@ -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()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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)?\/?$/
Expand All @@ -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',
]
Expand Down Expand Up @@ -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)
Expand Down
Loading