From 636da527ae24b99cd1aea5e8b777c6414ec5102a Mon Sep 17 00:00:00 2001 From: Jayam Srivastava Date: Fri, 4 Sep 2026 20:52:48 +0530 Subject: [PATCH] fix: add bounded requests and validation for analysis APIs --- src/app/lib/api.ts | 125 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 103 insertions(+), 22 deletions(-) diff --git a/src/app/lib/api.ts b/src/app/lib/api.ts index 6b9d62a..99f87d7 100644 --- a/src/app/lib/api.ts +++ b/src/app/lib/api.ts @@ -4,6 +4,16 @@ import { firebaseAuth } from "./firebase"; const viteEnv = (import.meta as ImportMeta & { env?: Record }).env; const API_BASE_URL = (viteEnv?.VITE_API_BASE_URL || "http://127.0.0.1:8000").replace(/\/$/, ""); +const ANALYSIS_TIMEOUTS_MS = { + text: 45_000, + url: 60_000, + image: 90_000, + video: 180_000, +} as const; + +const MAX_IMAGE_BYTES = 10 * 1024 * 1024; +const MAX_VIDEO_BYTES = 50 * 1024 * 1024; + type Prediction = "FAKE" | "REAL" | "UNCERTAIN"; type ApiError = { @@ -83,16 +93,54 @@ async function readResponse(response: Response): Promise { return response.json() as Promise; } +async function requestWithTimeout( + input: RequestInfo | URL, + init: RequestInit, + timeoutMs: number, +): Promise { + const controller = new AbortController(); + const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs); + + try { + return await fetch(input, { ...init, signal: controller.signal }); + } catch (error) { + if (error instanceof DOMException && error.name === "AbortError") { + throw new Error( + `The analysis timed out after ${Math.round(timeoutMs / 1000)} seconds. Please try a smaller file or try again later.`, + ); + } + + if (error instanceof TypeError) { + throw new Error("Unable to reach the VeritasAI analysis service. Check the backend URL and your connection."); + } + + throw error instanceof Error ? error : new Error("Analysis request failed."); + } finally { + window.clearTimeout(timeoutId); + } +} + +function validateUpload(file: File, kind: "image" | "video") { + const expectedType = `${kind}/`; + if (!file || !file.type.startsWith(expectedType)) { + throw new Error(`Please select a valid ${kind} file.`); + } + + const maxBytes = kind === "image" ? MAX_IMAGE_BYTES : MAX_VIDEO_BYTES; + if (file.size > maxBytes) { + const maxMb = maxBytes / (1024 * 1024); + throw new Error(`${kind === "image" ? "Image" : "Video"} is too large. Maximum supported size is ${maxMb} MB.`); + } + + if (file.size === 0) { + throw new Error(`The selected ${kind} file is empty.`); + } +} + function isFirebaseIdToken(value: string): boolean { return value.split(".").length === 3 && value.startsWith("eyJ"); } -/** - * Email/password authentication is handled entirely by Firebase Authentication. - * Render is only used for VeritasAI application APIs after authentication. - * The JWT-shaped password path exists only for the existing Google flow, whose - * caller already has a Firebase ID token and should not make a second auth request. - */ export async function login(payload: { email: string; password: string; @@ -169,45 +217,78 @@ export async function signup(payload: { } export async function analyzeText(text: string): Promise { - const response = await fetch(`${API_BASE_URL}/api/analyze/text`, { - method: "POST", headers: withUserHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ text }), - }); + if (!text.trim()) throw new Error("Please enter some text to analyze."); + const response = await requestWithTimeout(`${API_BASE_URL}/api/analyze/text`, { + method: "POST", + headers: withUserHeaders({ "Content-Type": "application/json" }), + body: JSON.stringify({ text }), + }, ANALYSIS_TIMEOUTS_MS.text); return readResponse(response); } export async function analyzeUrl(url: string): Promise { - const response = await fetch(`${API_BASE_URL}/api/analyze/url`, { - method: "POST", headers: withUserHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ url }), - }); + if (!url.trim()) throw new Error("Please enter a URL to analyze."); + let parsed: URL; + try { + parsed = new URL(url.trim()); + } catch { + throw new Error("Please enter a valid URL."); + } + if (!/^https?:$/.test(parsed.protocol)) throw new Error("Only HTTP(S) URLs are supported."); + + const response = await requestWithTimeout(`${API_BASE_URL}/api/analyze/url`, { + method: "POST", + headers: withUserHeaders({ "Content-Type": "application/json" }), + body: JSON.stringify({ url: parsed.toString() }), + }, ANALYSIS_TIMEOUTS_MS.url); return readResponse(response); } export async function analyzeImage(file: File): Promise { + validateUpload(file, "image"); const formData = new FormData(); formData.append("file", file); - const response = await fetch(`${API_BASE_URL}/api/analyze/image`, { - method: "POST", headers: withUserHeaders(), body: formData, - }); + const response = await requestWithTimeout(`${API_BASE_URL}/api/analyze/image`, { + method: "POST", + headers: withUserHeaders(), + body: formData, + }, ANALYSIS_TIMEOUTS_MS.image); return readResponse(response); } export async function analyzeVideo(file: File): Promise { + validateUpload(file, "video"); const formData = new FormData(); formData.append("file", file); - const response = await fetch(`${API_BASE_URL}/api/analyze/video`, { - method: "POST", headers: withUserHeaders(), body: formData, - }); + const response = await requestWithTimeout(`${API_BASE_URL}/api/analyze/video`, { + method: "POST", + headers: withUserHeaders(), + body: formData, + }, ANALYSIS_TIMEOUTS_MS.video); return readResponse(response); } export async function analyzeVideoUrl(url: string): Promise { - const response = await fetch(`${API_BASE_URL}/api/analyze/video-url`, { - method: "POST", headers: withUserHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ url }), - }); + if (!url.trim()) throw new Error("Please enter a video URL."); + let parsed: URL; + try { + parsed = new URL(url.trim()); + } catch { + throw new Error("Please enter a valid video URL."); + } + if (!/^https?:$/.test(parsed.protocol)) throw new Error("Only HTTP(S) video URLs are supported."); + + const response = await requestWithTimeout(`${API_BASE_URL}/api/analyze/video-url`, { + method: "POST", + headers: withUserHeaders({ "Content-Type": "application/json" }), + body: JSON.stringify({ url: parsed.toString() }), + }, ANALYSIS_TIMEOUTS_MS.video); return readResponse(response); } export async function fetchHistory(): Promise { - const response = await fetch(`${API_BASE_URL}/api/history`, { headers: withUserHeaders() }); + const response = await requestWithTimeout(`${API_BASE_URL}/api/history`, { + headers: withUserHeaders(), + }, 30_000); return readResponse(response); }