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..65760acd130 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", @@ -357,12 +359,15 @@ 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 as any)(nextMsg, undefined, {}, (err: any) => { + process.send(nextMsg, undefined, {}, (err: Error | null) => { if (err) { - process.stderr.write(err); + // 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;