From 69984faa5843a7dfe5e1838410b43f476840daf8 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Thu, 22 Jan 2026 07:28:36 +1300 Subject: [PATCH] fix: use presigned URLs for fetching large community workflows Update the community workflow API to fetch presigned URLs from node-banana-pro, then download workflows directly from R2. This avoids timeouts when fetching large workflow files (275MB+). Co-Authored-By: Claude Opus 4.5 --- src/app/api/community-workflows/[id]/route.ts | 67 +++++++++++++++---- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/src/app/api/community-workflows/[id]/route.ts b/src/app/api/community-workflows/[id]/route.ts index 3963a4e3..6c05a1e4 100644 --- a/src/app/api/community-workflows/[id]/route.ts +++ b/src/app/api/community-workflows/[id]/route.ts @@ -13,7 +13,8 @@ interface RouteParams { * GET: Load a specific community workflow by ID from the remote API * * This proxies to the node-banana-pro hosted service which stores - * community workflows in R2 storage. + * community workflows in R2 storage. The API returns a presigned URL + * which we use to fetch the actual workflow data directly from R2. */ export async function GET(request: Request, { params }: RouteParams) { const controller = new AbortController(); @@ -22,22 +23,22 @@ export async function GET(request: Request, { params }: RouteParams) { try { const { id } = await params; - const response = await fetch( + // Step 1: Get the presigned URL from the API + const urlResponse = await fetch( `${COMMUNITY_WORKFLOWS_API_URL}/${encodeURIComponent(id)}`, { headers: { Accept: "application/json", }, signal: controller.signal, - // Cache for 10 minutes (individual workflows change less frequently) - next: { revalidate: 600 }, + // Short cache since presigned URLs expire + next: { revalidate: 60 }, } ); - clearTimeout(timeoutId); - - if (!response.ok) { - if (response.status === 404) { + if (!urlResponse.ok) { + clearTimeout(timeoutId); + if (urlResponse.status === 404) { return NextResponse.json( { success: false, @@ -48,22 +49,60 @@ export async function GET(request: Request, { params }: RouteParams) { } console.error( - "Error fetching community workflow:", - response.status, - response.statusText + "Error fetching community workflow URL:", + urlResponse.status, + urlResponse.statusText ); return NextResponse.json( { success: false, error: "Failed to load workflow", }, - { status: response.status } + { status: urlResponse.status } + ); + } + + const urlData = await urlResponse.json(); + + if (!urlData.success || !urlData.downloadUrl) { + clearTimeout(timeoutId); + return NextResponse.json( + { + success: false, + error: urlData.error || "Failed to get download URL", + }, + { status: 500 } + ); + } + + // Step 2: Fetch the actual workflow from the presigned R2 URL + const workflowResponse = await fetch(urlData.downloadUrl, { + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (!workflowResponse.ok) { + console.error( + "Error fetching workflow from R2:", + workflowResponse.status, + workflowResponse.statusText + ); + return NextResponse.json( + { + success: false, + error: "Failed to download workflow", + }, + { status: 500 } ); } - const data = await response.json(); + const workflow = await workflowResponse.json(); - return NextResponse.json(data); + return NextResponse.json({ + success: true, + workflow, + }); } catch (error) { clearTimeout(timeoutId);