From 963b389e1b430cb2f9ce808fca841d6e4df9b4cc Mon Sep 17 00:00:00 2001 From: aasrithasure Date: Tue, 16 Jun 2026 17:50:33 +0530 Subject: [PATCH] Add Gemini API request timeout handling --- backend/.env.example | 3 ++ backend/controllers/aiController.js | 39 +++++++++++++++++++++++-- backend/controllers/resumeController.js | 21 ++++++++++++- backend/routes/AptitudeQuestions.js | 20 ++++++++++++- backend/routes/aiRoutes.js | 21 ++++++++++++- 5 files changed, 99 insertions(+), 5 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index 5dfee44..2946f3a 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -28,6 +28,9 @@ GEMINI_API_KEY=your_gemini_api_key_here # Gemini model to use (defaults to gemini-1.5-flash-latest if unset) GEMINI_MODEL=gemini-1.5-flash-latest +# Timeout duration for Gemini API calls in milliseconds (defaults to 30000 if unset) +GEMINI_TIMEOUT=30000 + # GitHub Configuration # Personal access token for fetching book data from GitHub # Create at: https://github.com/settings/tokens diff --git a/backend/controllers/aiController.js b/backend/controllers/aiController.js index 59ccf08..43e7b84 100644 --- a/backend/controllers/aiController.js +++ b/backend/controllers/aiController.js @@ -6,6 +6,7 @@ const { // Initialize Gemini with API key from .env const ai = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); +const GEMINI_TIMEOUT = parseInt(process.env.GEMINI_TIMEOUT, 10) || 30000; // @desc Generate interview questions and answers using Gemini // @route POST /api/ai/generate-questions @@ -37,17 +38,28 @@ const generateInterviewQuestions = async (req, res) => { let result = null; let usedModel = null; for (const m of candidateModels) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), GEMINI_TIMEOUT); try { console.log(`Trying model: ${m}`); - const model = ai.getGenerativeModel({ model: m }); + const model = ai.getGenerativeModel({ model: m }, { signal: controller.signal }); result = await model.generateContent([prompt]); usedModel = m; console.log(`Successfully used model: ${m}`); break; } catch (e) { + if (e.name === "AbortError" || (e.message && e.message.includes("abort"))) { + const timeoutErr = new Error(`Gemini API call timed out after ${GEMINI_TIMEOUT}ms for model ${m}`); + timeoutErr.name = "TimeoutError"; + console.error(`[Timeout] Model ${m} timed out:`, timeoutErr.message); + lastErr = timeoutErr; + break; + } console.error(`Model ${m} failed:`, e.message); lastErr = e; continue; + } finally { + clearTimeout(timeoutId); } } if (!result) throw lastErr || new Error("All Gemini models failed"); @@ -76,6 +88,12 @@ const generateInterviewQuestions = async (req, res) => { } } catch (error) { console.error("Gemini API Error:", error); // Log the error + if (error.name === "TimeoutError") { + return res.status(504).json({ + message: "Request timed out", + error: error.message, + }); + } res.status(500).json({ message: "Failed to generate questions", error: error.message, @@ -105,17 +123,28 @@ const generateConceptExplanation = async (req, res) => { let result = null; let usedModel = null; for (const m of candidateModels) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), GEMINI_TIMEOUT); try { console.log(`Trying model: ${m}`); - const model = ai.getGenerativeModel({ model: m }); + const model = ai.getGenerativeModel({ model: m }, { signal: controller.signal }); result = await model.generateContent([prompt]); usedModel = m; console.log(`Successfully used model: ${m}`); break; } catch (e) { + if (e.name === "AbortError" || (e.message && e.message.includes("abort"))) { + const timeoutErr = new Error(`Gemini API call timed out after ${GEMINI_TIMEOUT}ms for model ${m}`); + timeoutErr.name = "TimeoutError"; + console.error(`[Timeout] Model ${m} timed out:`, timeoutErr.message); + lastErr = timeoutErr; + break; + } console.error(`Model ${m} failed:`, e.message); lastErr = e; continue; + } finally { + clearTimeout(timeoutId); } } if (!result) throw lastErr || new Error("All Gemini models failed"); @@ -139,6 +168,12 @@ const generateConceptExplanation = async (req, res) => { } } catch (error) { console.error("Gemini API Error:", error); + if (error.name === "TimeoutError") { + return res.status(504).json({ + message: "Request timed out", + error: error.message, + }); + } res.status(500).json({ message: "Failed to generate explanation", error: error.message, diff --git a/backend/controllers/resumeController.js b/backend/controllers/resumeController.js index d7b5661..a00a545 100644 --- a/backend/controllers/resumeController.js +++ b/backend/controllers/resumeController.js @@ -59,6 +59,8 @@ const compileResume = async (req, res) => { } } +const GEMINI_TIMEOUT = parseInt(process.env.GEMINI_TIMEOUT, 10) || 30000; + const analyzeResume = async (req, res) => { try { if (!req.file) { @@ -102,8 +104,10 @@ DO NOT wrap the response in markdown blocks like \`\`\`json. Return ONLY the raw let result = null; for (const m of candidateModels) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), GEMINI_TIMEOUT); try { - const model = genAI.getGenerativeModel({ model: m }); + const model = genAI.getGenerativeModel({ model: m }, { signal: controller.signal }); result = await model.generateContent([ prompt, { @@ -115,8 +119,17 @@ DO NOT wrap the response in markdown blocks like \`\`\`json. Return ONLY the raw ]); break; // Stop on first success } catch (e) { + if (e.name === "AbortError" || (e.message && e.message.includes("abort"))) { + const timeoutErr = new Error(`Gemini API call timed out after ${GEMINI_TIMEOUT}ms for model ${m}`); + timeoutErr.name = "TimeoutError"; + console.error(`[Timeout] Model ${m} timed out:`, timeoutErr.message); + lastErr = timeoutErr; + break; + } lastErr = e; continue; + } finally { + clearTimeout(timeoutId); } } @@ -142,6 +155,12 @@ DO NOT wrap the response in markdown blocks like \`\`\`json. Return ONLY the raw } catch (error) { console.error("Resume Analysis Error:", error); + if (error.name === "TimeoutError") { + return res.status(504).json({ + message: "Request timed out", + error: error.message, + }); + } res.status(500).json({ message: "Failed to analyze resume", error: error.message }); } } diff --git a/backend/routes/AptitudeQuestions.js b/backend/routes/AptitudeQuestions.js index f96e3b6..8615993 100644 --- a/backend/routes/AptitudeQuestions.js +++ b/backend/routes/AptitudeQuestions.js @@ -5,6 +5,7 @@ const { sanitizeAiPrompt } = require("../middlewares/sanitizeAiPrompt"); const router = express.Router(); const ai = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); +const GEMINI_TIMEOUT = parseInt(process.env.GEMINI_TIMEOUT, 10) || 30000; // GET /api/questions?topic=Probability router.get("/", validateAiPrompt, sanitizeAiPrompt, async (req, res) => { @@ -39,17 +40,28 @@ router.get("/", validateAiPrompt, sanitizeAiPrompt, async (req, res) => { let usedModel = null; for (const m of candidateModels) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), GEMINI_TIMEOUT); try { console.log(`[Aptitude] Trying model: ${m}`); - const model = ai.getGenerativeModel({ model: m }); + const model = ai.getGenerativeModel({ model: m }, { signal: controller.signal }); result = await model.generateContent([prompt]); usedModel = m; console.log(`[Aptitude] Successfully used model: ${m}`); break; } catch (e) { + if (e.name === "AbortError" || (e.message && e.message.includes("abort"))) { + const timeoutErr = new Error(`Gemini API call timed out after ${GEMINI_TIMEOUT}ms for model ${m}`); + timeoutErr.name = "TimeoutError"; + console.error(`[Aptitude][Timeout] Model ${m} timed out:`, timeoutErr.message); + lastErr = timeoutErr; + break; + } console.error(`[Aptitude] Model ${m} failed:`, e.message); lastErr = e; continue; + } finally { + clearTimeout(timeoutId); } } @@ -83,6 +95,12 @@ router.get("/", validateAiPrompt, sanitizeAiPrompt, async (req, res) => { res.json(questions); } catch (error) { console.error("Gemini API error:", error); + if (error.name === "TimeoutError") { + return res.status(504).json({ + error: "Request timed out", + details: error.message, + }); + } res .status(500) .json({ error: "Failed to generate questions", details: error.message }); diff --git a/backend/routes/aiRoutes.js b/backend/routes/aiRoutes.js index 5ea1d95..063c14b 100644 --- a/backend/routes/aiRoutes.js +++ b/backend/routes/aiRoutes.js @@ -5,6 +5,8 @@ const { aiLimiter } = require('../middlewares/rateLimiter'); const { validateAiPrompt } = require('../middlewares/validateAiPrompt'); const { sanitizeAiPrompt } = require('../middlewares/sanitizeAiPrompt'); +const GEMINI_TIMEOUT = parseInt(process.env.GEMINI_TIMEOUT, 10) || 30000; + // Shared handler for text generation (used by multiple route aliases) async function generateHandler(req, res) { const { prompt } = req.body || {}; @@ -30,14 +32,25 @@ async function generateHandler(req, res) { let result = null; let usedModel = null; for (const m of candidateModels) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), GEMINI_TIMEOUT); try { - const model = genAI.getGenerativeModel({ model: m }); + const model = genAI.getGenerativeModel({ model: m }, { signal: controller.signal }); result = await model.generateContent(prompt); usedModel = m; break; } catch (e) { + if (e.name === "AbortError" || (e.message && e.message.includes("abort"))) { + const timeoutErr = new Error(`Gemini API call timed out after ${GEMINI_TIMEOUT}ms for model ${m}`); + timeoutErr.name = "TimeoutError"; + console.error(`[AI][Timeout] Model ${m} timed out:`, timeoutErr.message); + lastErr = timeoutErr; + break; + } lastErr = e; continue; + } finally { + clearTimeout(timeoutId); } } if (!result) throw lastErr || new Error("All Gemini models failed"); @@ -58,6 +71,12 @@ async function generateHandler(req, res) { return res.json({ text: cleanedText, model: usedModel }); } catch (error) { console.error("[AI] Generation failed:", error.message); + if (error.name === "TimeoutError") { + return res.status(504).json({ + error: "Request timed out", + detail: error.message, + }); + } return res .status(500) .json({ error: "Failed to generate content", detail: error.message });