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:
@@ -126,17 +126,27 @@ export const get = query({
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = node.data as Record<string, unknown> | undefined;
|
||||
if (!data?.storageId) {
|
||||
return node;
|
||||
}
|
||||
const data = node.data as Record<string, unknown> | 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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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<string, string | undefined>;
|
||||
|
||||
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<Id<"_storage">>,
|
||||
): Promise<StorageUrlMap> {
|
||||
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<StorageUrlResult> => {
|
||||
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({
|
||||
|
||||
Reference in New Issue
Block a user