From f8fca848384f8543ef88eed7856edc1c2759285f 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 1/2] 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 ++++++++ 2 files changed, 40 insertions(+) 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 5b7f9a64db5..2c0069db6c4 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 ({setup}) => { + const response = await fetch(`${protocol}://localhost:${setup.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 ({setup}) => { + const response = await fetch(`${protocol}://localhost:${setup.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 ({setup}) => { setup.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 From 5c549a83a21176704f480c674ff4037a1c687c09 Mon Sep 17 00:00:00 2001 From: Thomas <31560900+0xtlt@users.noreply.github.com> Date: Tue, 12 May 2026 17:37:10 +0200 Subject: [PATCH 2/2] fix: address reverse proxy lint errors --- packages/app/src/cli/utilities/app/http-reverse-proxy.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 86c1e4397ec..a0380b05995 100644 --- a/packages/app/src/cli/utilities/app/http-reverse-proxy.ts +++ b/packages/app/src/cli/utilities/app/http-reverse-proxy.ts @@ -75,9 +75,11 @@ function getProxyServerRequestListener( // 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-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()