Skip to content
Merged
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
26 changes: 26 additions & 0 deletions packages/provider-cursor/src/server/CursorSdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,37 @@ describe("CursorSdk unhandled rejection guard", () => {
}),
);

it.effect("keeps the server running when the SDK writes after its request stream ended", () =>
Effect.gen(function* () {
// Shaped like `ConnectError.from(e, Code.Aborted)` from the SDK's connect-node body pump.
const cause = Object.assign(new Error("write after end"), {
code: "ERR_STREAM_WRITE_AFTER_END",
});
const reason = Object.assign(new Error("[aborted] write after end", { cause }), {
name: "ConnectError",
code: 10,
rawMessage: "write after end",
});
assert.deepStrictEqual(yield* rejectThroughGuard(reason), []);
}),
);

it.effect("still exits on other rejections", () =>
Effect.gen(function* () {
assert.deepStrictEqual(yield* rejectThroughGuard(new Error("WritableIterable is closed")), [
[1],
]);
}),
);

it.effect("still exits on aborted requests other than a write after end", () =>
Effect.gen(function* () {
const cause = Object.assign(new Error("socket hang up"), { code: "ECONNRESET" });
const reason = Object.assign(new Error("[aborted] socket hang up", { cause }), {
name: "ConnectError",
code: 10,
});
assert.deepStrictEqual(yield* rejectThroughGuard(reason), [[1]]);
}),
);
});
21 changes: 16 additions & 5 deletions packages/provider-cursor/src/server/CursorSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,23 @@ function isCursorShellSpawnFailure(reason: unknown): boolean {
}

/**
* After a local exec fails, the SDK writes a final frame to the exec's stream from
* a promise nothing awaits. If that stream has already closed, the write rejects.
* The SDK's class is minified, so only its `name` identifies it.
* The SDK writes final frames from promises nothing awaits. A closed stream rejects them
* with WriteIterableClosedError; an ended request body rejects them with the Aborted
* ConnectError its connect-node pump wraps around ERR_STREAM_WRITE_AFTER_END. The classes
* are minified, so only `name` identifies them.
*/
function isCursorClosedStreamWrite(reason: unknown): boolean {
return reason instanceof Error && reason.name === "WriteIterableClosedError";
if (!(reason instanceof Error)) {
return false;
}
if (reason.name === "WriteIterableClosedError") {
return true;
}
return (
reason.name === "ConnectError" &&
reason.cause instanceof Error &&
Reflect.get(reason.cause, "code") === "ERR_STREAM_WRITE_AFTER_END"
);
}

function onUnhandledRejection(reason: unknown): void {
Expand All @@ -46,7 +57,7 @@ function onUnhandledRejection(reason: unknown): void {
return;
}
if (isCursorClosedStreamWrite(reason)) {
console.error("Cursor SDK wrote to a closed exec stream. The server will keep running.");
console.error("Cursor SDK wrote to a closed stream. The server will keep running.");
return;
}
if (process.listenerCount("unhandledRejection") > 1) {
Expand Down
Loading