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
This commit is contained in:
Matthias
2026-04-01 20:41:05 +02:00
parent 3926940c5a
commit 2142249ed5
2 changed files with 67 additions and 15 deletions

View File

@@ -131,7 +131,17 @@ export const get = query({
return node; 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 { return {
...node, ...node,

View File

@@ -3,8 +3,22 @@ import { v } from "convex/values";
import { requireAuth } from "./helpers"; import { requireAuth } from "./helpers";
import type { Id } from "./_generated/dataModel"; import type { Id } from "./_generated/dataModel";
const STORAGE_URL_BATCH_SIZE = 12;
type StorageUrlMap = Record<string, string | undefined>; type StorageUrlMap = Record<string, string | undefined>;
type StorageUrlResult =
| {
storageId: Id<"_storage">;
url: string | undefined;
error: null;
}
| {
storageId: Id<"_storage">;
url: null;
error: string;
};
async function assertCanvasOwner( async function assertCanvasOwner(
ctx: QueryCtx, ctx: QueryCtx,
canvasId: Id<"canvases">, canvasId: Id<"canvases">,
@@ -43,13 +57,41 @@ async function resolveStorageUrls(
ctx: QueryCtx, ctx: QueryCtx,
storageIds: Array<Id<"_storage">>, storageIds: Array<Id<"_storage">>,
): Promise<StorageUrlMap> { ): Promise<StorageUrlMap> {
const resolved: 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( const entries = await Promise.all(
storageIds.map( batch.map(async (id): Promise<StorageUrlResult> => {
async (id) => [id, (await ctx.storage.getUrl(id)) ?? undefined] as const, 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),
};
}
}),
); );
return Object.fromEntries(entries) as StorageUrlMap; 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({ export const generateUploadUrl = mutation({