From 7c7a24d94ea064ca41c6d56866e6aad81a91f4a5 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sat, 7 Feb 2026 20:21:32 +1300 Subject: [PATCH] fix: use positive content-type check for video/image data URLs WaveSpeed's CloudFront CDN returns `binary/octet-stream` which wasn't caught by the old `!== "application/octet-stream"` check, producing unplayable data URLs. Now only accepts content types starting with `video/` or `image/`, falling back to `video/mp4` or `image/png`. Co-Authored-By: Claude Opus 4.6 --- src/app/api/generate/route.ts | 17 ++++++++++------- src/app/api/save-generation/route.ts | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts index 33f42022..e712ca17 100644 --- a/src/app/api/generate/route.ts +++ b/src/app/api/generate/route.ts @@ -7,9 +7,8 @@ * - For local development, server.requestTimeout must be set in server.js (Node.js default is 5 minutes) * * FAL.AI QUEUE API NOTE: - * The generateWithFalQueue function exists but is NOT used because fal.ai's queue API - * has file size limitations that are too restrictive for our use case. We use the blocking - * fal.run endpoint instead, which requires the server timeout to be extended for video generation. + * Uses generateWithFalQueue with async queue submission + polling. + * Images are uploaded to fal CDN before submission to avoid payload size issues. */ import { NextRequest, NextResponse } from "next/server"; import { GoogleGenAI } from "@google/genai"; @@ -992,6 +991,7 @@ async function generateWithFalQueue( } const submitResult = await submitResponse.json(); + console.log(`[API:${requestId}] Queue submit response:`, JSON.stringify(submitResult).substring(0, 500)); const falRequestId = submitResult.request_id; if (!falRequestId) { @@ -1002,7 +1002,10 @@ async function generateWithFalQueue( }; } - console.log(`[API:${requestId}] Queue request submitted: ${falRequestId}`); + // Use URLs from response if provided, otherwise construct them + const statusUrl = submitResult.status_url || `https://queue.fal.run/${modelId}/requests/${falRequestId}/status`; + const responseUrl = submitResult.response_url || `https://queue.fal.run/${modelId}/requests/${falRequestId}`; + console.log(`[API:${requestId}] Queue request submitted: ${falRequestId}, status URL: ${statusUrl}`); // Poll for completion const maxWaitTime = 10 * 60 * 1000; // 10 minutes for video @@ -1022,7 +1025,7 @@ async function generateWithFalQueue( await new Promise(resolve => setTimeout(resolve, pollInterval)); const statusResponse = await fetch( - `https://queue.fal.run/${modelId}/requests/${falRequestId}/status`, + statusUrl, { headers: apiKey ? { "Authorization": `Key ${apiKey}` } : {} } ); @@ -1045,7 +1048,7 @@ async function generateWithFalQueue( if (status === "COMPLETED") { // Fetch the result const resultResponse = await fetch( - `https://queue.fal.run/${modelId}/requests/${falRequestId}`, + responseUrl, { headers: apiKey ? { "Authorization": `Key ${apiKey}` } : {} } ); @@ -2283,7 +2286,7 @@ async function generateWithWaveSpeed( const rawContentType = outputResponse.headers.get("content-type"); const contentType = - (rawContentType && rawContentType !== "application/octet-stream") + (rawContentType && (rawContentType.startsWith("video/") || rawContentType.startsWith("image/"))) ? rawContentType : (isVideoModel ? "video/mp4" : "image/png"); diff --git a/src/app/api/save-generation/route.ts b/src/app/api/save-generation/route.ts index c9ce94df..9c17be2b 100644 --- a/src/app/api/save-generation/route.ts +++ b/src/app/api/save-generation/route.ts @@ -166,7 +166,7 @@ export async function POST(request: NextRequest) { } const rawSaveContentType = response.headers.get("content-type"); - const contentType = (rawSaveContentType && rawSaveContentType !== "application/octet-stream") + const contentType = (rawSaveContentType && (rawSaveContentType.startsWith("video/") || rawSaveContentType.startsWith("image/"))) ? rawSaveContentType : (isVideo ? "video/mp4" : "image/png"); extension = getExtensionFromMime(contentType);