From 74e729b1d44824903423132d74a71576fec5ff2f Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Fri, 11 Sep 2026 21:46:44 +0530 Subject: [PATCH 1/2] fix(cli): avoid cloning function requests (CLI-2415) --- .../functions/serve-main-offline.e2e.test.ts | 37 +++++++++++++------ apps/cli/src/shared/functions/serve.main.ts | 8 ++-- packages/stack/src/functions/serve.main.ts | 8 ++-- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/apps/cli/src/shared/functions/serve-main-offline.e2e.test.ts b/apps/cli/src/shared/functions/serve-main-offline.e2e.test.ts index 8868d39a90..badf6bf0d9 100644 --- a/apps/cli/src/shared/functions/serve-main-offline.e2e.test.ts +++ b/apps/cli/src/shared/functions/serve-main-offline.e2e.test.ts @@ -78,17 +78,22 @@ const KONG_FUNCTIONS_CONFIG = JSON.stringify({ }); const CUSTOM_FUNCTION = `import { sharedValue } from "../_shared/value.ts"; -Deno.serve(() => new Response("ok", { - headers: { - "X-Custom-Id": "abc123", - "X-Function-Slug": Deno.env.get("SUPABASE_FUNCTION_SLUG") ?? "", - "X-Shared-Import": sharedValue, - "X-Shared": Deno.env.get("SHARED") ?? "", - "X-Function-Only": Deno.env.get("FUNCTION_ONLY") ?? "", - "X-Global-Only": Deno.env.get("GLOBAL_ONLY") ?? "", - "Access-Control-Expose-Headers": "X-Custom-Id", - }, -}));`; +Deno.serve((req) => { + if (req.headers.get("x-reject-before-body") === "true") { + return new Response("rejected", { status: 400 }); + } + return new Response("ok", { + headers: { + "X-Custom-Id": "abc123", + "X-Function-Slug": Deno.env.get("SUPABASE_FUNCTION_SLUG") ?? "", + "X-Shared-Import": sharedValue, + "X-Shared": Deno.env.get("SHARED") ?? "", + "X-Function-Only": Deno.env.get("FUNCTION_ONLY") ?? "", + "X-Global-Only": Deno.env.get("GLOBAL_ONLY") ?? "", + "Access-Control-Expose-Headers": "X-Custom-Id", + }, + }); +});`; const NESTED_FUNCTION = `Deno.serve(() => new Response("ok", { headers: { "X-Function-Slug": Deno.env.get("SUPABASE_FUNCTION_SLUG") ?? "", @@ -340,7 +345,7 @@ describe("functions serve runtime template (offline)", () => { ); test.skipIf(!dockerAvailable)( - "preserves function env and CORS headers and exposes JWT errors through Kong", + "preserves function env and CORS headers, exposes JWT errors, and returns early responses through Kong", { timeout: SERVE_OFFLINE_TEST_TIMEOUT_MS }, async () => { const imageDeadline = resolveDeadline(); @@ -492,6 +497,14 @@ describe("functions serve runtime template (offline)", () => { expect(aliasResponse.headers.get("x-shared-import")).toBe("shared-import-ok"); expect(nestedResponse.status).toBe(200); expect(nestedResponse.headers.get("x-function-slug")).toBe("nested-worker-path"); + const earlyResponse = await fetch(`${functionsUrl}/custom`, { + method: "POST", + headers: { "x-reject-before-body": "true" }, + body: new Uint8Array(128 * 1024), + signal: AbortSignal.timeout(5_000), + }); + expect(earlyResponse.status).toBe(400); + expect(await earlyResponse.text()).toBe("rejected"); const runtimeLogs = containerLogs(runtimeContainer); expect(runtimeLogs).toContain("Functions config:"); expect(runtimeLogs).toContain('"custom"'); diff --git a/apps/cli/src/shared/functions/serve.main.ts b/apps/cli/src/shared/functions/serve.main.ts index 2b70f91caa..0bb1eaee82 100644 --- a/apps/cli/src/shared/functions/serve.main.ts +++ b/apps/cli/src/shared/functions/serve.main.ts @@ -288,12 +288,12 @@ export function prepareUserRequest(req: Request): Request { const clonedURL = new URL(req.url); const forwardedHost = req.headers.get("x-forwarded-host"); clonedURL.hostname = forwardedHost ?? clonedURL.hostname; - const clonedReq = new Request(clonedURL, req.clone()); + const forwardedReq = new Request(clonedURL, req); - clonedReq.headers.delete("sb-api-key"); - EdgeRuntime.applySupabaseTag(req, clonedReq); + forwardedReq.headers.delete("sb-api-key"); + EdgeRuntime.applySupabaseTag(req, forwardedReq); - return clonedReq; + return forwardedReq; } Deno.serve({ diff --git a/packages/stack/src/functions/serve.main.ts b/packages/stack/src/functions/serve.main.ts index 67656d04f6..876572da71 100644 --- a/packages/stack/src/functions/serve.main.ts +++ b/packages/stack/src/functions/serve.main.ts @@ -195,10 +195,10 @@ export function prepareUserRequest(request: Request): Request { const url = new URL(request.url); const forwardedHost = request.headers.get("x-forwarded-host"); if (forwardedHost) url.hostname = forwardedHost; - const cloned = new Request(url, request.clone()); - cloned.headers.delete("sb-api-key"); - EdgeRuntime.applySupabaseTag(request, cloned); - return cloned; + const forwarded = new Request(url, request); + forwarded.headers.delete("sb-api-key"); + EdgeRuntime.applySupabaseTag(request, forwarded); + return forwarded; } Deno.serve({ From 9299468248e9f410ee369fd1208f7ca359c92899 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Sat, 12 Sep 2026 00:47:28 +0530 Subject: [PATCH 2/2] nits --- .../functions/serve-main-offline.e2e.test.ts | 31 +++++++++++++++---- apps/cli/src/shared/functions/serve.main.ts | 1 + packages/stack/src/functions/serve.main.ts | 1 + .../stack/src/public/whole-stack.e2e.test.ts | 14 ++++++++- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/apps/cli/src/shared/functions/serve-main-offline.e2e.test.ts b/apps/cli/src/shared/functions/serve-main-offline.e2e.test.ts index badf6bf0d9..c66812fff4 100644 --- a/apps/cli/src/shared/functions/serve-main-offline.e2e.test.ts +++ b/apps/cli/src/shared/functions/serve-main-offline.e2e.test.ts @@ -149,6 +149,21 @@ function containerLogs(container: string): string { return `${result.stdout ?? ""}\n${result.stderr ?? ""}`; } +async function fetchFunctionWithDiagnostics( + url: string, + diagnosticContainers: readonly string[], + init: RequestInit, +): Promise { + try { + return await fetch(url, init); + } catch (cause) { + const diagnostics = diagnosticContainers + .map((container) => `${container} logs:\n${containerLogs(container)}`) + .join("\n"); + throw new Error(`Function request to ${url} failed.\n${diagnostics}`, { cause }); + } +} + async function fetchColdFunction( url: string, diagnosticContainers: readonly string[], @@ -497,12 +512,16 @@ describe("functions serve runtime template (offline)", () => { expect(aliasResponse.headers.get("x-shared-import")).toBe("shared-import-ok"); expect(nestedResponse.status).toBe(200); expect(nestedResponse.headers.get("x-function-slug")).toBe("nested-worker-path"); - const earlyResponse = await fetch(`${functionsUrl}/custom`, { - method: "POST", - headers: { "x-reject-before-body": "true" }, - body: new Uint8Array(128 * 1024), - signal: AbortSignal.timeout(5_000), - }); + const earlyResponse = await fetchFunctionWithDiagnostics( + `${functionsUrl}/custom`, + diagnosticContainers, + { + method: "POST", + headers: { "x-reject-before-body": "true" }, + body: new Uint8Array(128 * 1024), + signal: AbortSignal.timeout(5_000), + }, + ); expect(earlyResponse.status).toBe(400); expect(await earlyResponse.text()).toBe("rejected"); const runtimeLogs = containerLogs(runtimeContainer); diff --git a/apps/cli/src/shared/functions/serve.main.ts b/apps/cli/src/shared/functions/serve.main.ts index 0bb1eaee82..37d5f09801 100644 --- a/apps/cli/src/shared/functions/serve.main.ts +++ b/apps/cli/src/shared/functions/serve.main.ts @@ -288,6 +288,7 @@ export function prepareUserRequest(req: Request): Request { const clonedURL = new URL(req.url); const forwardedHost = req.headers.get("x-forwarded-host"); clonedURL.hostname = forwardedHost ?? clonedURL.hostname; + // Cloning tees the body, so an unread branch can stall early worker responses. const forwardedReq = new Request(clonedURL, req); forwardedReq.headers.delete("sb-api-key"); diff --git a/packages/stack/src/functions/serve.main.ts b/packages/stack/src/functions/serve.main.ts index 876572da71..2b830002fd 100644 --- a/packages/stack/src/functions/serve.main.ts +++ b/packages/stack/src/functions/serve.main.ts @@ -195,6 +195,7 @@ export function prepareUserRequest(request: Request): Request { const url = new URL(request.url); const forwardedHost = request.headers.get("x-forwarded-host"); if (forwardedHost) url.hostname = forwardedHost; + // Cloning tees the body, so an unread branch can stall early worker responses. const forwarded = new Request(url, request); forwarded.headers.delete("sb-api-key"); EdgeRuntime.applySupabaseTag(request, forwarded); diff --git a/packages/stack/src/public/whole-stack.e2e.test.ts b/packages/stack/src/public/whole-stack.e2e.test.ts index 2e35a88892..0a00f97f28 100644 --- a/packages/stack/src/public/whole-stack.e2e.test.ts +++ b/packages/stack/src/public/whole-stack.e2e.test.ts @@ -461,7 +461,10 @@ const serviceHeaders = (credentials: PromiseStackCredentials): Record ` -Deno.serve(async () => { +Deno.serve(async (request) => { + if (request.headers.get("x-reject-before-body") === "true") { + return new Response("rejected", { status: 400 }); + } console.log("${marker}"); let publishableKey: unknown; try { @@ -838,6 +841,15 @@ const exerciseWholeStackFunctions = async (scenario: WholeStackScenario): Promis }), ); + const earlyResponse = await fetch(new URL(functionPath, `${api.url.replace(/\/$/u, "")}/`), { + method: "POST", + headers: { ...apiHeaders(credentials), "x-reject-before-body": "true" }, + body: new Uint8Array(128 * 1024), + signal: AbortSignal.timeout(5_000), + }); + expect(earlyResponse.status).toBe(400); + expect(await earlyResponse.text()).toBe("rejected"); + await writeFile( join(projectRoot, "supabase", "functions", functionSlug, "index.ts"), functionSource(table, markers.second),