Pass Storefront API requests through the dev proxy as-is#7377
Conversation
Requests to /api/{version|unstable}/graphql.json were returning 401
because the dev proxy injected the SFR devtools bearer via the
Authorization header. The public Storefront API rejects that token —
it expects X-Shopify-Storefront-Access-Token from the caller.
Forward these requests untouched (no Authorization, Cookie, referer,
or _fd/pb params) so the caller's own auth passes through. Introduces
isPassthroughRequest(event) as the extension point for future
pass-through rules.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adjusts the theme dev proxy so Storefront API GraphQL requests aren’t broken by theme-dev auth injection, preventing 401s when callers use their own Storefront API credentials.
Changes:
- Adds a Storefront API path matcher and
isPassthroughRequest(event)hook for future passthrough rules. - Skips adding theme auth/cookies/dev query params for
/api/{version|unstable}/graphql.jsonrequests. - Adds Vitest coverage for Storefront API passthrough behavior and a patch changeset.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/theme/src/cli/utilities/theme-environment/proxy.ts | Introduces SFAPI passthrough detection and conditionally avoids injecting theme-specific headers/query params. |
| packages/theme/src/cli/utilities/theme-environment/proxy.test.ts | Adds tests validating the passthrough path and non-matching fallback behavior. |
| .changeset/itchy-jobs-report.md | Patch changeset documenting the SFAPI proxy fix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Cookie: buildCookies(ctx.session, {headers}), | ||
| // Only include Authorization for theme dev, not theme-extensions | ||
| ...(ctx.type === 'theme' ? {Authorization: `Bearer ${ctx.session.storefrontToken}`} : {}), | ||
| }) |
There was a problem hiding this comment.
isPassthroughRequest skips the injection block, but the passthrough path still forwards any client-supplied cookie, authorization, and referer headers (from getProxyStorefrontHeaders(event)), and also forwards _fd/pb if they were already present in the incoming URL. This contradicts the stated intent of not sending auth/cookies/dev params to the public Storefront API, and can also leak caller Authorization tokens upstream. Consider explicitly deleting cookie/Cookie, authorization/Authorization, and referer from headers (and deleting _fd/pb from url.searchParams) when isPassthroughRequest(event) is true.
| }) | |
| }) | |
| } else { | |
| delete headers.cookie | |
| delete headers.Cookie | |
| delete headers.authorization | |
| delete headers.Authorization | |
| delete headers.referer | |
| delete headers.Referer | |
| url.searchParams.delete('_fd') | |
| url.searchParams.delete('pb') |
| test('forwards /api/YYYY-MM/graphql.json without injecting theme auth, cookies, referer, or dev params', async () => { | ||
| const event = createH3Event('POST', '/api/2026-01/graphql.json', { | ||
| 'x-shopify-storefront-access-token': 'public-access-token', | ||
| authorization: 'Bearer client-supplied-token', | ||
| }) | ||
|
|
||
| await proxyStorefrontRequest(event, passthroughCtx) | ||
|
|
||
| expect(fetchMock).toHaveBeenCalledOnce() | ||
| const [requestUrl, init] = fetchMock.mock.calls[0] as [URL, RequestInit] | ||
|
|
||
| expect(requestUrl.toString()).toBe('https://my-store.myshopify.com/api/2026-01/graphql.json') | ||
| expect(requestUrl.searchParams.has('_fd')).toBe(false) | ||
| expect(requestUrl.searchParams.has('pb')).toBe(false) | ||
|
|
||
| const headers = init.headers as Record<string, string> | ||
| expect(headers['x-shopify-storefront-access-token']).toBe('public-access-token') | ||
| expect(headers.authorization).toBe('Bearer client-supplied-token') | ||
| expect(headers.Authorization).toBeUndefined() | ||
| expect(headers.Cookie).toBeUndefined() | ||
| expect(headers.referer).toBeUndefined() | ||
| }) |
There was a problem hiding this comment.
The passthrough tests currently assert headers.Cookie/headers.Authorization are undefined, but getProxyStorefrontHeaders() produces lowercased keys (see existing snapshot in this file), so these assertions don’t prove that cookies/authorization aren’t being forwarded (they could still be present as headers.cookie / headers.authorization). If the goal is to ensure nothing auth/cookie-like is forwarded for SFAPI, update the test to set cookie/referer/authorization on the incoming request and assert both casings are absent (or, if forwarding caller auth is intended, clarify expectations and ensure the proxy doesn’t overwrite it).
There was a problem hiding this comment.
Before this gets merged let's get @EvilGenius13 to look at it who has done a bunch of header stuff recently to prevent 401 and 429s.
There was a problem hiding this comment.
This should be okay. What matters most is that regular proxy requests to storefront maintain having user agent and bearer token on all requests. This bypass looks safe.
WHY are these changes introduced?
Requests to /api/{version|unstable}/graphql.json were returning 401 because the dev proxy injected the SFR devtools bearer via the Authorization header. The public Storefront API rejects that token — it expects X-Shopify-Storefront-Access-Token from the caller.
WHAT is this pull request doing?
Forward these requests untouched (no Authorization, Cookie, referer, or _fd/pb params) so the caller's own auth passes through. Introduces isPassthroughRequest(event) as the extension point for future pass-through rules.
How to test your changes?
Make a request to SFAPI from the Chrome dev tools. Without this PR it will return 401.
Post-release steps
Checklist
patchfor bug fixes ·minorfor new features ·majorfor breaking changes) and added a changeset withpnpm changeset add