From 0f09630645ebcca9bcbb6b3fb201219617870948 Mon Sep 17 00:00:00 2001 From: Cassie_Fox <249835913+CassieFox23@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:10:39 -0400 Subject: [PATCH] fix(scripts): keep the first proxy error in the package-update verifier (#6362) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The registry fixture sent response headers before reading the upstream body, then always wrote a 500 on error. A body failure hit ERR_HTTP_HEADERS_SENT, which hid the real error and crashed the verifier with its helper still running. The fixture now reads the body first, destroys a response that already started, and the verifier rethrows the first fixture error. Refs #6038. 🤖 Generated with Claude Code (Sonnet 5.5) 🦊 Co-authored-by: Claude Sonnet 5.5 --- scripts/verify-package-update.mjs | 12 ++- .../packageUpdateRegistryFailure.test.ts | 98 +++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 tests/boundary/process/packageUpdateRegistryFailure.test.ts diff --git a/scripts/verify-package-update.mjs b/scripts/verify-package-update.mjs index 8d8582b447..0741e3fe5f 100644 --- a/scripts/verify-package-update.mjs +++ b/scripts/verify-package-update.mjs @@ -24,8 +24,14 @@ export async function verifyPackageUpdate({ '[mcp_servers.rea]\ncommand = "npx"\nargs = ["-y", "rea-agents@0.0.0", "mcp"]\n'; await mkdir(dirname(codexPath), { recursive: true }); await writeFile(codexPath, originalConfig); + let fixtureFailure; const server = createServer((request, response) => { void serveRegistry(request, response).catch((error) => { + fixtureFailure ??= error; + if (response.headersSent) { + response.destroy(); + return; + } response.writeHead(500); response.end(String(error)); }); @@ -57,11 +63,12 @@ export async function verifyPackageUpdate({ response.end("{}"); } else { const upstream = await fetch(`https://registry.npmjs.org${request.url}`); + const body = Buffer.from(await upstream.arrayBuffer()); response.writeHead(upstream.status, { "Content-Type": upstream.headers.get("content-type") ?? "application/octet-stream", }); - response.end(Buffer.from(await upstream.arrayBuffer())); + response.end(body); } } await new Promise((resolveListen, reject) => { @@ -171,11 +178,14 @@ if (result.status === "failed") process.exitCode = 1; throw new Error( "Updated CLI did not expose update as its sole maintenance command", ); + if (fixtureFailure !== undefined) throw fixtureFailure; return { installed_version: result.installedVersion, exact_release: true, maintenance_read_only: true, }; + } catch (error) { + throw fixtureFailure ?? error; } finally { server.closeAllConnections(); await new Promise((resolveClose, reject) => diff --git a/tests/boundary/process/packageUpdateRegistryFailure.test.ts b/tests/boundary/process/packageUpdateRegistryFailure.test.ts new file mode 100644 index 0000000000..303e8ac82b --- /dev/null +++ b/tests/boundary/process/packageUpdateRegistryFailure.test.ts @@ -0,0 +1,98 @@ +import { execFile } from "node:child_process"; +import { mkdir, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { promisify } from "node:util"; +import { pathToFileURL } from "node:url"; + +import { expect, it } from "vitest"; + +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const execFileAsync = promisify(execFile); +const injectedFailure = "injected upstream body failure"; + +it("reports the upstream body failure instead of a second header write", async () => { + const testRoot = await createTestTempDirectory("rea-update-registry-"); + const packageRoot = join(testRoot, "package"); + const workspace = join(testRoot, "workspace"); + const prefix = join(testRoot, "prefix"); + const tarball = join(testRoot, "release.tgz"); + const application = join(packageRoot, "dist", "application"); + await Promise.all([ + mkdir(application, { recursive: true }), + mkdir(join(packageRoot, "scripts"), { recursive: true }), + mkdir(workspace, { recursive: true }), + mkdir(prefix, { recursive: true }), + ]); + await Promise.all([ + writeFile(tarball, "archive"), + writeFile( + join(packageRoot, "package.json"), + JSON.stringify({ name: "rea-agents", version: "6.0.0" }), + ), + writeFile( + join(packageRoot, "dist", "generatedPackageMetadata.js"), + 'export const generated = { version: "6.0.0" };\n', + ), + writeFile( + join(packageRoot, "scripts", "rea.mjs"), + [ + 'import { readFileSync } from "node:fs";', + 'const metadata = new URL("../package.json", import.meta.url);', + "console.log(JSON.parse(readFileSync(metadata, 'utf8')).version);", + "", + ].join("\n"), + ), + writeFile( + join(application, "Update.js"), + [ + "export async function runUpdate() {", + " const registry = process.env.npm_config_registry;", + " const response = await fetch(`${registry}/unlisted`, {", + " signal: AbortSignal.timeout(5000),", + " });", + " const body = await response.text();", + " throw new Error(`registry fixture answered ${response.status}: ${body}`);", + "}", + "", + ].join("\n"), + ), + writeFile( + join(application, "UpdateRuntime.js"), + "export const systemUpdateHost = () => ({});\n", + ), + ]); + const verifierUrl = pathToFileURL( + resolve("scripts/verify-package-update.mjs"), + ).href; + const harness = ` +globalThis.fetch = async () => ({ + status: 200, + headers: new Headers({ "content-type": "application/json" }), + arrayBuffer: async () => { + throw new Error(${JSON.stringify(injectedFailure)}); + }, +}); +const { verifyPackageUpdate } = await import(${JSON.stringify(verifierUrl)}); +let failure; +try { + await verifyPackageUpdate({ + prefix: ${JSON.stringify(prefix)}, + packageRoot: ${JSON.stringify(packageRoot)}, + tarball: ${JSON.stringify(tarball)}, + workspace: ${JSON.stringify(workspace)}, + environment: { PATH: process.env.PATH }, + }); +} catch (cause) { + failure = cause instanceof Error ? cause.message : String(cause); +} +process.stdout.write(JSON.stringify({ failure })); +`; + const { stdout } = await execFileAsync( + process.execPath, + ["--input-type=module", "--eval", harness], + { env: { ...process.env }, maxBuffer: 1_024 * 1_024, timeout: 30_000 }, + ); + + expect(JSON.parse(stdout)).toEqual({ failure: injectedFailure }); +}, 45_000);