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>
This commit is contained in:
Willem Jiang
2025-10-26 07:34:12 +08:00
committed by GitHub
parent c7a82b82b4
commit 0441038672
3 changed files with 80 additions and 16 deletions

View File

@@ -13,8 +13,31 @@
# SERVERVAR="foo" # SERVERVAR="foo"
# NEXT_PUBLIC_CLIENTVAR="bar" # 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 NEXT_PUBLIC_API_URL=http://localhost:8000/api
# Github # Github OAuth Token (optional)
GITHUB_OAUTH_TOKEN=xxxx GITHUB_OAUTH_TOKEN=xxxx

View File

@@ -214,7 +214,7 @@ export function InputBox({
</div> </div>
<div className="flex items-center px-4 py-2"> <div className="flex items-center px-4 py-2">
<div className="flex grow gap-2"> <div className="flex grow gap-2">
{config?.models.reasoning?.[0] && ( {config?.models?.reasoning && config.models.reasoning.length > 0 && (
<Tooltip <Tooltip
className="max-w-60" className="max-w-60"
title={ title={
@@ -226,7 +226,7 @@ export function InputBox({
</h3> </h3>
<p> <p>
{t("deepThinkingTooltip.description", { {t("deepThinkingTooltip.description", {
model: config.models.reasoning?.[0] ?? "", model: config.models.reasoning[0] ?? "",
})} })}
</p> </p>
</div> </div>

View File

@@ -44,29 +44,70 @@ export function useReplayMetadata() {
return { title, isLoading, hasError: error }; return { title, isLoading, hasError: error };
} }
const DEFAULT_CONFIG: DeerFlowConfig = {
rag: { provider: "" },
models: { basic: [], reasoning: [] },
};
export function useConfig(): { export function useConfig(): {
config: DeerFlowConfig | null; config: DeerFlowConfig;
loading: boolean; loading: boolean;
} { } {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [config, setConfig] = useState<DeerFlowConfig | null>(null); const [config, setConfig] = useState<DeerFlowConfig>(DEFAULT_CONFIG);
useEffect(() => { useEffect(() => {
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY) { if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY) {
setLoading(false); setLoading(false);
return; return;
} }
fetch(resolveServiceURL("./config"))
.then((res) => res.json()) const fetchConfigWithRetry = async () => {
.then((config) => { const maxRetries = 2;
setConfig(config); let lastError: Error | null = null;
setLoading(false);
}) for (let attempt = 0; attempt <= maxRetries; attempt++) {
.catch((err) => { try {
console.error("Failed to fetch config", err); const res = await fetch(resolveServiceURL("./config"), {
setConfig(null); signal: AbortSignal.timeout(5000), // 5 second timeout
setLoading(false); });
});
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 }; return { config, loading };