From d3959173b9667b7a91c08a2f2b2fd272b7c50f9c Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Tue, 17 Sep 2024 15:18:42 +0900 Subject: [PATCH 01/12] Show error when returned theme ID does not match --- .../cli/utilities/theme-environment/html.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/theme/src/cli/utilities/theme-environment/html.ts b/packages/theme/src/cli/utilities/theme-environment/html.ts index a66a31f461..61a65b1380 100644 --- a/packages/theme/src/cli/utilities/theme-environment/html.ts +++ b/packages/theme/src/cli/utilities/theme-environment/html.ts @@ -2,9 +2,10 @@ import {getProxyStorefrontHeaders, patchRenderingResponse} from './proxy.js' import {getInMemoryTemplates, injectHotReloadScript} from './hot-reload/server.js' import {render} from './storefront-renderer.js' import {getExtensionInMemoryTemplates} from '../theme-ext-environment/theme-ext-server.js' -import {defineEventHandler, getCookie, setResponseHeader, setResponseStatus, type H3Error} from 'h3' +import {createError, defineEventHandler, getCookie, setResponseHeader, setResponseStatus, type H3Error} from 'h3' import {renderError} from '@shopify/cli-kit/node/ui' import {outputInfo} from '@shopify/cli-kit/node/output' +import type {Response} from '@shopify/cli-kit/node/http' import type {Theme} from '@shopify/cli-kit/node/themes/types' import type {DevServerContext} from './types.js' @@ -29,6 +30,8 @@ export function getHtmlHandler(theme: Theme, ctx: DevServerContext) { html = prettifySyntaxErrors(html) + assertThemeId(response, html, String(theme.id)) + if (ctx.options.liveReload !== 'off') { html = injectHotReloadScript(html) } @@ -50,7 +53,7 @@ export function getHtmlHandler(theme: Theme, ctx: DevServerContext) { let errorPageHtml = getErrorPage({ title, header: title, - message: [...rest, error.message].join('
'), + message: [...rest, cause?.message ?? error.message].join('
'), code: error.stack?.replace(`${error.message}\n`, '') ?? '', }) @@ -98,3 +101,16 @@ function getErrorPage(options: {title: string; header: string; message: string; ` } + +function assertThemeId(response: Response, html: string, expectedThemeId: string) { + const obtainedThemeId = html.match(/Shopify\.theme\s*=\s*{[^}]+?"id":\s*"?(\d+)"?(}|,)/)?.[1] + + if (obtainedThemeId && obtainedThemeId !== expectedThemeId) { + throw createError({ + status: 502, + statusText: 'Bad Gateway', + data: {url: response.url, requestId: response.headers.get('x-request-id')}, + cause: new Error(`Theme ID mismatch: expected ${expectedThemeId} but got ${obtainedThemeId}`), + }) + } +} From 70fc7122e0570eee761f38deef3611c5fdea3e6f Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Tue, 17 Sep 2024 18:54:50 +0900 Subject: [PATCH 02/12] Halt server on error --- .../cli/utilities/theme-environment/html.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/theme/src/cli/utilities/theme-environment/html.ts b/packages/theme/src/cli/utilities/theme-environment/html.ts index 61a65b1380..5f079f3ce1 100644 --- a/packages/theme/src/cli/utilities/theme-environment/html.ts +++ b/packages/theme/src/cli/utilities/theme-environment/html.ts @@ -2,9 +2,10 @@ import {getProxyStorefrontHeaders, patchRenderingResponse} from './proxy.js' import {getInMemoryTemplates, injectHotReloadScript} from './hot-reload/server.js' import {render} from './storefront-renderer.js' import {getExtensionInMemoryTemplates} from '../theme-ext-environment/theme-ext-server.js' -import {createError, defineEventHandler, getCookie, setResponseHeader, setResponseStatus, type H3Error} from 'h3' -import {renderError} from '@shopify/cli-kit/node/ui' +import {defineEventHandler, getCookie, setResponseHeader, setResponseStatus, type H3Error} from 'h3' +import {renderError, renderFatalError} from '@shopify/cli-kit/node/ui' import {outputInfo} from '@shopify/cli-kit/node/output' +import {AbortError} from '@shopify/cli-kit/node/error' import type {Response} from '@shopify/cli-kit/node/http' import type {Theme} from '@shopify/cli-kit/node/themes/types' import type {DevServerContext} from './types.js' @@ -106,11 +107,15 @@ function assertThemeId(response: Response, html: string, expectedThemeId: string const obtainedThemeId = html.match(/Shopify\.theme\s*=\s*{[^}]+?"id":\s*"?(\d+)"?(}|,)/)?.[1] if (obtainedThemeId && obtainedThemeId !== expectedThemeId) { - throw createError({ - status: 502, - statusText: 'Bad Gateway', - data: {url: response.url, requestId: response.headers.get('x-request-id')}, - cause: new Error(`Theme ID mismatch: expected ${expectedThemeId} but got ${obtainedThemeId}`), - }) + renderFatalError( + new AbortError( + `Theme ID mismatch: expected ${expectedThemeId} but got ${obtainedThemeId}.\nRequest ID: ${response.headers.get( + 'x-request-id', + )}\nURL: ${response.url}`, + `This is likely related to an issue in upstream Shopify APIs.\nPlease try again in a few minutes and report this issue:\nhttps://github.com/Shopify/cli/issues/new?template=bug-report.yml`, + ), + ) + + process.exit(1) } } From 1341fd837b921c4df889b9a036deead6dea2c9f0 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Wed, 18 Sep 2024 12:04:02 +0900 Subject: [PATCH 03/12] Add comment with RE example --- .../theme/src/cli/utilities/theme-environment/html.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/theme/src/cli/utilities/theme-environment/html.ts b/packages/theme/src/cli/utilities/theme-environment/html.ts index 5f079f3ce1..0e8a01a2c9 100644 --- a/packages/theme/src/cli/utilities/theme-environment/html.ts +++ b/packages/theme/src/cli/utilities/theme-environment/html.ts @@ -104,6 +104,17 @@ function getErrorPage(options: {title: string; header: string; message: string; } function assertThemeId(response: Response, html: string, expectedThemeId: string) { + /** + * DOM example: + * + * ``` + * + * ``` + */ const obtainedThemeId = html.match(/Shopify\.theme\s*=\s*{[^}]+?"id":\s*"?(\d+)"?(}|,)/)?.[1] if (obtainedThemeId && obtainedThemeId !== expectedThemeId) { From 68a3079ceef488da725d1055e7ab9c19bce4957e Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Wed, 18 Sep 2024 12:04:46 +0900 Subject: [PATCH 04/12] Enhance readability of error message --- .../theme/src/cli/utilities/theme-environment/html.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/theme/src/cli/utilities/theme-environment/html.ts b/packages/theme/src/cli/utilities/theme-environment/html.ts index 0e8a01a2c9..164eb22078 100644 --- a/packages/theme/src/cli/utilities/theme-environment/html.ts +++ b/packages/theme/src/cli/utilities/theme-environment/html.ts @@ -120,10 +120,12 @@ function assertThemeId(response: Response, html: string, expectedThemeId: string if (obtainedThemeId && obtainedThemeId !== expectedThemeId) { renderFatalError( new AbortError( - `Theme ID mismatch: expected ${expectedThemeId} but got ${obtainedThemeId}.\nRequest ID: ${response.headers.get( - 'x-request-id', - )}\nURL: ${response.url}`, - `This is likely related to an issue in upstream Shopify APIs.\nPlease try again in a few minutes and report this issue:\nhttps://github.com/Shopify/cli/issues/new?template=bug-report.yml`, + `Theme ID mismatch: expected ${expectedThemeId} but got ${obtainedThemeId}.` + + `\nRequest ID: ${response.headers.get('x-request-id')}` + + `\nURL: ${response.url}`, + `This is likely related to an issue in upstream Shopify APIs.` + + `\nPlease try again in a few minutes and report this issue:` + + `\nhttps://github.com/Shopify/cli/issues/new?template=bug-report.yml`, ), ) From a79ec38ae008e6525f759c7201961bce7f6207f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Wed, 18 Sep 2024 13:35:58 +0200 Subject: [PATCH 05/12] Add support for contract-based app modules --- .../cli/models/extensions/specification.ts | 13 +++++++++ .../specifications/marketing_activity.ts | 29 ++----------------- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/packages/app/src/cli/models/extensions/specification.ts b/packages/app/src/cli/models/extensions/specification.ts index b13fa76300..d7b6203b8f 100644 --- a/packages/app/src/cli/models/extensions/specification.ts +++ b/packages/app/src/cli/models/extensions/specification.ts @@ -263,6 +263,19 @@ export function createContractBasedConfigModuleSpecification( + identifier: string, +) { + return createExtensionSpecification({ + identifier, + schema: zod.any({}) as unknown as ZodSchemaType, + appModuleFeatures: () => [], + deployConfig: async (config, _) => { + return config + }, + }) +} + function resolveAppConfigTransform(transformConfig?: TransformationConfig | CustomTransformationConfig) { if (!transformConfig) return (content: object) => defaultAppConfigTransform(content as {[key: string]: unknown}) diff --git a/packages/app/src/cli/models/extensions/specifications/marketing_activity.ts b/packages/app/src/cli/models/extensions/specifications/marketing_activity.ts index 7e0becd9e9..c04d233bca 100644 --- a/packages/app/src/cli/models/extensions/specifications/marketing_activity.ts +++ b/packages/app/src/cli/models/extensions/specifications/marketing_activity.ts @@ -1,30 +1,5 @@ -import {MarketingActivityExtensionSchema} from './marketing_activity_schemas/marketing_activity_schema.js' -import {createExtensionSpecification} from '../specification.js' -import {randomUUID} from '@shopify/cli-kit/node/crypto' +import {createContractBasedModuleSpecification} from '../specification.js' -const spec = createExtensionSpecification({ - identifier: 'marketing_activity', - schema: MarketingActivityExtensionSchema, - appModuleFeatures: (_) => ['bundling'], - deployConfig: async (config, _) => { - return { - title: config.title, - description: config.description, - api_path: config.api_path, - tactic: config.tactic, - marketing_channel: config.marketing_channel, - referring_domain: config.referring_domain, - is_automation: config.is_automation, - use_external_editor: config.use_external_editor, - preview_data: config.preview_data, - fields: config.fields.map((field) => ({ - ...field, - // NOTE: we're not using this id anywhere, generating it to satisfy the schema - // decided not to remove it from the schema for now to minimize the risk of breaking changes - id: randomUUID(), - })), - } - }, -}) +const spec = createContractBasedModuleSpecification('marketing_activity') export default spec From a370db82a8f6eb802dac01fa12cd1faef13c324d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Thu, 19 Sep 2024 11:55:57 +0200 Subject: [PATCH 06/12] allow to inject app module features --- packages/app/src/cli/models/extensions/specification.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/src/cli/models/extensions/specification.ts b/packages/app/src/cli/models/extensions/specification.ts index d7b6203b8f..c6de742465 100644 --- a/packages/app/src/cli/models/extensions/specification.ts +++ b/packages/app/src/cli/models/extensions/specification.ts @@ -265,11 +265,12 @@ export function createContractBasedConfigModuleSpecification( identifier: string, + appModuleFeatures?: ExtensionFeature[], ) { return createExtensionSpecification({ identifier, schema: zod.any({}) as unknown as ZodSchemaType, - appModuleFeatures: () => [], + appModuleFeatures: () => appModuleFeatures ?? [], deployConfig: async (config, _) => { return config }, From c0478b441e25a4720bd9c7dc89e332ff19aff9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Thu, 19 Sep 2024 12:02:55 +0200 Subject: [PATCH 07/12] add a simple test --- .../specification.integration.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/app/src/cli/models/extensions/specification.integration.test.ts b/packages/app/src/cli/models/extensions/specification.integration.test.ts index aabb895ab5..b2a2abc941 100644 --- a/packages/app/src/cli/models/extensions/specification.integration.test.ts +++ b/packages/app/src/cli/models/extensions/specification.integration.test.ts @@ -1,4 +1,5 @@ import {loadLocalExtensionsSpecifications} from './load-specifications.js' +import {createContractBasedModuleSpecification} from './specification.js' import {AppSchema} from '../app/app.js' import {describe, test, expect, beforeAll} from 'vitest' @@ -26,3 +27,20 @@ describe('allLocalSpecs', () => { expect(got.length).not.toEqual(0) }) }) + +describe('createContractBasedModuleSpecification', () => { + test('creates a specification with the given identifier', () => { + // When + const got = createContractBasedModuleSpecification('test', ['bundling']) + + // Then + expect(got).toMatchObject( + expect.objectContaining({ + identifier: 'test', + experience: 'extension', + uidStrategy: 'uuid', + }), + ) + expect(got.appModuleFeatures()).toEqual(['bundling']) + }) +}) From cce54b10bb1778ed7762b451514e5b9da05be496 Mon Sep 17 00:00:00 2001 From: Guilherme Carreiro Date: Fri, 20 Sep 2024 07:37:38 +0200 Subject: [PATCH 08/12] Fix Theme Access authentication on `shopify theme dev` and `shopify theme console` commands. --- .changeset/hip-chefs-dance.md | 5 +++++ .../cli/utilities/theme-environment/dev-server-session.ts | 6 +++++- .../theme-environment/storefront-renderer.test.ts | 8 +++++++- .../utilities/theme-environment/storefront-renderer.ts | 7 +++++++ 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 .changeset/hip-chefs-dance.md diff --git a/.changeset/hip-chefs-dance.md b/.changeset/hip-chefs-dance.md new file mode 100644 index 0000000000..803ae4e91b --- /dev/null +++ b/.changeset/hip-chefs-dance.md @@ -0,0 +1,5 @@ +--- +'@shopify/theme': patch +--- + +Fix Theme Access authentication on `shopify theme dev` and `shopify theme console` commands diff --git a/packages/theme/src/cli/utilities/theme-environment/dev-server-session.ts b/packages/theme/src/cli/utilities/theme-environment/dev-server-session.ts index e89c56f7ba..62630ac21f 100644 --- a/packages/theme/src/cli/utilities/theme-environment/dev-server-session.ts +++ b/packages/theme/src/cli/utilities/theme-environment/dev-server-session.ts @@ -50,7 +50,11 @@ async function fetchDevServerSession( const session = await ensureAuthenticatedThemes(adminSession.storeFqdn, adminPassword, []) const storefrontToken = await ensureAuthenticatedStorefront([], adminPassword) - const sessionCookies = await getStorefrontSessionCookies(baseUrl, themeId, storefrontPassword, {}) + const sessionCookies = await getStorefrontSessionCookies(baseUrl, themeId, storefrontPassword, { + 'X-Shopify-Shop': session.storeFqdn, + 'X-Shopify-Access-Token': session.token, + Authorization: `Bearer ${storefrontToken}`, + }) return { ...session, diff --git a/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.test.ts b/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.test.ts index 40071d6d64..c57703fedc 100644 --- a/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.test.ts +++ b/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.test.ts @@ -12,7 +12,7 @@ vi.mock('@shopify/cli-kit/node/http', async () => { } }) -const successResponse = {ok: true, status: 200, headers: {get: vi.fn()}} as any +const successResponse = {ok: true, status: 200, headers: {get: vi.fn(), delete: vi.fn()}} as any const session: DevServerSession = { token: 'admin_token_abc123', @@ -50,6 +50,7 @@ describe('render', () => { // Then expect(response.status).toEqual(200) + expect(response.headers.delete).toBeCalled() expect(fetch).toHaveBeenCalledWith( 'https://store.myshopify.com/products/1?_fd=0&pb=0', expect.objectContaining({ @@ -74,6 +75,7 @@ describe('render', () => { // Then expect(response.status).toEqual(200) + expect(response.headers.delete).toBeCalled() expect(fetch).toHaveBeenCalledWith( 'https://theme-kit-access.shopifyapps.com/cli/sfr/products/1?_fd=0&pb=0', expect.objectContaining({ @@ -109,6 +111,7 @@ describe('render', () => { // Then expect(response.status).toEqual(200) + expect(response.headers.delete).toBeCalled() expect(fetch).toHaveBeenCalledWith( 'https://store.myshopify.com/products/1?_fd=0&pb=0§ion_id=sections--1__announcement-bar', expect.objectContaining({ @@ -135,6 +138,7 @@ describe('render', () => { // Then expect(response.status).toEqual(200) + expect(response.headers.delete).toBeCalled() expect(fetch).toHaveBeenCalledWith( 'https://store.myshopify.com/products/1?_fd=0&pb=0&app_block_id=00001111222233334444', expect.objectContaining({ @@ -162,6 +166,7 @@ describe('render', () => { // Then expect(response.status).toEqual(200) + expect(response.headers.delete).toBeCalled() expect(fetch).toHaveBeenCalledWith( 'https://store.myshopify.com/products/1?_fd=0&pb=0§ion_id=sections--1__announcement-bar', expect.objectContaining({ @@ -191,6 +196,7 @@ describe('render', () => { // Then expect(response.status).toEqual(200) + expect(response.headers.delete).toBeCalled() expect(fetch).toHaveBeenCalledWith( 'https://store.myshopify.com/products/1?_fd=0&pb=0&value=A&value=B', expect.objectContaining({ diff --git a/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.ts b/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.ts index ab04ec0ba5..f9b49f3abd 100644 --- a/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.ts +++ b/packages/theme/src/cli/utilities/theme-environment/storefront-renderer.ts @@ -34,6 +34,13 @@ export async function render(session: DevServerSession, context: DevServerRender const requestId = response.headers.get('x-request-id') outputDebug(`← ${response.status} (request_id: ${requestId})`) + /** + * Theme Access app requests return the 'application/json' content type. + * However, patched renderings will never patch JSON requests; so we're + * consistently discarding the content type. + */ + response.headers.delete('Content-Type') + return response } From 853b20234c1b2f6e3d39ecb62649ec9b3fb95e64 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Fri, 20 Sep 2024 21:52:31 +0900 Subject: [PATCH 09/12] Prevent removing Accept header in proxy to fix cart/add request --- packages/theme/src/cli/utilities/theme-environment/proxy.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/theme/src/cli/utilities/theme-environment/proxy.ts b/packages/theme/src/cli/utilities/theme-environment/proxy.ts index 619411574f..43fdf5603b 100644 --- a/packages/theme/src/cli/utilities/theme-environment/proxy.ts +++ b/packages/theme/src/cli/utilities/theme-environment/proxy.ts @@ -4,7 +4,7 @@ import { defineEventHandler, clearResponseHeaders, sendProxy, - getProxyRequestHeaders, + getRequestHeaders, getRequestWebStream, getRequestIP, type H3Event, @@ -167,7 +167,9 @@ const HOP_BY_HOP_HEADERS = [ 'trailer', 'transfer-encoding', 'upgrade', + 'expect', 'content-security-policy', + 'host', ] function patchProxiedResponseHeaders(ctx: DevServerContext, event: H3Event, response: Response | NodeResponse) { @@ -203,7 +205,7 @@ function patchProxiedResponseHeaders(ctx: DevServerContext, event: H3Event, resp * Filters headers to forward to SFR. */ export function getProxyStorefrontHeaders(event: H3Event) { - const proxyRequestHeaders = getProxyRequestHeaders(event) as {[key: string]: string} + const proxyRequestHeaders = getRequestHeaders(event) as {[key: string]: string} // H3 already removes most hop-by-hop request headers: // https://github.com/unjs/h3/blob/ac6d83de2abe5411d4eaea8ecf2165ace16a65f3/src/utils/proxy.ts#L25 From 492225c47489a6a35d7821b30e22b249f86db3ed Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Fri, 20 Sep 2024 21:53:10 +0900 Subject: [PATCH 10/12] Changesets --- .changeset/sharp-eggs-remember.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sharp-eggs-remember.md diff --git a/.changeset/sharp-eggs-remember.md b/.changeset/sharp-eggs-remember.md new file mode 100644 index 0000000000..b38333b4d8 --- /dev/null +++ b/.changeset/sharp-eggs-remember.md @@ -0,0 +1,5 @@ +--- +'@shopify/theme': patch +--- + +Fix cart/add request in development. From 5957ef08c3dd9623e36691f4ca934d2ff1b5af12 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Fri, 20 Sep 2024 22:01:43 +0900 Subject: [PATCH 11/12] Cleanup --- packages/theme/src/cli/utilities/theme-environment/proxy.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/theme/src/cli/utilities/theme-environment/proxy.ts b/packages/theme/src/cli/utilities/theme-environment/proxy.ts index 43fdf5603b..4387ce0e48 100644 --- a/packages/theme/src/cli/utilities/theme-environment/proxy.ts +++ b/packages/theme/src/cli/utilities/theme-environment/proxy.ts @@ -207,8 +207,6 @@ function patchProxiedResponseHeaders(ctx: DevServerContext, event: H3Event, resp export function getProxyStorefrontHeaders(event: H3Event) { const proxyRequestHeaders = getRequestHeaders(event) as {[key: string]: string} - // H3 already removes most hop-by-hop request headers: - // https://github.com/unjs/h3/blob/ac6d83de2abe5411d4eaea8ecf2165ace16a65f3/src/utils/proxy.ts#L25 for (const headerKey of HOP_BY_HOP_HEADERS) { delete proxyRequestHeaders[headerKey] } From fa91638660d16820048fc3551e37c3519480adf2 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Fri, 20 Sep 2024 22:03:17 +0900 Subject: [PATCH 12/12] Fix unit tests --- .../theme/src/cli/utilities/theme-environment/proxy.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 3f01315cfc..d6e9ba9550 100644 --- a/packages/theme/src/cli/utilities/theme-environment/proxy.test.ts +++ b/packages/theme/src/cli/utilities/theme-environment/proxy.test.ts @@ -180,9 +180,9 @@ describe('dev proxy', () => { // Removed: event.node.req.headers.connection = '...' event.node.req.headers['proxy-authenticate'] = '...' - event.node.req.headers.accept = 'text/html' event.node.req.headers.host = 'abnb' // Kept: + event.node.req.headers.accept = 'text/html' event.node.req.headers.cookie = 'oreo' event.node.req.headers['user-agent'] = 'vitest' event.node.req.headers['x-custom'] = 'true' @@ -190,6 +190,7 @@ describe('dev proxy', () => { expect(getProxyStorefrontHeaders(event)).toMatchInlineSnapshot(` { "X-Forwarded-For": "42", + "accept": "text/html", "cookie": "oreo", "user-agent": "vitest", "x-custom": "true",