From 6588df956a579acd0736a98a0dfb4bd3f2f6faaf Mon Sep 17 00:00:00 2001 From: Thomas <31560900+0xtlt@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:19:09 +0200 Subject: [PATCH] fix: handle CORS preflight in reverse proxy during local dev --- .../utilities/app/http-reverse-proxy.test.ts | 28 +++++++++++++++++++ .../cli/utilities/app/http-reverse-proxy.ts | 12 ++++++++ pnpm-lock.yaml | 4 +-- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/app/src/cli/utilities/app/http-reverse-proxy.test.ts b/packages/app/src/cli/utilities/app/http-reverse-proxy.test.ts index db354ab7720..37b680c2f22 100644 --- a/packages/app/src/cli/utilities/app/http-reverse-proxy.test.ts +++ b/packages/app/src/cli/utilities/app/http-reverse-proxy.test.ts @@ -55,6 +55,34 @@ describe.sequential.each(each)('http-reverse-proxy for %s', (protocol) => { }) }) + test('responds to CORS preflight OPTIONS with default headers', {retry: 2}, async ({ports, servers}) => { + const response = await fetch(`${protocol}://localhost:${ports.proxyPort}/path1/test`, { + method: 'OPTIONS', + headers: { + Origin: 'https://extensions.shopifycdn.com', + 'Access-Control-Request-Method': 'GET', + 'Access-Control-Request-Headers': 'Authorization', + }, + agent, + }) + expect(response.status).toBe(204) + expect(response.headers.get('access-control-allow-origin')).toBe('https://extensions.shopifycdn.com') + expect(response.headers.get('access-control-allow-methods')).toBe('GET') + expect(response.headers.get('access-control-allow-headers')).toBe('Authorization') + expect(response.headers.get('access-control-max-age')).toBe('86400') + }) + + test('responds to CORS preflight OPTIONS with defaults when no request headers', {retry: 2}, async ({ports, servers}) => { + const response = await fetch(`${protocol}://localhost:${ports.proxyPort}/path1/test`, { + method: 'OPTIONS', + agent, + }) + expect(response.status).toBe(204) + expect(response.headers.get('access-control-allow-origin')).toBe('*') + expect(response.headers.get('access-control-allow-methods')).toBe('GET, POST, PUT, DELETE, PATCH, OPTIONS') + expect(response.headers.get('access-control-allow-headers')).toBe('Content-Type, Authorization') + }) + test('closes the server when aborted', {retry: 2}, async ({ports, servers}) => { servers.abortController.abort() // Try the assertion immediately, and if it fails, wait and retry diff --git a/packages/app/src/cli/utilities/app/http-reverse-proxy.ts b/packages/app/src/cli/utilities/app/http-reverse-proxy.ts index 9304bab8e84..86c1e4397ec 100644 --- a/packages/app/src/cli/utilities/app/http-reverse-proxy.ts +++ b/packages/app/src/cli/utilities/app/http-reverse-proxy.ts @@ -70,6 +70,18 @@ function getProxyServerRequestListener( return function (req, res) { const target = match(rules, req) if (target) { + // Handle CORS preflight requests directly + // The proxy does not forward OPTIONS reliably, so we respond here + // using the headers requested by the client. + if (req.method === 'OPTIONS') { + res.writeHead(204, { + 'Access-Control-Allow-Origin': req.headers['origin'] ?? '*', + 'Access-Control-Allow-Methods': req.headers['access-control-request-method'] ?? 'GET, POST, PUT, DELETE, PATCH, OPTIONS', + 'Access-Control-Allow-Headers': req.headers['access-control-request-headers'] ?? 'Content-Type, Authorization', + 'Access-Control-Max-Age': '86400', + }) + return res.end() + } return proxy.web(req, res, {target}, (err) => { useConcurrentOutputContext({outputPrefix: 'proxy', stripAnsi: false}, () => { const lastError = isAggregateError(err) ? err.errors[err.errors.length - 1] : undefined diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56e2b589cde..663d840f340 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5917,12 +5917,12 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + deprecated: Glob versions prior to v9 are no longer supported glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + deprecated: Glob versions prior to v9 are no longer supported global-agent@3.0.0: resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==}