From 1bd484797fe8a4340975bc603887b1b0a878bd4e Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Fri, 5 Dec 2025 00:43:13 +1000 Subject: [PATCH] fix: add 120s timeout per tool and increase route maxDuration to 300s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add AbortController timeout (120s) to Railway fetch requests - Gracefully handle timeout errors and report to health check system - Increase /api/chat maxDuration from 60s to 300s (5 minutes) - Prevents entire chat from timing out when one tool has large dependencies - Tools that timeout are logged and skipped, allowing others to load Fixes issue where tools like ctx-zip with many dependencies would cause the entire chat request to timeout after 60 seconds. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/playground/src/app/api/chat/route.ts | 2 +- .../playground/src/lib/dynamic-tool-loader.ts | 99 ++++++++++++------- 2 files changed, 66 insertions(+), 35 deletions(-) diff --git a/apps/playground/src/app/api/chat/route.ts b/apps/playground/src/app/api/chat/route.ts index fbe1c41..d8b8049 100644 --- a/apps/playground/src/app/api/chat/route.ts +++ b/apps/playground/src/app/api/chat/route.ts @@ -12,7 +12,7 @@ import { loadAllTools, sanitizeToolName } from '~/lib/tool-loader'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; -export const maxDuration = 60; +export const maxDuration = 300; // 5 minutes for complex tool loading // Add conversation state tracking (in-memory for MVP) // biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex diff --git a/apps/playground/src/lib/dynamic-tool-loader.ts b/apps/playground/src/lib/dynamic-tool-loader.ts index f2be8ae..64bf6dc 100644 --- a/apps/playground/src/lib/dynamic-tool-loader.ts +++ b/apps/playground/src/lib/dynamic-tool-loader.ts @@ -111,47 +111,78 @@ export async function loadToolDynamically( console.log(`🔗 Railway URL: ${RAILWAY_SERVICE_URL}`); // Call Railway service to load and describe tool - const response = await fetch(`${RAILWAY_SERVICE_URL}/load-and-describe`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - packageName, - exportName, - version, - importUrl: importUrl || `https://esm.sh/${packageName}@${version}`, - env: env || {}, - }), - }); + // 120 second timeout per tool to handle large dependency downloads + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 120000); - if (!response.ok) { - const errorText = await response.text(); - console.error(`❌ Railway service error (${response.status}): ${errorText}`); + let response; + let data; - // Trigger health check update in background (non-blocking) - reportToolFailure( - packageName, - exportName, - `Railway service error (${response.status}): ${errorText}`, - 'import' - ).catch((err) => - console.error(`Failed to report tool failure for ${packageName}/${exportName}:`, err) - ); + try { + response = await fetch(`${RAILWAY_SERVICE_URL}/load-and-describe`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + packageName, + exportName, + version, + importUrl: importUrl || `https://esm.sh/${packageName}@${version}`, + env: env || {}, + }), + signal: controller.signal, + }); - return null; - } + clearTimeout(timeout); - const data = await response.json(); + if (!response.ok) { + const errorText = await response.text(); + console.error(`❌ Railway service error (${response.status}): ${errorText}`); - if (!data.success) { - console.error(`❌ Failed to load tool: ${data.error}`); - - // Trigger health check update in background (non-blocking) - reportToolFailure(packageName, exportName, data.error || 'Unknown error', 'import').catch( - (err) => + // Trigger health check update in background (non-blocking) + reportToolFailure( + packageName, + exportName, + `Railway service error (${response.status}): ${errorText}`, + 'import' + ).catch((err) => console.error(`Failed to report tool failure for ${packageName}/${exportName}:`, err) - ); + ); - return null; + return null; + } + + data = await response.json(); + + if (!data.success) { + console.error(`❌ Failed to load tool: ${data.error}`); + + // Trigger health check update in background (non-blocking) + reportToolFailure(packageName, exportName, data.error || 'Unknown error', 'import').catch( + (err) => + console.error(`Failed to report tool failure for ${packageName}/${exportName}:`, err) + ); + + return null; + } + } catch (fetchError) { + clearTimeout(timeout); + + if (fetchError instanceof Error && fetchError.name === 'AbortError') { + console.error(`❌ Railway request timeout after 120s for ${packageName}/${exportName}`); + + reportToolFailure( + packageName, + exportName, + 'Railway service timeout (120s) - tool dependencies may be too large', + 'import' + ).catch((err) => + console.error(`Failed to report tool failure for ${packageName}/${exportName}:`, err) + ); + + return null; + } + + throw fetchError; } console.log(`✅ Tool loaded from Railway: ${cacheKey}`);