Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
73 changes: 73 additions & 0 deletions src/emulator/types.spec.ts
Original file line number Diff line number Diff line change
@@ -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;
});
});
});
13 changes: 9 additions & 4 deletions src/emulator/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ChildProcess } from "child_process";
import { EventEmitter } from "events";

import { getErrStack } from "../error";

export enum Emulators {
AUTH = "auth",
HUB = "hub",
Expand Down Expand Up @@ -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 = [];
}
Comment thread
mattsears18 marked this conversation as resolved.

EmulatorLog.WAITING_FOR_FLUSH = EmulatorLog.LOG_BUFFER.length > 0;
Expand Down