From b1cb9093bbeb51cdb8db601a5956a67026e4612e Mon Sep 17 00:00:00 2001 From: tdurieux Date: Sun, 6 Sep 2026 09:19:18 +0200 Subject: [PATCH] fix: close streaming responses after upstream errors --- src/server/routes/webview.ts | 4 ++-- src/streamer/route.ts | 3 +++ test/production-regressions.test.js | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/server/routes/webview.ts b/src/server/routes/webview.ts index d89db2c..1385b62 100644 --- a/src/server/routes/webview.ts +++ b/src/server/routes/webview.ts @@ -172,10 +172,10 @@ async function webView(req: express.Request, res: express.Response) { const html = `Content
${body}
`; res.contentType("text/html").send(html); } catch { - f.send(res); + await f.send(res); } } else { - f.send(res); + await f.send(res); } } catch (error) { handleError(error, res, req); diff --git a/src/streamer/route.ts b/src/streamer/route.ts index 49cb1ce..b1238e5 100644 --- a/src/streamer/route.ts +++ b/src/streamer/route.ts @@ -39,6 +39,7 @@ router.post( ); } catch (error) { handleError(error, res); + if (res.headersSent && !res.writableEnded) res.destroy(); } } ); @@ -111,6 +112,8 @@ router.post("/", async (req: express.Request, res: express.Response) => { content.destroy(); } handleError(error, res); + if (res.headersSent && !res.writableEnded) res.destroy(); + anonymizer.destroy(); } content .on("error", handleStreamError) diff --git a/test/production-regressions.test.js b/test/production-regressions.test.js index f7d9800..0cd8cc8 100644 --- a/test/production-regressions.test.js +++ b/test/production-regressions.test.js @@ -232,6 +232,21 @@ describe("production regressions", function () { expect(res.headers["Content-Security-Policy"]).to.include("sandbox"); expect(res.headers["Content-Security-Policy"]).not.to.include("allow-same-origin"); }); + it("terminates the streamer response after a late upstream error", async function () { + const input = new (require("stream").PassThrough)(); + stub(GitHubStream.prototype, "getFileContentCache", async () => input); + stub(require("../src/server/routes/route-utils"), "handleError", () => {}); + const res = new (require("stream").PassThrough)(); + res.header = res.contentType = () => res; + res.headersSent = true; + res.resume(); + const handler = require("../src/streamer/route").default.stack.find(x => x.route.path === "/").route.stack[0].handle; + await handler({ body: { token: "token", repoFullName: "owner/repo", repoId: "repo", filePath: "file.txt", anonymizerOptions: { filePath: "file.txt", image: true, link: true } } }, res); + input.write("partial data"); + input.destroy(new Error("upstream broke")); + await new Promise(resolve => setImmediate(resolve)); + expect(res.destroyed).to.equal(true); + }); it("keeps a conference eligible for retry when repository expiration fails", async function () { const Conference = require("../src/core/Conference").default; const model = { status: "ready", save: async () => {} };