From 155934c5b7f839abe9e1f72d7446b5943d2be208 Mon Sep 17 00:00:00 2001 From: Leo Date: Sun, 19 Oct 2025 13:47:15 -0400 Subject: [PATCH] fix: retry electric proxy and delay key toast --- apps/server/src/index.ts | 81 ++++++++++++++++++--------- apps/web/src/components/chat-room.tsx | 5 +- 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 657833db..51c8bf7b 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -317,46 +317,34 @@ new Elysia() if (value !== null) passthroughParams.set(key, value); } - const target = new URL(`${ELECTRIC_BASE_URL}/v1/shape`); - passthroughParams.forEach((value, key) => { - target.searchParams.set(key, value); - }); - let allowTables = DEFAULT_GATEKEEPER_TABLES.slice(); + let chatIdParam: string | null = null; switch (scope) { case "chats": { - target.searchParams.set("table", "chat"); - target.searchParams.set("where", `"user_id" = $1`); - target.searchParams.set("params[1]", userId); - target.searchParams.set("columns", "id,title,updated_at,last_message_at,user_id"); allowTables = ["chat"]; break; } case "messages": { - const chatId = url.searchParams.get("chatId"); - if (!chatId) { + chatIdParam = url.searchParams.get("chatId"); + if (!chatIdParam) { return withSecurityHeaders(new Response("Missing chatId", { status: 400 }), context.request); } try { const owned = await db .select({ id: chat.id }) .from(chat) - .where(and(eq(chat.id, chatId), eq(chat.userId, userId))); + .where(and(eq(chat.id, chatIdParam), eq(chat.userId, userId))); if (owned.length === 0) { - if (!inMemoryChatOwned(userId, chatId)) { + if (!inMemoryChatOwned(userId, chatIdParam)) { return withSecurityHeaders(new Response("Not Found", { status: 404 }), context.request); } } } catch (error) { if (process.env.NODE_ENV !== "test") console.error("chat.verify", error); - if (!inMemoryChatOwned(userId, chatId)) { + if (!inMemoryChatOwned(userId, chatIdParam)) { return withSecurityHeaders(new Response("Not Found", { status: 404 }), context.request); } } - target.searchParams.set("table", "message"); - target.searchParams.set("where", `"chat_id" = $1`); - target.searchParams.set("params[1]", chatId); - target.searchParams.set("columns", "id,chat_id,role,content,created_at,updated_at"); allowTables = ["message"]; break; } @@ -377,14 +365,57 @@ new Elysia() const ifNoneMatch = context.request.headers.get("if-none-match"); if (ifNoneMatch) upstreamHeaders.set("if-none-match", ifNoneMatch); - let upstreamResponse: Response; - try { - upstreamResponse = await fetch(target, { - method: "GET", - headers: upstreamHeaders, + const baseCandidates = [ELECTRIC_BASE_URL]; + const baseWithoutPort = ELECTRIC_BASE_URL.replace(/:\\d+$/, ""); + const candidate3000 = `${baseWithoutPort}:3000`; + if (!baseCandidates.includes(candidate3000)) { + baseCandidates.push(candidate3000); + } + + const buildTarget = (base: string) => { + const target = new URL(`${base}/v1/shape`); + passthroughParams.forEach((value, key) => { + target.searchParams.set(key, value); }); - } catch (error) { - console.error("electric.fetch", error); + switch (scope) { + case "chats": + target.searchParams.set("table", "chat"); + target.searchParams.set("where", `"user_id" = $1`); + target.searchParams.set("params[1]", userId); + target.searchParams.set("columns", "id,title,updated_at,last_message_at,user_id"); + break; + case "messages": { + target.searchParams.set("table", "message"); + target.searchParams.set("where", `"chat_id" = $1`); + target.searchParams.set("params[1]", chatIdParam ?? ""); + target.searchParams.set("columns", "id,chat_id,role,content,created_at,updated_at"); + break; + } + } + return target; + }; + + let upstreamResponse: Response | null = null; + let lastError: unknown = null; + for (const base of baseCandidates) { + const target = buildTarget(base); + try { + const response = await fetch(target, { + method: "GET", + headers: upstreamHeaders, + }); + if (response.status < 500) { + upstreamResponse = response; + break; + } + lastError = new Error(`electric responded ${response.status}`); + } catch (error) { + lastError = error; + } + } + + if (!upstreamResponse) { + console.error("electric.fetch", lastError); return withSecurityHeaders(new Response("Electric service unreachable", { status: 504 }), context.request); } diff --git a/apps/web/src/components/chat-room.tsx b/apps/web/src/components/chat-room.tsx index ade5af11..7ff60ce0 100644 --- a/apps/web/src/components/chat-room.tsx +++ b/apps/web/src/components/chat-room.tsx @@ -57,6 +57,7 @@ export default function ChatRoom({ chatId, initialMessages }: ChatRoomProps) { const [modelsLoading, setModelsLoading] = useState(false); const [modelOptions, setModelOptions] = useState<{ value: string; label: string; description?: string }[]>([]); const [selectedModel, setSelectedModel] = useState(null); + const [checkedApiKey, setCheckedApiKey] = useState(false); const missingKeyToastRef = useRef(null); useEffect(() => { @@ -158,10 +159,12 @@ export default function ChatRoom({ chatId, initialMessages }: ChatRoomProps) { setApiKey(stored); await fetchModels(stored); } + setCheckedApiKey(true); })(); }, [fetchModels]); useEffect(() => { + if (!checkedApiKey) return; if (!apiKey) { if (missingKeyToastRef.current == null) { missingKeyToastRef.current = toast.warning("Add your OpenRouter API key", { @@ -177,7 +180,7 @@ export default function ChatRoom({ chatId, initialMessages }: ChatRoomProps) { toast.dismiss(missingKeyToastRef.current); missingKeyToastRef.current = null; } - }, [apiKey, router]); + }, [apiKey, router, checkedApiKey]); const handleSaveApiKey = useCallback( async (key: string) => {