From 1535a222b85f59002f95e5a104901b37583cefe3 Mon Sep 17 00:00:00 2001 From: AbelOsaretin Date: Thu, 23 Jul 2026 12:10:34 +0100 Subject: [PATCH] fix(quizzes): add configurable timeout to AI service fetch The fetch call to the AI service in ai-client.ts had no AbortController or timeout configuration. If the AI service hangs, the request blocks indefinitely, holding open sockets and event loop references. Under AI service degradation this can exhaust available sockets. Add AbortController with a configurable timeout (default 30s) via the new AI_TIMEOUT_MS environment variable. The timeout error is caught, logged, and re-thrown so the existing fallback in quizService.generateQuiz continues to work. Closes #74 --- src/config/index.ts | 1 + src/modules/quizzes/ai-client.ts | 56 ++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/config/index.ts b/src/config/index.ts index 5666da6..261f2c6 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -39,6 +39,7 @@ const envSchema = z.object({ // AI service (chainlearn-ai) used for quiz generation AI_SERVICE_URL: z.string().url().default("http://localhost:8000"), + AI_TIMEOUT_MS: z.coerce.number().default(30_000), }); export type Env = z.infer; diff --git a/src/modules/quizzes/ai-client.ts b/src/modules/quizzes/ai-client.ts index 58f0acb..f01be64 100644 --- a/src/modules/quizzes/ai-client.ts +++ b/src/modules/quizzes/ai-client.ts @@ -25,26 +25,40 @@ export interface GenerateQuizFromAIParams { export async function generateQuizFromAI( params: GenerateQuizFromAIParams ): Promise { - const response = await fetch(`${config.AI_SERVICE_URL}/generate-quiz`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - user_id: params.userId, - course_id: params.courseId, - module_id: params.moduleId, - difficulty: params.difficulty, - num_questions: params.numQuestions, - }), - }); - - if (!response.ok) { - logger.error( - { status: response.status }, - "AI service quiz generation failed" - ); - throw new Error(`AI service returned ${response.status}`); - } + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), config.AI_TIMEOUT_MS); + + try { + const response = await fetch(`${config.AI_SERVICE_URL}/generate-quiz`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + user_id: params.userId, + course_id: params.courseId, + module_id: params.moduleId, + difficulty: params.difficulty, + num_questions: params.numQuestions, + }), + signal: controller.signal, + }); - const data = (await response.json()) as AiQuizResponse; - return data.questions; + if (!response.ok) { + logger.error( + { status: response.status }, + "AI service quiz generation failed" + ); + throw new Error(`AI service returned ${response.status}`); + } + + const data = (await response.json()) as AiQuizResponse; + return data.questions; + } catch (err) { + if (err instanceof Error && err.name === "AbortError") { + logger.error({ timeout: config.AI_TIMEOUT_MS }, "AI service request timed out"); + throw new Error(`AI service request timed out after ${config.AI_TIMEOUT_MS}ms`); + } + throw err; + } finally { + clearTimeout(timeout); + } }