Skip to content

Fix frontend-only backend setup and CORS handling#951

Open
neubig wants to merge 17 commits into
mainfrom
codex/frontend-backend-cors-flow
Open

Fix frontend-only backend setup and CORS handling#951
neubig wants to merge 17 commits into
mainfrom
codex/frontend-backend-cors-flow

Conversation

@neubig
Copy link
Copy Markdown
Member

@neubig neubig commented May 29, 2026

Summary

  • Make agent-server backends explicit (kind: "agent-server") while normalizing legacy stored kind: "local" values on read.
  • Split agent-server config sources by transport: same-origin backends come only from launcher config, remote backends come only from the backend manager UI, and cloud auth stays separate.
  • Stop baking launcher-only backend/session env into static builds; static/package launchers inject runtime config into index.html when they start a same-origin backend.
  • Remove VITE_BACKEND_BASE_URL; remote agent servers are added through the backend dialog.
  • Reuse the backend manager for missing, unauthorized, and likely CORS backend failures instead of a custom startup screen.
  • Stop implicitly probing /projects as a workspace parent; only user-stored workspace parents are scanned.

Launch Path Screenshots

Launch path Expected startup state Screenshot
npm run dev Vite frontend with launcher-injected same-origin backend npm run dev startup
npm run dev:frontend Vite frontend with no launcher backend; backend manager opens npm run dev:frontend startup
npm run dev:static -- --skip-build Static frontend served locally with runtime-injected same-origin backend static same-origin startup
node scripts/static-server.mjs --dir build Static frontend with no launcher backend, matching dumb static hosting static no-backend startup

The PNGs are committed under .pr/qa/ and listed in .pr/README.md.

Verification

  • npm run typecheck
  • npm test
  • npm run build
  • git diff --check
  • Whitespace-only file check against origin/main: comm -23 <(git diff --name-only origin/main...HEAD | sort) <(git diff -w --name-only origin/main...HEAD | sort)

🐳 Docker images for this PR

GHCR package: https://github.com/OpenHands/agent-canvas/pkgs/container/agent-canvas

Component Value
Image ghcr.io/openhands/agent-canvas
Architectures amd64, arm64
Agent Server ghcr.io/openhands/agent-server:1.24.0-python
Automation openhands-automation==1.0.0a5
Commit d126c4cdb9a001b7f2eedd6f3c6a5a3ee7971132

Pull (multi-arch manifest)

# Multi-arch manifest — Docker automatically pulls the correct architecture
docker pull ghcr.io/openhands/agent-canvas:sha-d126c4c

Run

docker run -it --rm \
  -p 8000:8000 \
  ghcr.io/openhands/agent-canvas:sha-d126c4c

All tags pushed for this build

ghcr.io/openhands/agent-canvas:sha-d126c4c-amd64
ghcr.io/openhands/agent-canvas:codex-frontend-backend-cors-flow-amd64
ghcr.io/openhands/agent-canvas:pr-951-amd64
ghcr.io/openhands/agent-canvas:sha-d126c4c-arm64
ghcr.io/openhands/agent-canvas:codex-frontend-backend-cors-flow-arm64
ghcr.io/openhands/agent-canvas:pr-951-arm64
ghcr.io/openhands/agent-canvas:sha-d126c4c
ghcr.io/openhands/agent-canvas:codex-frontend-backend-cors-flow
ghcr.io/openhands/agent-canvas:pr-951

About Multi-Architecture Support

  • Each tag (e.g., sha-d126c4c) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., sha-d126c4c-amd64) are also available if needed

@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agent-canvas Ready Ready Preview, Comment Jun 1, 2026 11:53am

Request Review

- avoid seeding a backend for frontend-only dev launches

- route missing or unauthorized agent server preflight to backend settings

- surface CORS failures with localized agent-server launch guidance

- keep create-conversation CORS errors to a single toast
@neubig
Copy link
Copy Markdown
Member Author

neubig commented May 30, 2026

@OpenHands /iterate

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 30, 2026

Uh oh! There was an unexpected error starting the job :(

@neubig
Copy link
Copy Markdown
Member Author

neubig commented May 30, 2026

@OpenHands /iterate

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 30, 2026

Uh oh! There was an unexpected error starting the job :(

@neubig
Copy link
Copy Markdown
Member Author

neubig commented May 30, 2026

@OpenHands /iterate

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 30, 2026

I'm on it! neubig can track my progress at all-hands.dev

….tsx conflict

Conflict in src/root.tsx: PR replaces MissingAgentServerScreen (modal-based) with
a redirect to /settings/backend + AgentServerConnectionScreen; main added links()
export for the favicon. Resolution: keep PR's redirect logic, preserve links() export.

Co-authored-by: openhands <openhands@all-hands.dev>
The PR changed readStoredBackends() to only seed the default local
backend when hasConfiguredAgentServerDefaults() is true (deployed with
env vars). In Vitest environments without VITE_BACKEND_BASE_URL, the
function returns false so no seeding occurs.

- active-backend-context.test.tsx: mock hasConfiguredAgentServerDefaults
  to return true so seeding tests still exercise the production path
- agent-server-compatibility-bundled-pin.test.ts: same mock so
  hasEffectiveLocalBackend() returns true for the no-local-backends case
- add-backend-modal.test.tsx: change toHaveLength(2) → toHaveLength(1)
  since without env defaults no default backend is seeded before the test

Co-authored-by: openhands <openhands@all-hands.dev>
Copy link
Copy Markdown
Member Author

neubig commented May 30, 2026

Addressed CI failures in 649bc8a. Two commits were pushed:

  1. Merge main → branch (18dfa33): Resolved merge conflict in src/root.tsx — kept the PR's redirect-to-/settings/backend approach while preserving the links() favicon export that main added.

  2. Fix conditional backend seeding tests (649bc8a): The PR changed readStoredBackends() to only seed the default local backend when hasConfiguredAgentServerDefaults() is true (i.e., env vars are set or it's a production build). Vitest environments have no VITE_BACKEND_BASE_URL and run in DEV mode, so the function returned false and seeding was skipped, causing three test failures:

    • active-backend-context.test.tsx: added vi.mock for hasConfiguredAgentServerDefaultstrue (production-like scenario)
    • agent-server-compatibility-bundled-pin.test.ts: same mock so hasEffectiveLocalBackend() returns true for the no-local-backends case
    • add-backend-modal.test.tsx: changed toHaveLength(2)toHaveLength(1) (no seeded default without env defaults)

This comment was created by an AI agent (OpenHands) on behalf of the PR author.

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 30, 2026

Summary of Work on PR #951

What was requested

The /iterate command was invoked — drive PR #951 to a CI-green, merge-ready state.

What was done

✅ Identified the problem

The PR branch codex/frontend-backend-cors-flow had diverged significantly from main, causing a merge conflict in src/root.tsx and making the PR CONFLICTING (unmergeable).

✅ Resolved the merge conflict

  • File: src/root.tsx
  • Resolution: Kept the PR's redirect-to-/settings/backend + AgentServerConnectionScreen approach (the PR's core feature), while preserving the links() favicon export (LinksFunction) that main added independently. Dropped MissingAgentServerScreen which the PR intentionally replaced.

✅ Fixed CI test failures (3 tests in 2 CI jobs)

The PR had changed readStoredBackends() to only seed the default local backend when hasConfiguredAgentServerDefaults() is true (i.e., VITE_BACKEND_BASE_URL is set or it's a production build). In Vitest, neither condition holds, so seeding was skipped, breaking tests that assumed it always happened:

Test File Fix Applied
__tests__/contexts/active-backend-context.test.tsx Added vi.mock of hasConfiguredAgentServerDefaults → true to simulate production deployment
__tests__/api/agent-server-compatibility-bundled-pin.test.ts Same mock so hasEffectiveLocalBackend() returns true for cloud-only backend scenario
__tests__/components/backends/add-backend-modal.test.tsx Changed toHaveLength(2) → toHaveLength(1) (no seeded default in test env)

✅ Final CI state

  • 12/12 checks passing (ubuntu ✅, windows ✅, snapshots ✅, Docker ✅, Vercel ✅)
  • Mergeable — no conflicts
  • 0 unresolved review threads

⏳ Remaining blocker

The PR is REVIEW_REQUIRED — it needs a human maintainer's approval. The automated PR-review bot workflows (pr-review-by-openhands.yml) were deleted from main before this PR was merged, so no bot review is available. This requires human action.

Changes are concise

All three commits are directly required to address the PR's state: one merge commit (necessary to resolve conflicts) and one test-fix commit (necessary to make CI pass). No extraneous changes were made.

Copy link
Copy Markdown
Contributor

all-hands-bot commented May 30, 2026

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

Copy link
Copy Markdown
Contributor

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is a well-structured PR that addresses three distinct but related pain points: the frontend-only dev workflow defaulting to a backend that may not exist, the absence of actionable CORS error guidance, and the "unauthorized" vs "unreachable" distinction for backend errors. The overall design is sound. A few issues are worth addressing.

✅ Strengths

  • CORS detection + localized guidance — Surfacing OH_ALLOW_CORS_ORIGINS instructions with translated detail strings is excellent UX for the cross-origin deployment scenario. The frontendOrigin !== backendOrigin guard prevents false positives for same-origin failures.
  • AgentServerUnavailableReason discriminated union — Properly separates "unauthorized" (401/403) from "unreachable" (network/other HTTP) failure modes, enabling AgentServerConnectionScreen to give context-specific guidance.
  • preflightAgentServerAccess two-phase check — Probing /server_info for reachability then /api/settings for auth is clean; distinguishes the two failure modes without a dedicated auth endpoint.
  • Navigate to /settings/backend instead of modal overlay — The connection screen gets its own URL, the modal-on-top-of-nothing pattern is gone, and the backend-settings.tsx redirect-to-home on successful reconnect is an elegant post-recovery UX.
  • Query key scoped to backend identityWEB_CLIENT_CONFIG_BY_BACKEND(backend) prevents stale config from surviving a backend switch.
  • Stale auto-seeded backend cleanup — Removing the empty-apiKey default-local entry when defaults are no longer configured avoids phantom backends after switching to frontend-only mode.

🔴 Issue 1 — Silent error swallowing in HomeChatLauncher

The onError callback now dismisses the loading toast without any user-facing feedback. Previously it called displayErrorToast for any error. Now, only CORS errors surfaced via the useConfig/AgentServerConnectionScreen path are visible — but that path only covers the initial config load, not conversation creation.

Any createConversation failure (network timeout, server 5xx, SDK error, non-CORS reasons) will silently dismiss the toast with no indication to the user. Consider at minimum showing a fallback for non-CORS errors, or add a comment explaining which path surfaces them.


🟡 Issue 2 — Inconsistent normalization in isAutoSeededDefaultLocalBackend

In src/api/backend-registry/storage.ts:

LEGACY_FRONTEND_ONLY_DEV_BACKEND_URL ("http://127.0.0.1:8000") is compared against an already-normalized host without itself being normalized. If normalizeHostForComparison strips the protocol prefix or trailing slashes, this branch will never match and the legacy cleanup becomes dead code.

Fix: host === normalizeHostForComparison(LEGACY_FRONTEND_ONLY_DEV_BACKEND_URL)


🟡 Issue 3 — English-only browser error strings for CORS detection

In src/utils/agent-server-cors-error.ts, FETCH_NETWORK_ERROR_FRAGMENTS contains English-language browser error messages. Browsers with non-English UI locales or future browser versions that change error wording will silently miss CORS detection. The detection degrades gracefully (no false positives given the frontendOrigin !== backendOrigin guard), but a brief comment explaining the heuristic and its known limitations would help maintainers.


🟢 Minor — Dead code in containsHtmlDocument regex

In src/api/agent-server-compatibility.ts, the regex includes &lt;!doctype and &lt;html (HTML-entity-encoded forms). SDK error messages come from raw HTTP response bodies and contain literal < characters, not &lt;. These variants appear to be dead code; removing them simplifies the regex without changing behavior.


This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

Comment thread src/components/features/home/home-chat-launcher.tsx
Comment thread src/api/backend-registry/storage.ts Outdated
Comment thread src/utils/agent-server-cors-error.ts
@neubig
Copy link
Copy Markdown
Member Author

neubig commented May 30, 2026

@OpenHands /iterate

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 30, 2026

Uh oh! There was an unexpected error starting the job :(

@neubig
Copy link
Copy Markdown
Member Author

neubig commented May 30, 2026

@OpenHands /iterate

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 30, 2026

I'm on it! neubig can track my progress at all-hands.dev

@neubig neubig force-pushed the codex/frontend-backend-cors-flow branch from 72f550f to e977698 Compare May 31, 2026 17:30
github-actions Bot added a commit that referenced this pull request May 31, 2026
github-actions Bot added a commit that referenced this pull request May 31, 2026
@github-actions
Copy link
Copy Markdown
Contributor

PR Artifacts Notice

This PR contains a .pr/ directory with PR-specific artifacts. This directory will be automatically removed when the PR is approved.

Fork PRs require manual cleanup before merging.

…d-cors-flow

# Conflicts:
#	__tests__/utils/mcp-marketplace-utils.test.ts
Copy link
Copy Markdown
Contributor

all-hands-bot commented May 31, 2026

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

Copy link
Copy Markdown
Contributor

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

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

Overview

This is a well-structured refactor with a clear goal: stop conflating "same-origin launcher-managed backend" with "any local backend", and stop baking launcher-specific config into static builds at compile time.

The key changes are:

  • kind: "local"kind: "agent-server" across the codebase, with transparent migration of stored values on read via normalizeBackendKind.
  • AgentServerTransport ("same-origin" | "remote") distinguishes whether the frontend and agent-server share an origin (static/package launchers) from a separately hosted backend added via the UI. This drives getBackendBaseUrl() to return window.location.origin for same-origin backends, eliminating hardcoded host comparisons elsewhere.
  • AgentCanvasRuntimeConfig / window.__AGENT_CANVAS_RUNTIME_CONFIG__ replaces the openhands-agent-server-config localStorage-write-on-load approach with a clean runtime injection point in index.html, meaning session keys are no longer baked into the bundle at publish time.
  • CORS error utility surfaces an actionable message when a cross-origin network failure is likely a CORS misconfiguration, with origin-mismatch guard to prevent false positives.
  • getBackendBaseUrl() centralises URL resolution so the "same-origin → window.location.origin" rule can't drift out of sync across callers.
  • Implicit /projects workspace parent removal: the dev-only heuristic that probed /projects was causing noisy 404s against remote backends; good cleanup.
  • Fallback screen simplified: MissingAgentServerScreen now renders AddBackendModal directly (no lazy ManageBackendsModal) with showCloseButton=false, forcing users to configure a backend before proceeding. The new test cases covering 401/unauthorized flows are a solid addition.

The migration path (normalizeBackendKind, normalizeAgentServerTransport) handles old stored values cleanly, and coverage for the new auth-failure root states is thorough.

A few issues worth addressing are noted below.

This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

"failed to fetch",
"load failed",
"networkerror when attempting to fetch resource",
] as const;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 Important: browser error message matching is fragile across locales and browser versions

The three strings (, , ) are hardcoded English error messages from Chromium, Safari, and Firefox respectively. They will miss CORS failures in non-English browser locales (Firefox and Safari localise messages in some versions) and in future browser releases that change the wording.

All real CORS/network failures from surface as in every spec-compliant browser. Since the origin-mismatch guard at the call site already confirms the backend is cross-origin, checking first gives a much more robust signal:

Keeping the string fallback as a secondary check preserves behaviour for SDK-wrapped errors that strip the original identity.

Comment thread scripts/static-server.mjs
@@ -417,9 +437,12 @@ async function handleStatic(req, res, dirAbs, sessionApiKey = null) {
filePath = resolve(filePath, "index.html");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 Important: property name collision is confusing

The local variable (built in ) has a property also named (the parsed JSON payload). requires double-reading and the same pattern appears a second time a few lines below, making both branches hard to scan.

Consider renaming the inner property when building the composite object in :

Then replace with throughout and . This also makes self-documenting.

Comment thread vite.config.ts
VITE_AGENT_SERVER_PROXY_TARGET: configuredProxyTarget,
VITE_USE_TLS = "false",
VITE_FRONTEND_PORT = "3001",
VITE_INSECURE_SKIP_VERIFY = "false",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 **Suggestion: silently overrides all file values, not just **

The spread order means any shell/CI variable already set takes precedence over for every key destructured here, not just the new proxy target. This is almost certainly intentional to let pass without requiring a file, but it can silently shadow developer overrides for , , etc.

A narrower alternative is to only promote the new variable from :

If the current broad-override behaviour is intentional, a short comment explaining why would help future readers avoid inadvertently reverting it.

if (typeof value !== "object" || value === null) return null;
const v = value as Partial<Backend>;
return (
const rawKind = (value as { kind?: unknown }).kind;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Suggestion: migration values / should be documented or removed once fully migrated

maps legacy values ( → , → ). If these are one-time migration aliases for values that were written by a previous version, a brief comment noting which release introduced and which release can remove them would help avoid keeping dead migration code indefinitely. If they are still being written somewhere, that write site should be updated to use the canonical values.

@neubig
Copy link
Copy Markdown
Member Author

neubig commented Jun 1, 2026

@OpenHands

Actually we have a several-step onboarding process already (select agent, backend, llm, starting prompt), so I don't think that we need to add the initial screen for onboarding a backend. OTOH, we need to make sure that the onboarding process works regardless of they way of launching. Fix this PR to rely on the onboarding process instead of adding an extra screen.

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Jun 1, 2026

Uh oh! There was an unexpected error starting the job :(

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

📸 Snapshot Test Report

Warning

Snapshot comparison step crashed (timeout, OOM, or runner error) — diff results below may be incomplete or absent.
Check the CI logs for the full error output (look for the "Run snapshot comparison" step).

✅ 4 snapshots changed — acknowledged via the update-snapshots label. New baselines will be uploaded when this PR merges.

Category Count
🔴 Changed 4
🆕 New 0
✅ Unchanged 69
Total 73
🔴 Changed snapshots (4)

backends-extended — 2 snapshots

backend-cancel-nothing-saved

Expected (main) Actual (PR) Diff
expected actual diff

backend-manage-two-listed

Expected (main) Actual (PR) Diff
expected actual diff

backends

backend-manage-modal

Expected (main) Actual (PR) Diff
expected actual diff

onboarding

onboarding-step-1-check-backend

Expected (main) Actual (PR) Diff
expected actual diff
✅ Unchanged snapshots (69)

archived-conversation

  • conversation-panel-with-archived-badges
  • conversation-view-archived
  • conversation-view-sandbox-error

automations

  • automations-delete-modal
  • automations-list-active-inactive
  • automations-no-automations
  • automations-search-no-results

backends-extended

  • backend-add-blank-disabled
  • backend-add-cloud-advanced-open
  • backend-add-cloud-no-key-disabled
  • backend-add-cloud-with-key-enabled
  • backend-add-form-partially-filled
  • backend-add-invalid-url-disabled
  • backend-add-local-ready
  • backend-add-name-only-disabled
  • backend-add-two-column-layout
  • backend-add-whitespace-host-disabled
  • backend-after-switch
  • backend-dropdown-two-backends
  • backend-edit-prefilled
  • backend-manage-after-removal
  • backend-remove-cancelled
  • backend-remove-confirmation
  • backend-switch-overlay

backends

  • backend-add-modal
  • backend-selector-open

changes-tab

  • changes-deleted-file
  • changes-diff-viewer
  • changes-empty

collapsible-thinking

  • reasoning-content-collapsed
  • reasoning-content-expanded
  • think-action-collapsed
  • think-action-expanded

mcp-page

  • mcp-custom-server-1-editor-open
  • mcp-custom-server-2-url-filled
  • mcp-custom-server-3-all-filled
  • mcp-custom-server-4-installed
  • mcp-custom-server-editor
  • mcp-empty-installed
  • mcp-search-filtered
  • mcp-slack-install-1-marketplace
  • mcp-slack-install-2-modal
  • mcp-slack-install-3-filled
  • mcp-slack-install-4-installed

onboarding

  • onboarding-step-0-choose-agent
  • onboarding-step-2-setup-llm
  • onboarding-step-3-say-hello

projects-workspace-browser

  • projects-workspace-browser

settings-page

  • add-backend-modal
  • analytics-consent-modal
  • home-screen
  • settings-app-page
  • settings-page

settings-secrets

  • secrets-add-form-filled
  • secrets-add-form
  • secrets-after-save
  • secrets-delete-confirm
  • secrets-list

settings-verification

  • condenser-settings
  • verification-settings-off
  • verification-settings-on

sidebar

  • sidebar-collapsed
  • sidebar-conversation-panel
  • sidebar-filter-menu

skills-page

  • skills-empty
  • skills-loaded
  • skills-no-match
  • skills-search-filtered
  • skills-type-filter

Generated by the Snapshot Tests workflow. This comment was created by an AI agent (OpenHands) on behalf of the repo maintainers.

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

Labels

review-this update-snapshots Intentional snapshot changes — CI diff check bypassed; new baselines uploaded on merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants