Skip to content

Commit cbd309c

Browse files
committed
feat(frontend): robust API calls with timeout and fallback; add size guard to avoid large upload timeouts
1 parent 08e62cf commit cbd309c

File tree

2 files changed

+47
-6
lines changed

2 files changed

+47
-6
lines changed

frontend/src/components/VideoUpload.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface VideoUploadProps {
1414
type ProcessingStage = "idle" | "uploading" | "analyzing" | "tracking" | "generating" | "complete";
1515

1616
const VideoUpload = ({ onVideoProcessed }: VideoUploadProps) => {
17+
const MAX_SIZE_MB = 60; // guard against very large uploads on free tiers
1718
const [uploadedVideo, setUploadedVideo] = useState<File | null>(null);
1819
const [videoUrl, setVideoUrl] = useState<string>("");
1920
const [processedVideoUrl, setProcessedVideoUrl] = useState<string>("");
@@ -36,6 +37,14 @@ const VideoUpload = ({ onVideoProcessed }: VideoUploadProps) => {
3637
const videoFile = files.find(file => file.type.startsWith('video/'));
3738

3839
if (videoFile) {
40+
if (videoFile.size > MAX_SIZE_MB * 1024 * 1024) {
41+
toast({
42+
title: "File too large",
43+
description: `Please upload a video under ${MAX_SIZE_MB}MB for stable processing`,
44+
variant: "destructive",
45+
});
46+
return;
47+
}
3948
setUploadedVideo(videoFile);
4049
const url = URL.createObjectURL(videoFile);
4150
setVideoUrl(url);
@@ -55,6 +64,14 @@ const VideoUpload = ({ onVideoProcessed }: VideoUploadProps) => {
5564
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
5665
const file = e.target.files?.[0];
5766
if (file && file.type.startsWith('video/')) {
67+
if (file.size > MAX_SIZE_MB * 1024 * 1024) {
68+
toast({
69+
title: "File too large",
70+
description: `Please upload a video under ${MAX_SIZE_MB}MB for stable processing`,
71+
variant: "destructive",
72+
});
73+
return;
74+
}
5875
setUploadedVideo(file);
5976
const url = URL.createObjectURL(file);
6077
setVideoUrl(url);

frontend/src/lib/jobs.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
import { API_BASE } from "@/lib/api";
22

3+
async function fetchWithTimeout(input: RequestInfo | URL, init: RequestInit = {}, timeoutMs = 60000): Promise<Response> {
4+
const controller = new AbortController();
5+
const id = setTimeout(() => controller.abort(), timeoutMs);
6+
try {
7+
const res = await fetch(input, { ...init, signal: controller.signal });
8+
return res;
9+
} finally {
10+
clearTimeout(id);
11+
}
12+
}
13+
314
export async function createProcessingJob(file: File, fast = false): Promise<string> {
415
const form = new FormData();
516
form.append("video", file);
617
form.append("fast", String(fast));
7-
const res = await fetch(`${API_BASE ? API_BASE : ""}/api/jobs`, {
8-
method: "POST",
9-
body: form,
10-
});
18+
// Try absolute backend URL first; on network error, fall back to relative (Vercel rewrite)
19+
let res: Response;
20+
try {
21+
res = await fetchWithTimeout(`${API_BASE ? API_BASE : ""}/api/jobs`, { method: "POST", body: form }, 120000);
22+
} catch (_) {
23+
res = await fetchWithTimeout(`/api/jobs`, { method: "POST", body: form }, 120000);
24+
}
1125
if (!res.ok) throw new Error(await res.text());
1226
const data = await res.json();
1327
return data.job_id as string;
@@ -16,7 +30,12 @@ export async function createProcessingJob(file: File, fast = false): Promise<str
1630
export async function pollJob(jobId: string, { intervalMs = 3000, timeoutMs = 15 * 60 * 1000 } = {}): Promise<void> {
1731
const start = Date.now();
1832
while (true) {
19-
const res = await fetch(`${API_BASE ? API_BASE : ""}/api/jobs/${jobId}/status`);
33+
let res: Response;
34+
try {
35+
res = await fetchWithTimeout(`${API_BASE ? API_BASE : ""}/api/jobs/${jobId}/status`, {}, 30000);
36+
} catch (_) {
37+
res = await fetchWithTimeout(`/api/jobs/${jobId}/status`, {}, 30000);
38+
}
2039
if (!res.ok) throw new Error(await res.text());
2140
const data = await res.json();
2241
if (data.status === "done") return;
@@ -27,7 +46,12 @@ export async function pollJob(jobId: string, { intervalMs = 3000, timeoutMs = 15
2746
}
2847

2948
export async function fetchJobResult(jobId: string): Promise<Blob> {
30-
const res = await fetch(`${API_BASE ? API_BASE : ""}/api/jobs/${jobId}/result`);
49+
let res: Response;
50+
try {
51+
res = await fetchWithTimeout(`${API_BASE ? API_BASE : ""}/api/jobs/${jobId}/result`, {}, 60000);
52+
} catch (_) {
53+
res = await fetchWithTimeout(`/api/jobs/${jobId}/result`, {}, 60000);
54+
}
3155
if (res.status === 202) throw new Error("Job not complete yet");
3256
if (!res.ok) throw new Error(await res.text());
3357
const contentType = res.headers.get("content-type") || "";

0 commit comments

Comments
 (0)