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:
@@ -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,
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user