Skip to content

fix: SSE auth and stale data when switching remote nodes (#1708)#1714

Merged
yottahmd merged 7 commits intomainfrom
1708-sse-remote-node
Mar 2, 2026
Merged

fix: SSE auth and stale data when switching remote nodes (#1708)#1714
yottahmd merged 7 commits intomainfrom
1708-sse-remote-node

Conversation

@yottahmd
Copy link
Copy Markdown
Collaborator

@yottahmd yottahmd commented Mar 2, 2026

Part of #1708

Summary by CodeRabbit

  • New Features

    • Tokens can now be provided as query parameters and are automatically converted to authentication headers.
  • Bug Fixes

    • Fixed stale data displayed when switching between remote nodes; caches now properly clear on node selection.
    • Improved data loading behavior with enhanced polling fallback to prevent empty states during initial loads and reconnections.
  • Improvements

    • Refined authentication handling for streaming endpoints.
    • Enhanced cache invalidation logic for more consistent data display across remote node switches.

yottahmd added 5 commits March 2, 2026 20:17
Move SSE routes behind the standard auth middleware instead of
maintaining parallel inline auth logic. Add QueryTokenMiddleware to
bridge browser EventSource (which cannot set custom headers) with the
Authorization header-based auth middleware. Stop forwarding the local
JWT token in proxy URLs to remote nodes since node.ApplyAuth() already
sets the correct remote credentials via the Authorization header.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 2, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request refactors authentication handling from per-handler logic to middleware, introducing QueryTokenMiddleware to convert token query parameters to Authorization headers. Concurrently, it introduces a new data caching pattern on the frontend via useLastValidData hook and adjusts polling logic across multiple components to account for SSE data availability.

Changes

Cohort / File(s) Summary
Authentication Middleware
internal/service/frontend/auth/middleware.go, internal/service/frontend/auth/middleware_test.go
Added QueryTokenMiddleware factory that promotes token query parameters to Authorization Bearer headers when no Authorization header exists. Includes comprehensive tests covering header preference, empty tokens, and pass-through scenarios.
SSE Handler Refactoring
internal/service/frontend/sse/handler.go, internal/service/frontend/sse/handler_test.go
Removed authService field and authentication logic from Handler. NewHandler signature changed from three parameters (hub, resolver, authService) to two (hub, resolver). Authentication now handled by middleware instead of per-handler validation.
Server Authentication Refactoring
internal/service/frontend/server.go
Introduced buildStreamAuthOptions(realm string) to configure streaming endpoint authentication. Refactored SSE routing to use single r.Route block with authentication middleware stack (QueryTokenMiddleware, ClientIPMiddleware, stream-auth). Updated buildAgentAuthMiddleware to compose middlewares and rely on QueryTokenMiddleware for token handling.
SSE Proxy Changes
internal/service/frontend/sse/proxy.go, internal/service/frontend/sse/proxy_test.go
Removed token parameter from buildRemoteURL function signature. Token-based authentication removed from URL construction; authentication now handled via Authorization header through node.ApplyAuth instead of query parameters.
Frontend Caching Hook
ui/src/hooks/useLastValidData.ts
New hook that caches last non-null data value using refs. Resets cache synchronously when resetKey changes. Returns latest non-null data or cached fallback value.
Frontend SSE Connection Reset
ui/src/hooks/useSSE.ts
Added synchronous state reset by computing connectionKey from endpoint, remoteNode, config.apiURL, and enabled status. Resets INITIAL_STATE when connection parameters change instead of relying on useEffect dependency handling.
Frontend Cache Invalidation & Display Data Refactoring
ui/src/App.tsx, ui/src/features/dag-runs/components/dag-run-details/DAGRunDetailsModal.tsx, ui/src/features/dag-runs/components/dag-run-details/DAGRunDetailsPanel.tsx, ui/src/features/dags/components/dag-details/DAGDetailsModal.tsx, ui/src/features/dags/components/dag-details/DAGDetailsPanel.tsx, ui/src/pages/dag-runs/index.tsx, ui/src/pages/dags/index.tsx, ui/src/pages/index.tsx
Added globalMutate call in App.tsx to clear SWR cache on remote node switch. Replaced keepPreviousData flag with useLastValidData hook across multiple DAG/DAG-run components. Refined polling conditions to trigger when SSE data is absent, enabling fallback polling during initial load.
Frontend Content Editor Context Isolation
ui/src/features/dags/components/dag-editor/DAGSpec.tsx, ui/src/pages/docs/components/DocEditor.tsx
Updated useContentEditor key from single identifier (fileName, docPath) to composite key including remoteNode (${fileName}:${remoteNode}, ${docPath}:${remoteNode}), ensuring per-remote-node isolation of editor state and conflict resolution.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The review requires understanding interconnected architectural changes: migration of authentication from handler to middleware layer, removal of token from URL in favor of headers, introduction of a new frontend data caching pattern, and polling logic adjustments across multiple components. The heterogeneity of backend refactoring and frontend pattern introduction, combined with multiple touch points across components, demands careful verification of control flow changes and state management implications.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main changes: fixing SSE authentication handling and stale data issues when switching remote nodes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 1708-sse-remote-node

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
internal/service/frontend/auth/middleware.go (1)

47-53: Strip token from the cloned query after promoting it to Authorization.

Right now the token is copied into the header but still left in RawQuery. Clearing it reduces accidental propagation in downstream logs/middleware/proxies.

Suggested patch
 			token := r.URL.Query().Get("token")
 			if token != "" {
 				r2 := r.Clone(r.Context())
+				q := r2.URL.Query()
+				q.Del("token")
+				r2.URL.RawQuery = q.Encode()
 				r2.Header.Set("Authorization", "Bearer "+token)
 				next.ServeHTTP(w, r2)
 				return
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/service/frontend/auth/middleware.go` around lines 47 - 53, The
middleware copies the URL token into the Authorization header (token, r.Clone,
r2.Header.Set("Authorization", ...)) but leaves it in the cloned request's
RawQuery, risking accidental leakage; after promoting the token set
Authorization on r2 and remove it from r2.URL.RawQuery (or clear the token param
from r2.URL.Query and re-encode) before calling next.ServeHTTP so downstream
handlers/middleware/proxies/logs don't see the token in the query string.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/service/frontend/server.go`:
- Around line 1039-1043: The current branch in server.go that checks
authCfg.Mode == config.AuthModeBasic returns auth.Options{Realm: realm} which
leaves no auth methods enabled so middleware (auth.Options) treats requests as
unauthenticated; update that return to enable Basic auth by populating
auth.Options with the Basic auth method/validator (e.g., set the Methods/Basic
field or attach the Basic credential validator used elsewhere) so the middleware
enforces Basic authentication for authCfg.Mode == config.AuthModeBasic instead
of allowing a pass-through.

In `@ui/src/features/dag-runs/components/dag-run-details/DAGRunDetailsModal.tsx`:
- Around line 41-45: The code references remoteNode when initializing
prevRemoteNodeRef (useRef(remoteNode)) before remoteNode is declared, causing a
TDZ crash; move the remoteNode declaration so it appears immediately after the
isVisible state initialization (so remoteNode exists before calling useRef) and
remove the later duplicate declaration around line 71, keeping the single
remoteNode definition and leaving prevRemoteNodeRef and previousDataRef logic
intact.

In `@ui/src/pages/dags/index.tsx`:
- Around line 314-315: The DAGTable is being fed from dagFiles (derived directly
from data) which can go empty during transient refetches even though the cached
displayData from useLastValidData(remoteNode) is available; update the DAGTable
prop so it uses displayData (or dagFiles derived from displayData) instead of
dagFiles derived from raw data, ensuring components like DAGTable render stale
data while fetching; look for useLastValidData, displayData, dagFiles, data,
remoteNode and change the source passed into DAGTable accordingly so stale
cached displayData is used during refetch.

---

Nitpick comments:
In `@internal/service/frontend/auth/middleware.go`:
- Around line 47-53: The middleware copies the URL token into the Authorization
header (token, r.Clone, r2.Header.Set("Authorization", ...)) but leaves it in
the cloned request's RawQuery, risking accidental leakage; after promoting the
token set Authorization on r2 and remove it from r2.URL.RawQuery (or clear the
token param from r2.URL.Query and re-encode) before calling next.ServeHTTP so
downstream handlers/middleware/proxies/logs don't see the token in the query
string.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 19aada7 and c1ffc85.

📒 Files selected for processing (21)
  • internal/service/frontend/auth/middleware.go
  • internal/service/frontend/auth/middleware_test.go
  • internal/service/frontend/server.go
  • internal/service/frontend/sse/handler.go
  • internal/service/frontend/sse/handler_test.go
  • internal/service/frontend/sse/proxy.go
  • internal/service/frontend/sse/proxy_test.go
  • ui/src/App.tsx
  • ui/src/features/dag-runs/components/dag-run-details/DAGRunDetailsModal.tsx
  • ui/src/features/dag-runs/components/dag-run-details/DAGRunDetailsPanel.tsx
  • ui/src/features/dags/components/dag-details/DAGDetailsModal.tsx
  • ui/src/features/dags/components/dag-details/DAGDetailsPanel.tsx
  • ui/src/features/dags/components/dag-editor/DAGSpec.tsx
  • ui/src/features/dags/components/dag-execution/DAGExecutionHistory.tsx
  • ui/src/hooks/useLastValidData.ts
  • ui/src/hooks/useSSE.ts
  • ui/src/pages/dag-runs/index.tsx
  • ui/src/pages/dags/dag/index.tsx
  • ui/src/pages/dags/index.tsx
  • ui/src/pages/docs/components/DocEditor.tsx
  • ui/src/pages/index.tsx

Comment thread internal/service/frontend/server.go Outdated
Comment on lines 1039 to 1043
// Basic auth mode: EventSource API cannot send Basic auth headers.
// Skip auth to match current behavior (pre-existing security gap).
if authCfg.Mode == config.AuthModeBasic {
return auth.Options{Realm: realm}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Basic mode currently bypasses auth for stream endpoints.

On Line 1042, returning only Realm means no auth methods are enabled. In internal/service/frontend/auth/middleware.go (Line 120), that causes a full pass-through.
Result: /api/v1/events/* and the agent stream route can be accessed without auth in basic mode.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/service/frontend/server.go` around lines 1039 - 1043, The current
branch in server.go that checks authCfg.Mode == config.AuthModeBasic returns
auth.Options{Realm: realm} which leaves no auth methods enabled so middleware
(auth.Options) treats requests as unauthenticated; update that return to enable
Basic auth by populating auth.Options with the Basic auth method/validator
(e.g., set the Methods/Basic field or attach the Basic credential validator used
elsewhere) so the middleware enforces Basic authentication for authCfg.Mode ==
config.AuthModeBasic instead of allowing a pass-through.

Comment on lines +41 to +45
const prevRemoteNodeRef = useRef(remoteNode);
if (prevRemoteNodeRef.current !== remoteNode) {
prevRemoteNodeRef.current = remoteNode;
previousDataRef.current = null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd '^DAGRunDetailsModal\.tsx$'

Repository: dagu-org/dagu

Length of output: 131


🏁 Script executed:

cat -n ui/src/features/dag-runs/components/dag-run-details/DAGRunDetailsModal.tsx | head -90

Repository: dagu-org/dagu

Length of output: 3467


Fix TDZ crash: remoteNode must be declared before first use.

remoteNode is referenced on line 41 (via useRef(remoteNode)) before its declaration on line 71. This will throw a ReferenceError at runtime. Move the declaration to line 38, immediately after isVisible state initialization:

  const [shouldRender, setShouldRender] = useState(isOpen);
  const [isVisible, setIsVisible] = useState(false);
+ const remoteNode = appBarContext.selectedRemoteNode || 'local';
  const previousDataRef = useRef<PreviousData | null>(null);
  const prevRemoteNodeRef = useRef(remoteNode);
  if (prevRemoteNodeRef.current !== remoteNode) {
    prevRemoteNodeRef.current = remoteNode;
    previousDataRef.current = null;
  }

Then remove the duplicate declaration from line 71.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const prevRemoteNodeRef = useRef(remoteNode);
if (prevRemoteNodeRef.current !== remoteNode) {
prevRemoteNodeRef.current = remoteNode;
previousDataRef.current = null;
}
const [shouldRender, setShouldRender] = useState(isOpen);
const [isVisible, setIsVisible] = useState(false);
const remoteNode = appBarContext.selectedRemoteNode || 'local';
const previousDataRef = useRef<PreviousData | null>(null);
const prevRemoteNodeRef = useRef(remoteNode);
if (prevRemoteNodeRef.current !== remoteNode) {
prevRemoteNodeRef.current = remoteNode;
previousDataRef.current = null;
}
🧰 Tools
🪛 Biome (2.4.4)

[error] 41-41: This variable is used before its declaration.

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 42-42: This variable is used before its declaration.

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 43-43: This variable is used before its declaration.

(lint/correctness/noInvalidUseBeforeDeclaration)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/features/dag-runs/components/dag-run-details/DAGRunDetailsModal.tsx`
around lines 41 - 45, The code references remoteNode when initializing
prevRemoteNodeRef (useRef(remoteNode)) before remoteNode is declared, causing a
TDZ crash; move the remoteNode declaration so it appears immediately after the
isVisible state initialization (so remoteNode exists before calling useRef) and
remove the later duplicate declaration around line 71, keeping the single
remoteNode definition and leaving prevRemoteNodeRef and previousDataRef logic
intact.

Comment on lines +314 to 315
const displayData = useLastValidData(data ?? null, remoteNode);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use cached displayData for table rows; current prop can still blank the list.

Line 329 still feeds DAGTable from dagFiles (derived from non-cached data), so during transient refetch states the table can render empty even when displayData is available.

💡 Proposed fix
-          <DAGTable
-            dags={isLoading && !displayData ? [] : dagFiles}
+          <DAGTable
+            dags={displayData.dags || []}
             group={group}
             refreshFn={refreshFn}
             searchText={searchText}

Based on learnings: Applies to ui/**/*.{ts,tsx} : NEVER use full-page loading overlays or LoadingIndicator components that hide content; instead show stale data while fetching updates.

Also applies to: 329-329

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/pages/dags/index.tsx` around lines 314 - 315, The DAGTable is being
fed from dagFiles (derived directly from data) which can go empty during
transient refetches even though the cached displayData from
useLastValidData(remoteNode) is available; update the DAGTable prop so it uses
displayData (or dagFiles derived from displayData) instead of dagFiles derived
from raw data, ensuring components like DAGTable render stale data while
fetching; look for useLastValidData, displayData, dagFiles, data, remoteNode and
change the source passed into DAGTable accordingly so stale cached displayData
is used during refetch.

@yottahmd yottahmd merged commit abc8974 into main Mar 2, 2026
6 checks passed
@yottahmd yottahmd deleted the 1708-sse-remote-node branch March 2, 2026 13:57
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 2, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.43%. Comparing base (e8673de) to head (0b3546b).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1714      +/-   ##
==========================================
+ Coverage   69.38%   69.43%   +0.04%     
==========================================
  Files         396      396              
  Lines       43713    43713              
==========================================
+ Hits        30332    30350      +18     
+ Misses      10924    10908      -16     
+ Partials     2457     2455       -2     

see 8 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 19aada7...0b3546b. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant