mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-20 22:02:19 +03:00
fix(providers): stop devin-cli spawn error from double-closing SSE controller (#12517)
child.on("error") was manually calling controller.close() without
setting the finished flag, so the subsequent child.on("close") event
for the same failed spawn saw finished=false and called finish() ->
emit() -> controller.enqueue() on an already-closed controller,
throwing an uncaughtException. Route the error handler through the
existing finish(msg) helper, which already guards on finished, so the
error still reaches the client as a sanitized SSE error event exactly
once.
This commit is contained in:
1
changelog.d/fixes/12517-devin-cli-sse-double-close.md
Normal file
1
changelog.d/fixes/12517-devin-cli-sse-double-close.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): stop devin-cli spawn error from double-closing the SSE controller (#12517)
|
||||
@@ -170,11 +170,7 @@ export class DevinCliExecutor extends BaseExecutor {
|
||||
err.message.includes("ENOENT") || err.message.includes("not found")
|
||||
? `Devin CLI not found: ${devinBin}. Install via https://cli.devin.ai or set CLI_DEVIN_BIN env var.`
|
||||
: `Devin CLI spawn error: ${err.message}`;
|
||||
emit(
|
||||
`data: ${JSON.stringify({ error: { message: msg, type: "devin_cli_error", code: "spawn_failed" } })}\n\n`
|
||||
);
|
||||
emit("data: [DONE]\n\n");
|
||||
controller.close();
|
||||
finish(msg);
|
||||
});
|
||||
|
||||
if (signal) {
|
||||
|
||||
55
tests/unit/executor-devin-cli-12517-double-close.test.ts
Normal file
55
tests/unit/executor-devin-cli-12517-double-close.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, it, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const mod = await import("../../open-sse/executors/devin-cli.ts");
|
||||
|
||||
describe("DevinCliExecutor — #12517 spawn error must not double-close SSE controller", () => {
|
||||
it("surfaces a sanitized SSE error and never fires uncaughtException on ENOENT spawn", async () => {
|
||||
const previousBin = process.env.CLI_DEVIN_BIN;
|
||||
process.env.CLI_DEVIN_BIN = "/nonexistent/absolute/path/to/devin-bin-12517";
|
||||
|
||||
let caught: unknown = null;
|
||||
const onUncaught = (err: unknown) => {
|
||||
caught = err;
|
||||
};
|
||||
process.on("uncaughtException", onUncaught);
|
||||
|
||||
try {
|
||||
const executor = new mod.DevinCliExecutor();
|
||||
const { response } = await executor.execute({
|
||||
model: "devin-cli",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: true,
|
||||
credentials: {},
|
||||
signal: undefined,
|
||||
log: undefined,
|
||||
} as never);
|
||||
|
||||
const reader = response.body!.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let payload = "";
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
payload += decoder.decode(value);
|
||||
}
|
||||
|
||||
assert.match(payload, /Devin CLI not found/);
|
||||
assert.match(payload, /data: \[DONE\]/);
|
||||
|
||||
// Give the child's async "close" event (which fires after "error" for a
|
||||
// failed spawn) room to run before asserting no uncaughtException fired.
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
assert.equal(caught, null, `expected no uncaughtException, got: ${String(caught)}`);
|
||||
} finally {
|
||||
process.removeListener("uncaughtException", onUncaught);
|
||||
if (previousBin === undefined) delete process.env.CLI_DEVIN_BIN;
|
||||
else process.env.CLI_DEVIN_BIN = previousBin;
|
||||
}
|
||||
});
|
||||
|
||||
after(() => {
|
||||
delete process.env.CLI_DEVIN_BIN;
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user