mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-14 11:25:59 +00:00
* fix(frontend): keep workspace interactive when SSR auth probe cannot reach gateway (#3493) When the SSR auth probe at /api/v1/auth/me times out or fails, the workspace layout used to render a static fallback page without AuthProvider or QueryClientProvider, making logout and every other interaction non-functional until the gateway recovered. Render the normal WorkspaceContent in 'gateway_unavailable' mode instead, surfacing a polite offline banner that re-probes the gateway in the background and hides itself the moment refreshUser() returns an authenticated user. The probe is reentrancy-guarded so a slow gateway cannot pile up parallel /auth/me requests. Closes #3493 * fix(workspace): silent probe in offline banner to avoid /login redirect during gateway recovery (#3493) The banner previously delegated retry probes to AuthProvider.refreshUser(), which treats any 401 from /api/v1/auth/me as 'session expired' and force-redirects to /login. During gateway recovery, the first few requests may transiently return 401 before the gateway is fully ready, which would incorrectly kick the user out — defeating the purpose of the offline banner. Now the banner silently fetches /api/v1/auth/me itself and only delegates to refreshUser() on 200 OK. Non-200 responses (401 / 5xx / network) are swallowed and retried on the next interval tick, ensuring the user stays logged in across short gateway outages. Verified in Docker: - docker pause deer-flow-gateway → banner appears, page interactive - docker unpause deer-flow-gateway → banner auto-disappears within 10s, user remains on /workspace/chats/new with full session restored - All 117 unit tests pass * fix(workspace): fix banner polling leak and persistent 401 handling (#3493) - Stop polling immediately after user recovery: add user to effect dependencies, cleanup interval when user !== null - Handle persistent 401: trigger login redirect after 3 consecutive unauthorized responses - Extract decision logic to pure helper, add 8 unit tests covering all critical paths * fix(workspace): address CR feedback on gateway offline recovery (#3493) - gateway-offline-banner-helpers: decrement (not reset) auth-failure streak on transient outcomes so a flapping gateway (401 alternating with 5xx) still converges on session-expired - gateway-offline-banner: reuse probe response body to apply user directly via new AuthProvider.applyUser, halving the recovery burst against an already-struggling gateway - gateway-offline-banner: extract classifyProbe into helpers for unit testability; log probe failures via console.warn instead of swallowing - gateway-offline-fallback: new shared component used by both workspace and (auth) layouts so auth pages recover the same way the workspace does, fixing the lockup where bare static HTML had no AuthProvider - AuthProvider.logout: fall back to hard navigation when the gateway logout fetch fails, matching legacy form-POST behaviour and avoiding stale client state during outage - tests: extend gateway-offline-banner-helpers.test with flapping convergence and classifyProbe branch coverage (19 cases total)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { type ReactNode } from "react";
|
||||
|
||||
import { GatewayOfflineFallback } from "@/components/workspace/gateway-offline-fallback";
|
||||
import { AuthProvider } from "@/core/auth/AuthProvider";
|
||||
import { getServerSideUser } from "@/core/auth/server";
|
||||
import { assertNever } from "@/core/auth/types";
|
||||
@@ -25,18 +25,17 @@ export default async function AuthLayout({
|
||||
case "unauthenticated":
|
||||
return <AuthProvider initialUser={null}>{children}</AuthProvider>;
|
||||
case "gateway_unavailable":
|
||||
// Auth pages have no banner of their own, so render one here. The
|
||||
// fallback's AuthProvider replaces the bare-HTML branch that
|
||||
// previously locked users out without any logout/retry capability.
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center gap-4">
|
||||
<p className="text-muted-foreground">
|
||||
Service temporarily unavailable.
|
||||
</p>
|
||||
<Link
|
||||
href="/login"
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-md px-4 py-2 text-sm"
|
||||
>
|
||||
Retry
|
||||
</Link>
|
||||
</div>
|
||||
<GatewayOfflineFallback renderBanner>
|
||||
<div className="flex h-screen flex-col items-center justify-center gap-4">
|
||||
<p className="text-muted-foreground">
|
||||
Service temporarily unavailable.
|
||||
</p>
|
||||
</div>
|
||||
</GatewayOfflineFallback>
|
||||
);
|
||||
case "config_error":
|
||||
throw new Error(result.message);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { GatewayOfflineFallback } from "@/components/workspace/gateway-offline-fallback";
|
||||
import { AuthProvider } from "@/core/auth/AuthProvider";
|
||||
import { getServerSideUser } from "@/core/auth/server";
|
||||
import { assertNever } from "@/core/auth/types";
|
||||
@@ -28,31 +28,13 @@ export default async function WorkspaceLayout({
|
||||
case "unauthenticated":
|
||||
redirect("/login");
|
||||
case "gateway_unavailable":
|
||||
// GatewayOfflineFallback supplies the AuthProvider; WorkspaceContent
|
||||
// already mounts the banner inside its sidebar layout, so renderBanner
|
||||
// stays false here to avoid double-mounting.
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center gap-4">
|
||||
<p className="text-muted-foreground">
|
||||
Service temporarily unavailable.
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
The backend may be restarting. Please wait a moment and try again.
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Link
|
||||
href="/workspace"
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-md px-4 py-2 text-sm"
|
||||
>
|
||||
Retry
|
||||
</Link>
|
||||
<form action="/api/v1/auth/logout" method="post">
|
||||
<button
|
||||
type="submit"
|
||||
className="text-muted-foreground hover:bg-muted rounded-md border px-4 py-2 text-sm"
|
||||
>
|
||||
Logout & Reset
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<GatewayOfflineFallback>
|
||||
<WorkspaceContent gatewayUnavailable>{children}</WorkspaceContent>
|
||||
</GatewayOfflineFallback>
|
||||
);
|
||||
case "config_error":
|
||||
throw new Error(result.message);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Toaster } from "sonner";
|
||||
import { QueryClientProvider } from "@/components/query-client-provider";
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { CommandPalette } from "@/components/workspace/command-palette";
|
||||
import { GatewayOfflineBanner } from "@/components/workspace/gateway-offline-banner";
|
||||
import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar";
|
||||
|
||||
function parseSidebarOpenCookie(
|
||||
@@ -16,7 +17,11 @@ function parseSidebarOpenCookie(
|
||||
|
||||
export async function WorkspaceContent({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
gatewayUnavailable = false,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
gatewayUnavailable?: boolean;
|
||||
}>) {
|
||||
const cookieStore = await cookies();
|
||||
const initialSidebarOpen = parseSidebarOpenCookie(
|
||||
cookieStore.get("sidebar_state")?.value,
|
||||
@@ -26,7 +31,10 @@ export async function WorkspaceContent({
|
||||
<QueryClientProvider>
|
||||
<SidebarProvider className="h-screen" defaultOpen={initialSidebarOpen}>
|
||||
<WorkspaceSidebar />
|
||||
<SidebarInset className="min-w-0">{children}</SidebarInset>
|
||||
<SidebarInset className="min-w-0">
|
||||
<GatewayOfflineBanner gatewayUnavailable={gatewayUnavailable} />
|
||||
{children}
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
<CommandPalette />
|
||||
<Toaster position="top-center" />
|
||||
|
||||
Reference in New Issue
Block a user