From 7a163f2d3501e370d510a7e126947b51c5f748a9 Mon Sep 17 00:00:00 2001 From: tdurieux Date: Thu, 7 May 2026 07:44:15 +0300 Subject: [PATCH] Fix streamer crash and misclassified transient GitHub errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add missing error handler on the anonymizer transform stream in the streamer route — without it, an upstream error tears down the pipe and the anonymizer emits an unhandled error that crashes the process (surfacing as ECONNRESET to the main server). Classify transient network errors (ReadError, ECONNRESET, ETIMEDOUT) as upstream_error/502 instead of file_not_found/404 so they are distinguishable in logs and don't cache-poison downstream. Update handleError tests to match the existing sanitization behavior that returns internal_error for non-AnonymousError instances. --- src/core/source/GitHubStream.ts | 11 ++++++++++- src/streamer/route.ts | 1 + test/route-utils-handleError.test.js | 10 +++++----- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/core/source/GitHubStream.ts b/src/core/source/GitHubStream.ts index 4a877d2..efc5cf6 100644 --- a/src/core/source/GitHubStream.ts +++ b/src/core/source/GitHubStream.ts @@ -291,14 +291,23 @@ export default class GitHubStream extends GitHubBase { ?.statusCode ?? (error as { status?: number })?.status ?? (error as { httpStatus?: number })?.httpStatus; + const errCode = (error as { code?: string })?.code; + const isTransient = + !httpStatus && + (errCode === "ECONNRESET" || + errCode === "ETIMEDOUT" || + errCode === "ERR_BODY_PARSE_FAILURE" || + error.name === "ReadError"); const code = httpStatus === 422 ? "file_too_big" : httpStatus === 403 ? "file_not_accessible" + : isTransient + ? "upstream_error" : "file_not_found"; const wrapped = new AnonymousError(code, { - httpStatus, + httpStatus: isTransient ? 502 : httpStatus, cause: error as Error, object: filePath, }); diff --git a/src/streamer/route.ts b/src/streamer/route.ts index ea9dfcc..1c3faec 100644 --- a/src/streamer/route.ts +++ b/src/streamer/route.ts @@ -108,6 +108,7 @@ router.post("/", async (req: express.Request, res: express.Response) => { content .on("error", handleStreamError) .pipe(anonymizer) + .on("error", handleStreamError) .pipe(res) .on("error", handleStreamError) .on("close", () => { diff --git a/test/route-utils-handleError.test.js b/test/route-utils-handleError.test.js index 160dbfe..895d615 100644 --- a/test/route-utils-handleError.test.js +++ b/test/route-utils-handleError.test.js @@ -53,14 +53,14 @@ describe("route-utils.handleError", function () { }); handleError(err, res); expect(res.statusCode).to.equal(503); - expect(res.body).to.deep.equal({ error: "S3 down" }); + expect(res.body).to.deep.equal({ error: "internal_error" }); }); it("maps messages containing 'not_found' to 404", function () { const res = makeRes(); handleError(new Error("repo_not_found"), res); expect(res.statusCode).to.equal(404); - expect(res.body).to.deep.equal({ error: "repo_not_found" }); + expect(res.body).to.deep.equal({ error: "internal_error" }); }); it("maps messages containing '(Not Found)' (got HTTPError style) to 404", function () { @@ -73,21 +73,21 @@ describe("route-utils.handleError", function () { const res = makeRes(); handleError(new Error("user_not_connected"), res); expect(res.statusCode).to.equal(401); - expect(res.body).to.deep.equal({ error: "user_not_connected" }); + expect(res.body).to.deep.equal({ error: "internal_error" }); }); it("defaults to 500 when nothing matches", function () { const res = makeRes(); handleError(new Error("kaboom"), res); expect(res.statusCode).to.equal(500); - expect(res.body).to.deep.equal({ error: "kaboom" }); + expect(res.body).to.deep.equal({ error: "internal_error" }); }); it("accepts a string error and stringifies it in the body", function () { const res = makeRes(); handleError("something_bad", res); expect(res.statusCode).to.equal(500); - expect(res.body).to.deep.equal({ error: "something_bad" }); + expect(res.body).to.deep.equal({ error: "internal_error" }); }); it("does not call res when headersSent is true", function () {