From 3e464a76f2d1e5985f259270a4258f3db4f17c18 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 11 Jun 2026 11:50:49 +0200 Subject: [PATCH] fix(image-proxy): use ArrayBuffer instead of Blob to prevent OOM on Node v24 (#5926) --- packages/image-proxy/src/index.ts | 23 ++++++++++++++--------- packages/redis/src/lib/channel.ts | 6 +++++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/image-proxy/src/index.ts b/packages/image-proxy/src/index.ts index 3660064c3..900509d38 100644 --- a/packages/image-proxy/src/index.ts +++ b/packages/image-proxy/src/index.ts @@ -10,6 +10,8 @@ import { createGetSetChannel } from "@homarr/redis"; const logger = createLogger({ module: "imageProxy" }); +export const IMAGE_PROXY_REDIS_TTL_SECONDS = 86400; + const createHmacChannel = (hmac: `${string}.${string}`) => createGetSetChannel(`image-proxy:hmac:${hmac}`); const createUrlByIdChannel = (id: string) => createGetSetChannel<{ @@ -45,7 +47,7 @@ export class ImageProxy { return this.createImageUrl(id); } - public async forwardImageAsync(id: string): Promise { + public async forwardImageAsync(id: string): Promise { const urlAndHeaders = await this.getImageUrlAndHeadersAsync(id); if (!urlAndHeaders) { return null; @@ -69,16 +71,16 @@ export class ImageProxy { return null; } - const blob = (await response.blob()) as Blob; + const arrayBuffer = await response.arrayBuffer(); logger.debug("Forwarding image succeeded", { id, url: this.redactUrl(urlAndHeaders.url), headers: this.redactHeaders(urlAndHeaders.headers), proxyUrl, - size: `${(blob.size / 1024).toFixed(1)}KB`, + size: `${(arrayBuffer.byteLength / 1024).toFixed(1)}KB`, }); - return blob; + return arrayBuffer; } private createImageUrl(id: string): string { @@ -113,11 +115,14 @@ export class ImageProxy { const hashChannel = createHmacChannel(`${urlHash}.${headerHash}`); const urlHeaderChannel = createUrlByIdChannel(id); - await urlHeaderChannel.setAsync({ - url: encryptSecret(url), - headers: encryptSecret(JSON.stringify(headers ?? null)), - }); - await hashChannel.setAsync(id); + await urlHeaderChannel.setAsync( + { + url: encryptSecret(url), + headers: encryptSecret(JSON.stringify(headers ?? null)), + }, + { ttlSeconds: IMAGE_PROXY_REDIS_TTL_SECONDS }, + ); + await hashChannel.setAsync(id, { ttlSeconds: IMAGE_PROXY_REDIS_TTL_SECONDS }); logger.debug("Stored image in the proxy", { id, diff --git a/packages/redis/src/lib/channel.ts b/packages/redis/src/lib/channel.ts index 03025565e..9f2b04a98 100644 --- a/packages/redis/src/lib/channel.ts +++ b/packages/redis/src/lib/channel.ts @@ -112,9 +112,13 @@ export const createGetSetChannel = (name: string) => { /** * Set data in the channel * @param data data to be stored in the channel + * @param options optional TTL in seconds */ - setAsync: async (data: TData) => { + setAsync: async (data: TData, options?: { ttlSeconds?: number }) => { await getSetClient.set(name, superjson.stringify(data)); + if (options?.ttlSeconds) { + await getSetClient.expire(name, options.ttlSeconds); + } }, /** * Remove data from the channel