From 04410386722276244e0be729439e5915433efe90 Mon Sep 17 00:00:00 2001 From: Willem Jiang Date: Sun, 26 Oct 2025 07:34:12 +0800 Subject: [PATCH] fix: improve config loading resilience for non-localhost access (#510) (#658) * fix: improve config loading resilience for non-localhost access (#510) - Add DEFAULT_CONFIG fallback to always return valid config even if fetch fails - Implement retry logic with exponential backoff (max 2 retries) to handle transient failures - Add 5-second fetch timeout to prevent hanging on unreachable backends - Improve error logging with clear messages about config fetch status - Always return DeerFlowConfig (never null) to prevent UI rendering issues - Add safety checks in input-box component to verify reasoning models before access - Improve type safety: verify array length before accessing array indices - Add comprehensive documentation in .env.example with examples for different deployment scenarios - Document NEXT_PUBLIC_API_URL variable behavior and fallback mechanism * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: add nullish coalescing to prevent TypeScript error in input-box - Add ?? operator to handle potential undefined value when accessing reasoning[0] - Fixes TS2322 error: Type 'string | undefined' is not assignable to type 'string | number | Date' --------- Co-authored-by: Willem Jiang <143703838+willem-bd@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- web/.env.example | 25 ++++++++- web/src/app/chat/components/input-box.tsx | 4 +- web/src/core/api/hooks.ts | 67 ++++++++++++++++++----- 3 files changed, 80 insertions(+), 16 deletions(-) diff --git a/web/.env.example b/web/.env.example index 8641b06..e712a26 100644 --- a/web/.env.example +++ b/web/.env.example @@ -13,8 +13,31 @@ # SERVERVAR="foo" # NEXT_PUBLIC_CLIENTVAR="bar" +# API Configuration +# IMPORTANT: Set this variable when accessing the application from a network IP address +# or when the backend server is hosted on a different machine. +# +# Default value (when not set): http://localhost:8000/api +# This default works only when accessing from localhost (http://localhost:3000) +# +# Examples: +# - Local development (no change needed): +# NEXT_PUBLIC_API_URL=http://localhost:8000/api +# +# - Accessing from LAN IP (e.g., http://192.168.1.100:3000): +# NEXT_PUBLIC_API_URL=http://192.168.1.100:8000/api +# +# - Accessing from different machine on network (e.g., http://10.24.9.33:3000): +# NEXT_PUBLIC_API_URL=http://your-backend-server-ip:8000/api +# +# - Remote deployment (behind reverse proxy): +# NEXT_PUBLIC_API_URL=https://your-domain.com/api +# +# If not set, the frontend will attempt to use http://localhost:8000/api as fallback. +# If the backend is unreachable, the application will use default configuration with +# limited features (some LLM-based features may be disabled). NEXT_PUBLIC_API_URL=http://localhost:8000/api -# Github +# Github OAuth Token (optional) GITHUB_OAUTH_TOKEN=xxxx diff --git a/web/src/app/chat/components/input-box.tsx b/web/src/app/chat/components/input-box.tsx index ae18ec4..0d6025a 100644 --- a/web/src/app/chat/components/input-box.tsx +++ b/web/src/app/chat/components/input-box.tsx @@ -214,7 +214,7 @@ export function InputBox({
- {config?.models.reasoning?.[0] && ( + {config?.models?.reasoning && config.models.reasoning.length > 0 && (

{t("deepThinkingTooltip.description", { - model: config.models.reasoning?.[0] ?? "", + model: config.models.reasoning[0] ?? "", })}

diff --git a/web/src/core/api/hooks.ts b/web/src/core/api/hooks.ts index 133cc62..37ae323 100644 --- a/web/src/core/api/hooks.ts +++ b/web/src/core/api/hooks.ts @@ -44,29 +44,70 @@ export function useReplayMetadata() { return { title, isLoading, hasError: error }; } +const DEFAULT_CONFIG: DeerFlowConfig = { + rag: { provider: "" }, + models: { basic: [], reasoning: [] }, +}; + export function useConfig(): { - config: DeerFlowConfig | null; + config: DeerFlowConfig; loading: boolean; } { const [loading, setLoading] = useState(true); - const [config, setConfig] = useState(null); + const [config, setConfig] = useState(DEFAULT_CONFIG); useEffect(() => { if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY) { setLoading(false); return; } - fetch(resolveServiceURL("./config")) - .then((res) => res.json()) - .then((config) => { - setConfig(config); - setLoading(false); - }) - .catch((err) => { - console.error("Failed to fetch config", err); - setConfig(null); - setLoading(false); - }); + + const fetchConfigWithRetry = async () => { + const maxRetries = 2; + let lastError: Error | null = null; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + const res = await fetch(resolveServiceURL("./config"), { + signal: AbortSignal.timeout(5000), // 5 second timeout + }); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + + const configData = await res.json(); + setConfig(configData); + setLoading(false); + return; // Success, exit retry loop + } catch (err) { + lastError = err instanceof Error ? err : new Error(String(err)); + + // Log attempt details + if (attempt === 0) { + const apiUrl = resolveServiceURL("./config"); + console.warn( + `[Config] Failed to fetch from ${apiUrl}: ${lastError.message}`, + ); + } + + // Wait before retrying (exponential backoff: 100ms, 500ms) + if (attempt < maxRetries) { + const delay = Math.pow(2, attempt) * 100; + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } + + // All retries failed, use default config + console.warn( + `[Config] Using default config after ${maxRetries + 1} attempts. Last error: ${lastError?.message ?? "Unknown"}`, + ); + setConfig(DEFAULT_CONFIG); + setLoading(false); + }; + + void fetchConfigWithRetry(); }, []); return { config, loading };