From 4f1c712f4fb43eaab05d2aab18c20d69b4b7f1b6 Mon Sep 17 00:00:00 2001 From: Matt Sears Date: Sat, 8 Aug 2026 10:13:04 -0400 Subject: [PATCH 1/3] fix(emulator): stringify send() errors before writing them to stderr EmulatorLog.flush() passed the Error object from process.send()'s callback straight to process.stderr.write(), which accepts only a string, Buffer, TypedArray, or DataView. Writing an Error makes write() throw synchronously, and it does so inside a callback with no surrounding try/catch -- an uncaught exception. The cost is diagnostic and it compounds: this path only runs when the IPC channel to the functions runtime has already failed, so the error that gets destroyed is the one explaining why the runtime became unreachable, and what operators see instead is a TypeError about stream chunk types pointing at Node's stream internals. Fixes #10876 --- CHANGELOG.md | 1 + src/emulator/types.spec.ts | 73 ++++++++++++++++++++++++++++++++++++++ src/emulator/types.ts | 7 ++-- 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 src/emulator/types.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0752cf767f3..014ceefabc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,3 +2,4 @@ - Added `appcheck:apps:list` to show every app with its configured App Check providers. - Added web app support for Crashlytics MCP tools and prompts. - Added support for forwarding custom HTTP headers (`Mcp-Param-*`) to remote MCP tools when defined in tool parameter input schemas (`x-mcp-header`), per [SEP-2243](https://modelcontextprotocol.io/seps/2243-http-standardization). +- Fixed the Functions emulator replacing an IPC failure with an unrelated `TypeError` about stream chunk types, hiding why the runtime became unreachable (#10876). diff --git a/src/emulator/types.spec.ts b/src/emulator/types.spec.ts new file mode 100644 index 00000000000..e2c2acf68d9 --- /dev/null +++ b/src/emulator/types.spec.ts @@ -0,0 +1,73 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { EmulatorLog } from "./types"; + +type SendCallback = (err: unknown) => void; + +describe("EmulatorLog", () => { + describe("flush()", () => { + let stderrWrite: sinon.SinonStub; + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalSend = process.send; + + beforeEach(() => { + // Enforce the same chunk types the real stream does, so a regression throws here too. + stderrWrite = sinon.stub(process.stderr, "write").callsFake((chunk: unknown): boolean => { + if (typeof chunk !== "string" && !ArrayBuffer.isView(chunk)) { + throw new TypeError( + `The "chunk" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received ${typeof chunk}`, + ); + } + return true; + }); + }); + + afterEach(() => { + sinon.restore(); + process.send = originalSend; + }); + + // Reports `err` to the send callback, the way Node does when the IPC channel has failed. + function stubSend(err: unknown): void { + process.send = (( + _message: unknown, + _sendHandle: unknown, + _options: unknown, + callback: SendCallback, + ): boolean => { + callback(err); + return true; + }) as unknown as typeof process.send; + } + + it("writes the stack of an Error reported by process.send()", () => { + stubSend(new Error("channel closed")); + + expect(() => new EmulatorLog("INFO", "system", "hello").log()).to.not.throw(); + + expect(stderrWrite.calledOnce).to.be.true; + const written = stderrWrite.firstCall.args[0] as string; + expect(written).to.be.a("string"); + expect(written).to.contain("channel closed"); + expect(written).to.match(/\n$/); + }); + + it("writes a non-Error reported by process.send()", () => { + stubSend("ERR_IPC_CHANNEL_CLOSED"); + + expect(() => new EmulatorLog("INFO", "system", "hello").log()).to.not.throw(); + + expect(stderrWrite.calledOnce).to.be.true; + expect(stderrWrite.firstCall.args[0]).to.contain("ERR_IPC_CHANNEL_CLOSED"); + }); + + it("writes nothing when process.send() succeeds", () => { + stubSend(null); + + new EmulatorLog("INFO", "system", "hello").log(); + + expect(stderrWrite.called).to.be.false; + }); + }); +}); diff --git a/src/emulator/types.ts b/src/emulator/types.ts index 601ce0d6e43..9964ab34bc2 100644 --- a/src/emulator/types.ts +++ b/src/emulator/types.ts @@ -1,6 +1,8 @@ import { ChildProcess } from "child_process"; import { EventEmitter } from "events"; +import { getErrStack } from "../error"; + export enum Emulators { AUTH = "auth", HUB = "hub", @@ -360,9 +362,10 @@ export class EmulatorLog { // For some reason our node.d.ts file does not include the version of subprocess.send() with a callback // but the node docs assert that it has an optional callback. // https://nodejs.org/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback - (process.send as any)(nextMsg, undefined, {}, (err: any) => { + (process.send as any)(nextMsg, undefined, {}, (err: unknown) => { if (err) { - process.stderr.write(err); + // err is an Error, which stream.write() rejects by throwing. + process.stderr.write(`${getErrStack(err)}\n`); } EmulatorLog.WAITING_FOR_FLUSH = EmulatorLog.LOG_BUFFER.length > 0; From 8b6112b4126ead90cac0abb676ce990bb23d8895 Mon Sep 17 00:00:00 2001 From: Matt Sears Date: Sat, 8 Aug 2026 10:29:26 -0400 Subject: [PATCH 2/3] Update src/emulator/types.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/emulator/types.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/emulator/types.ts b/src/emulator/types.ts index 9964ab34bc2..140b2137091 100644 --- a/src/emulator/types.ts +++ b/src/emulator/types.ts @@ -361,11 +361,13 @@ export class EmulatorLog { if (process.send) { // For some reason our node.d.ts file does not include the version of subprocess.send() with a callback // but the node docs assert that it has an optional callback. - // https://nodejs.org/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback - (process.send as any)(nextMsg, undefined, {}, (err: unknown) => { if (err) { - // err is an Error, which stream.write() rejects by throwing. + // process.send() hands the callback an Error object, which stream.write() + // rejects -- writing it directly throws and destroys the original error. process.stderr.write(`${getErrStack(err)}\n`); + // Clear the buffer to prevent flooding stderr with duplicate stack traces + // for subsequent messages when the IPC channel is permanently broken. + EmulatorLog.LOG_BUFFER = []; } EmulatorLog.WAITING_FOR_FLUSH = EmulatorLog.LOG_BUFFER.length > 0; From c88751760413db291d34fb88050d05b3bd50522e Mon Sep 17 00:00:00 2001 From: Matt Sears Date: Sat, 8 Aug 2026 10:47:40 -0400 Subject: [PATCH 3/3] fix(emulator): restore the send() call and drop the unneeded any cast Applying the buffer-clearing suggestion removed the process.send() call line itself along with its callback, leaving the if (err) body and its closing brace behind, so the file no longer compiled. Restore the call without the `as any` cast. @types/node declares send(message, sendHandle?, options?, callback?) with the callback overload, so the cast is unnecessary and the comment claiming otherwise was stale; typing the callback parameter Error | null typechecks cleanly. The node docs link is kept as the reference for that signature. --- src/emulator/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emulator/types.ts b/src/emulator/types.ts index 140b2137091..65760acd130 100644 --- a/src/emulator/types.ts +++ b/src/emulator/types.ts @@ -359,8 +359,8 @@ export class EmulatorLog { EmulatorLog.WAITING_FOR_FLUSH = true; if (process.send) { - // For some reason our node.d.ts file does not include the version of subprocess.send() with a callback - // but the node docs assert that it has an optional callback. + // https://nodejs.org/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback + process.send(nextMsg, undefined, {}, (err: Error | null) => { if (err) { // process.send() hands the callback an Error object, which stream.write() // rejects -- writing it directly throws and destroys the original error.