From c6b4456baf9ce6afe14ac71267d340b6845dd671 Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Thu, 4 Dec 2025 14:25:59 +1000 Subject: [PATCH] fix: use function body in DefaultChatTransport to send latest env vars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROBLEM: DefaultChatTransport body is cloned ONCE on mount, so env vars were always empty {} even after localStorage loaded them. SOLUTION: Use a function for body instead of an object. AI SDK v6 calls body() on each request, ensuring latest env vars are sent. Changes: - useChat.ts: body: { env } → body: () => ({ env: buildEnvObject() }) - buildEnvObject() is called fresh on each request - Env vars now sent correctly to /api/chat Credit: ChatGPT for identifying the exact AI SDK v6 pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ENV_VAR_TRANSPORT_ISSUE.md | 86 +++++++++++++++++++ apps/playground/src/app/api/chat/route.ts | 5 +- apps/playground/src/hooks/useChat.ts | 24 +++--- .../playground/src/lib/dynamic-tool-loader.ts | 5 +- apps/railway-executor/server.ts | 5 +- 5 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 ENV_VAR_TRANSPORT_ISSUE.md diff --git a/ENV_VAR_TRANSPORT_ISSUE.md b/ENV_VAR_TRANSPORT_ISSUE.md new file mode 100644 index 0000000..3a04693 --- /dev/null +++ b/ENV_VAR_TRANSPORT_ISSUE.md @@ -0,0 +1,86 @@ +# Environment Variables Not Sent to API - Frontend Transport Issue + +## Problem + +Environment variables saved in localStorage are NOT being sent to `/api/chat` endpoint. + +**Evidence from logs:** +``` +📥 Request body: { + "conversationId": "7xur5hf1GDSOQMgFYF-l7", + "env": {}, // ❌ EMPTY - should have FIRECRAWL_API_KEY + ... +} +``` + +## Root Cause + +The issue is in `apps/playground/src/hooks/useChat.ts`: + +```typescript +export function useChat() { + const [conversationId] = useState(() => nanoid()); + const envVars = useEnvVars(); // ❌ Empty on first render (useEffect loads async) + + const envObject = envVars.reduce( + (acc, { key, value }) => { + acc[key] = value; + return acc; + }, + {} as Record + ); + + const chat = useAISDKChat({ + transport: new DefaultChatTransport({ // ❌ Created ONCE with empty envObject + api: '/api/chat', + body: { + conversationId, + env: envObject, // ❌ This is {} on first render, never updates + }, + }), + }); + + return { ...chat, conversationId }; +} +``` + +**Why it fails:** + +1. `useEnvVars()` loads from localStorage inside a `useEffect` (async) +2. On first render, `envVars = []`, so `envObject = {}` +3. `DefaultChatTransport` is created with `body: { env: {} }` +4. Even when `envVars` updates later, the transport is already created and doesn't re-create + +## Attempted Solutions That Don't Work + +❌ **Just updating state** - Transport is created once and cached +❌ **Using useEffect** - Transport is already created before effect runs + +## What We Need + +The `body` field in `DefaultChatTransport` needs to be **dynamic** and read the latest env vars on each request, not just once during component mount. + +## Questions for ChatGPT + +1. **How do we make `DefaultChatTransport` body dynamic?** Can we pass a function instead of an object? + +2. **Does AI SDK have a way to update transport body between messages?** The env vars might change while the chat is open. + +3. **Should we use a custom transport instead?** Can we implement our own transport that reads env vars fresh on each request? + +4. **Alternative: Can we manually add env to each message?** Is there a way to inject extra data per-request instead of per-transport? + +## Current Code Files + +- `apps/playground/src/hooks/useChat.ts` - The broken hook +- `apps/playground/src/components/sidebar/SettingsSidebar.tsx` - Where env vars are stored (works fine) +- `apps/playground/src/app/api/chat/route.ts` - Server expects `body.env` but receives `{}` + +## What We Know Works + +✅ Saving env vars to localStorage - working +✅ Reading env vars from localStorage - working +✅ Server accepting and using env vars - working +❌ **Sending env vars from client to server - BROKEN** + +The ONLY broken part is the transport not sending the latest env object. diff --git a/apps/playground/src/app/api/chat/route.ts b/apps/playground/src/app/api/chat/route.ts index 11c0ba2..fbe1c41 100644 --- a/apps/playground/src/app/api/chat/route.ts +++ b/apps/playground/src/app/api/chat/route.ts @@ -32,7 +32,10 @@ export async function POST(request: NextRequest) { const clientEnv: Record = body.env || {}; console.log(`🔑 Conversation ID: ${conversationId}`); - console.log(`🔐 Client env vars: ${Object.keys(clientEnv).length} keys`, Object.keys(clientEnv)); + console.log( + `🔐 Client env vars: ${Object.keys(clientEnv).length} keys`, + Object.keys(clientEnv) + ); // Store env vars for this conversation (so cached tools can access them) setConversationEnv(conversationId, clientEnv); diff --git a/apps/playground/src/hooks/useChat.ts b/apps/playground/src/hooks/useChat.ts index f85fcc7..a2b7fca 100644 --- a/apps/playground/src/hooks/useChat.ts +++ b/apps/playground/src/hooks/useChat.ts @@ -18,22 +18,24 @@ export function useChat(): ReturnType & { conversationId: s // Get environment variables from settings sidebar const envVars = useEnvVars(); - // Convert env vars to object format - const envObject = envVars.reduce( - (acc, { key, value }) => { - acc[key] = value; - return acc; - }, - {} as Record - ); + // Convert env vars to object format (called fresh on each request) + const buildEnvObject = () => + envVars.reduce( + (acc, { key, value }) => { + acc[key] = value; + return acc; + }, + {} as Record + ); const chat = useAISDKChat({ transport: new DefaultChatTransport({ api: '/api/chat', - body: { + // Use function body so it's evaluated on each request (not just once on mount) + body: () => ({ conversationId, // Pass conversation ID to API - env: envObject, // Pass environment variables to API - }, + env: buildEnvObject(), // Pass LATEST environment variables to API + }), }), }); diff --git a/apps/playground/src/lib/dynamic-tool-loader.ts b/apps/playground/src/lib/dynamic-tool-loader.ts index 8acf005..51379cf 100644 --- a/apps/playground/src/lib/dynamic-tool-loader.ts +++ b/apps/playground/src/lib/dynamic-tool-loader.ts @@ -103,7 +103,10 @@ export async function loadToolDynamically( // Get the latest env vars for this conversation (not from closure!) const currentEnv = getConversationEnv(conversationId); - console.log(`🔐 Using env vars for conversation ${conversationId}:`, Object.keys(currentEnv)); + console.log( + `🔐 Using env vars for conversation ${conversationId}:`, + Object.keys(currentEnv) + ); const execResponse = await fetch(`${RAILWAY_SERVICE_URL}/execute-tool`, { method: 'POST', diff --git a/apps/railway-executor/server.ts b/apps/railway-executor/server.ts index c866293..4bb6382 100644 --- a/apps/railway-executor/server.ts +++ b/apps/railway-executor/server.ts @@ -231,7 +231,10 @@ async function executeTool(req: Request): Promise { console.log(` ✅ Set ${key} = ${String(value).substring(0, 10)}...`); } // Verify they're set - console.log(`🔍 Verification - Deno.env has:`, envKeys.map(k => `${k}=${Deno.env.get(k)?.substring(0, 10)}...`)); + console.log( + `🔍 Verification - Deno.env has:`, + envKeys.map((k) => `${k}=${Deno.env.get(k)?.substring(0, 10)}...`) + ); } else { console.log(`⚠️ No env vars provided in request`); }