From 37a2b754fe1a58522af5c0ac4ab7f7d42c0d7efd Mon Sep 17 00:00:00 2001 From: Dominic Byrd-McDevitt Date: Wed, 15 Apr 2026 10:35:44 -0400 Subject: [PATCH 1/4] fix: release upstream response body before sending error on status/header failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Body cancellation on okStatus and okHeaders failures is fire-and-forget — no need to await before calling sendError, which avoids adding latency to the error response path while still releasing the connection. Co-Authored-By: Claude Sonnet 4.6 --- src/ThumbnailApi.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ThumbnailApi.ts b/src/ThumbnailApi.ts index 370dd14..fd04620 100644 --- a/src/ThumbnailApi.ts +++ b/src/ThumbnailApi.ts @@ -98,6 +98,7 @@ export class ThumbnailApi { if (!this.responseHelper.okStatus(status)) { const error = new Error(`Status ${String(status)} from upstream.`); + remoteImageResponse.body?.cancel?.().catch(() => {}); this.sendError(expressResponse, itemId, 404, error); return; } @@ -106,6 +107,7 @@ export class ThumbnailApi { if (!this.responseHelper.okHeaders(remoteImageResponse.headers)) { const error = new Error(`Got bad headers from upstream.`); + remoteImageResponse.body?.cancel?.().catch(() => {}); this.sendError(expressResponse, itemId, 404, error); return; } From 535ebc833ee3a5902db1e3ba53107fb456ceb80b Mon Sep 17 00:00:00 2001 From: Dominic Byrd-McDevitt Date: Wed, 15 Apr 2026 20:20:19 -0400 Subject: [PATCH 2/4] refactor: extract releaseUpstreamBody helper to remove duplication Co-Authored-By: Claude Sonnet 4.5 --- src/ThumbnailApi.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ThumbnailApi.ts b/src/ThumbnailApi.ts index fd04620..b921629 100644 --- a/src/ThumbnailApi.ts +++ b/src/ThumbnailApi.ts @@ -98,7 +98,7 @@ export class ThumbnailApi { if (!this.responseHelper.okStatus(status)) { const error = new Error(`Status ${String(status)} from upstream.`); - remoteImageResponse.body?.cancel?.().catch(() => {}); + this.releaseUpstreamBody(remoteImageResponse); this.sendError(expressResponse, itemId, 404, error); return; } @@ -107,7 +107,7 @@ export class ThumbnailApi { if (!this.responseHelper.okHeaders(remoteImageResponse.headers)) { const error = new Error(`Got bad headers from upstream.`); - remoteImageResponse.body?.cancel?.().catch(() => {}); + this.releaseUpstreamBody(remoteImageResponse); this.sendError(expressResponse, itemId, 404, error); return; } @@ -171,6 +171,10 @@ export class ThumbnailApi { } } + private releaseUpstreamBody(response: Response): void { + void response.body?.cancel?.().catch(() => {}); + } + sendError( res: express.Response, itemId: string, From 8db4551b4655f303cbb2c1d9ed1d2f24a1d51328 Mon Sep 17 00:00:00 2001 From: Dominic Byrd-McDevitt Date: Wed, 15 Apr 2026 21:34:28 -0400 Subject: [PATCH 3/4] Use optional chaining on .catch() in releaseUpstreamBody to prevent TypeError body?.cancel?.() returns undefined when body is null (e.g. a no-body upstream response), and calling .catch() on undefined throws TypeError. Using ?.catch() safely short-circuits to undefined in that case while still suppressing Promise rejections when cancel returns a Promise. Co-Authored-By: Claude Sonnet 4.6 --- src/ThumbnailApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ThumbnailApi.ts b/src/ThumbnailApi.ts index b921629..a1f8a65 100644 --- a/src/ThumbnailApi.ts +++ b/src/ThumbnailApi.ts @@ -172,7 +172,7 @@ export class ThumbnailApi { } private releaseUpstreamBody(response: Response): void { - void response.body?.cancel?.().catch(() => {}); + void response.body?.cancel?.()?.catch(() => {}); } sendError( From f31d6e9effdfc818cecbd32e70d3727fd7cf1a75 Mon Sep 17 00:00:00 2001 From: Dominic Byrd-McDevitt Date: Wed, 15 Apr 2026 23:11:10 -0400 Subject: [PATCH 4/4] fix: release upstream body in serveItemFromS3 on non-OK status Apply the same releaseUpstreamBody call to the serveItemFromS3 error path that was already added to proxyItemFromContributor, so connections are returned to the pool on S3 upstream failures too. Co-Authored-By: Claude Sonnet 4.6 --- src/ThumbnailApi.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ThumbnailApi.ts b/src/ThumbnailApi.ts index a1f8a65..35c2404 100644 --- a/src/ThumbnailApi.ts +++ b/src/ThumbnailApi.ts @@ -141,6 +141,7 @@ export class ThumbnailApi { if (!this.responseHelper.okStatus(status)) { const error = new Error(`Status ${String(status)} from upstream.`); + this.releaseUpstreamBody(response); this.sendError(expressResponse, itemId, 404, error); return; }