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); + } }