From 2142249ed558e362b405567236f290e98e0374d0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Apr 2026 20:41:05 +0200 Subject: [PATCH] Handle storage URL resolution failures gracefully - Batch storage URL lookups to reduce request spikes - Log and skip failed URL resolutions instead of throwing - Preserve node reads when attached storage is unavailable --- convex/nodes.ts | 28 ++++++++++++++++-------- convex/storage.ts | 54 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/convex/nodes.ts b/convex/nodes.ts index 4828b7d..b46a2d0 100644 --- a/convex/nodes.ts +++ b/convex/nodes.ts @@ -126,17 +126,27 @@ export const get = query({ return null; } - const data = node.data as Record | undefined; - if (!data?.storageId) { - return node; - } + const data = node.data as Record | undefined; + if (!data?.storageId) { + return node; + } - const url = await ctx.storage.getUrl(data.storageId as Id<"_storage">); + let url: string | null; + try { + url = await ctx.storage.getUrl(data.storageId as Id<"_storage">); + } catch (error) { + console.warn("[nodes.get] failed to resolve storage URL", { + nodeId: node._id, + storageId: data.storageId, + error: String(error), + }); + return node; + } - return { - ...node, - data: { - ...data, + return { + ...node, + data: { + ...data, url: url ?? undefined, }, }; diff --git a/convex/storage.ts b/convex/storage.ts index c0483ef..e5d2815 100644 --- a/convex/storage.ts +++ b/convex/storage.ts @@ -3,8 +3,22 @@ import { v } from "convex/values"; import { requireAuth } from "./helpers"; import type { Id } from "./_generated/dataModel"; +const STORAGE_URL_BATCH_SIZE = 12; + type StorageUrlMap = Record; +type StorageUrlResult = + | { + storageId: Id<"_storage">; + url: string | undefined; + error: null; + } + | { + storageId: Id<"_storage">; + url: null; + error: string; + }; + async function assertCanvasOwner( ctx: QueryCtx, canvasId: Id<"canvases">, @@ -43,13 +57,41 @@ async function resolveStorageUrls( ctx: QueryCtx, storageIds: Array>, ): Promise { - const entries = await Promise.all( - storageIds.map( - async (id) => [id, (await ctx.storage.getUrl(id)) ?? undefined] as const, - ), - ); + const resolved: StorageUrlMap = {}; - return Object.fromEntries(entries) as StorageUrlMap; + for (let i = 0; i < storageIds.length; i += STORAGE_URL_BATCH_SIZE) { + const batch = storageIds.slice(i, i + STORAGE_URL_BATCH_SIZE); + + const entries = await Promise.all( + batch.map(async (id): Promise => { + try { + const url = await ctx.storage.getUrl(id); + return { storageId: id, url: url ?? undefined, error: null }; + } catch (error) { + return { + storageId: id, + url: null, + error: String(error), + }; + } + }), + ); + + for (const entry of entries) { + if (entry.error) { + console.warn("[storage.batchGetUrlsForCanvas] getUrl failed", { + storageId: entry.storageId, + error: entry.error, + }); + continue; + } + + const { storageId, url } = entry; + resolved[storageId] = url; + } + } + + return resolved; } export const generateUploadUrl = mutation({