fix(acp): catch unhandled rejection from handleGatewayEvent in event callback (#100558)

* fix(acp): catch unhandled rejection from handleGatewayEvent in event callback

* fix(acp): catch unhandled rejection from handleGatewayEvent

* test(acp): add regression test for handleGatewayEvent error boundary

Verifies that when handleChatEvent throws, handleGatewayEvent catches the
error, logs it via this.log(), and does not reject.

* chore: remove committed PR body artifact

* fix(acp): contain rejected gateway events

* chore: defer ACP release note

* refactor(acp): simplify gateway rejection boundary

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Zechen Wang
2026-07-06 23:47:48 +08:00
committed by GitHub
parent b82e3c1481
commit 1de014bc7f
2 changed files with 59 additions and 2 deletions
+51 -1
View File
@@ -2,6 +2,7 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
type GatewayClientCallbacks = {
onEvent?: (evt: { event: string; payload?: unknown }) => void;
onHelloOk?: () => void;
onConnectError?: (err: Error) => void;
onClose?: (code: number, reason: string) => void;
@@ -33,6 +34,7 @@ const mockState = vi.hoisted(() => ({
gatewayAuth: [] as GatewayClientAuth[],
gatewayOptions: [] as GatewayClientOptions[],
agentSideConnectionCtor: vi.fn(),
agentHandleGatewayEvent: vi.fn(async (_evt: unknown) => {}),
agentStart: vi.fn(),
routeLogsToStderr: vi.fn(),
startProxy: vi.fn(async (_configForTest: unknown) => null as unknown),
@@ -70,6 +72,10 @@ class MockGatewayClient {
emitConnectError(message: string): void {
this.callbacks.onConnectError?.(new Error(message));
}
emitEvent(event: { event: string; payload?: unknown }): void {
this.callbacks.onEvent?.(event);
}
}
vi.mock("@agentclientprotocol/sdk", () => ({
@@ -171,7 +177,9 @@ vi.mock("./translator.js", () => ({
handleGatewayDisconnect(): void {}
async handleGatewayEvent(): Promise<void> {}
async handleGatewayEvent(event: unknown): Promise<void> {
await mockState.agentHandleGatewayEvent(event);
}
},
}));
@@ -283,6 +291,7 @@ describe("serveAcpGateway startup", () => {
mockState.gatewayAuth.length = 0;
mockState.gatewayOptions.length = 0;
mockState.agentSideConnectionCtor.mockReset();
mockState.agentHandleGatewayEvent.mockReset();
mockState.agentStart.mockReset();
mockState.routeLogsToStderr.mockReset();
mockState.startProxy.mockReset();
@@ -330,6 +339,47 @@ describe("serveAcpGateway startup", () => {
}
});
it.each([
{
name: "default logging",
opts: {},
expected: ["openclaw acp: gateway event chat failed\n"],
},
{
name: "verbose logging",
opts: { verbose: true },
expected: [
"openclaw acp: gateway event chat failed\n",
"openclaw acp: gateway event chat error: Error: handler boom\n",
],
},
])("contains rejected gateway event handling with $name", async ({ opts, expected }) => {
const { signalHandlers, onceSpy } = captureProcessSignalHandlers();
const writes: string[] = [];
const writeSpy = vi
.spyOn(process.stderr, "write")
.mockImplementation((chunk: string | Uint8Array): boolean => {
writes.push(String(chunk));
return true;
});
mockState.agentHandleGatewayEvent.mockRejectedValueOnce(new Error("handler boom"));
try {
const servePromise = serveAcpGateway(opts);
await emitHelloAndWaitForAgentSideConnection();
getMockGateway().emitEvent({ event: "chat" });
await vi.waitFor(() => {
expect(writes).toEqual(expected);
});
await stopServeWithSigint(signalHandlers, servePromise);
} finally {
writeSpy.mockRestore();
onceSpy.mockRestore();
}
});
it("routes logs to stderr before loading gateway config", async () => {
const { signalHandlers, onceSpy } = captureProcessSignalHandlers();
+8 -1
View File
@@ -91,7 +91,14 @@ export async function serveAcpGateway(opts: AcpServerOptions = {}): Promise<void
mode: GATEWAY_CLIENT_MODES.CLI,
caps: [GATEWAY_CLIENT_CAPS.TOOL_EVENTS],
onEvent: (evt) => {
void agent?.handleGatewayEvent(evt);
// Gateway delivery stays non-blocking, but translator failures must not
// escape this callback as unhandled process rejections.
void agent?.handleGatewayEvent(evt).catch((err: unknown) => {
process.stderr.write(`openclaw acp: gateway event ${evt.event} failed\n`);
if (opts.verbose) {
process.stderr.write(`openclaw acp: gateway event ${evt.event} error: ${String(err)}\n`);
}
});
},
onHelloOk: () => {
resolveGatewayReady();