Skip to content

[BUG] GUI dashboard intermittently flips to "offline / cannot connect to proxy" with 401 on /api/* while proxy health is OK #3279

Closed

Description

@Arkos123

Client or integration

OpenCodex dashboard

Area

Dashboard

Summary

Right after opening the dashboard it shows "online", but within seconds
it flips back to "offline". Reproduction is not fully stable — sometimes it stays online
for a long time with no issue.

Image Image

I check it with AI (for your reference, may not be correct):

Proxy is healthy (measured)

GET /healthz -> 200 {"status":"ok","service":"opencodex","version":"2.40.0","uptime":3063,"pid":44740,"port":10100}
GET /        -> 200 (GUI served, session meta injected)

What we found

  1. GET / injects a fresh loopback GUI session token into the HTML meta
    (opencodex-session-token, -csrf, -origin, -server-origin). Server-side this is
    issueGuiSession() with LOOPBACK_GUI_SESSION_TTL_MS = 5 * 60_000 (gui-session.ts).
  2. The freshly minted token works:

curl -H "Authorization: Bearer $TOKEN"
-H "x-opencodex-gui-origin: http://localhost:10100"
http://localhost:10100/api/system/health
-> 200 {"status":"ok",...}

 But the same token without x-opencodex-gui-origin returns:

 401 {"error":"opencodex admin token required"} (management-auth.ts).
  1. The GUI frontend only installs its window.fetch wrapper (which attaches the session
    token) when Ib() validates that the meta browser origin === window.location.origin
    AND meta server origin === target.serverOrigin; otherwise it clears the session
    (a.session = Ab()), and every subsequent /api/* request goes out without a token
    -> 401 -> dashboard shows offline.

  2. Reproduction of the host mismatch:

  3. After a 401 the frontend tries to re-mint via GET /opencodex-session (Xb/Qb),
    and on failure sets promptCancelled permanently, so the dashboard stays offline until
    the page is reloaded. This explains the flakiness: whether the retry wins or not depends
    on timing/which host the request went out on.

Expected

  • Loopback GUI auth should not depend on the browser host spelling being identical to the
    one at mint time (localhost / 127.0.0.1 / ::1 are all loopback), or
  • The 401 -> re-mint path should converge and never leave the dashboard stuck offline, and
  • A single page load should never require a manual ocx start when the proxy is healthy.

Reproduction

  1. Start ocx and open the dashboard, wait for a few seconds.
  2. The dashboard turns from "online" to "offline"
    (Reproduction is not fully stable. Sometimes it stays online for a long time with no issue.)

Version

2.40.0

Operating system

Windows 10

Provider and model

No response

Logs or error output

Screenshots and supporting files

No response

Redacted configuration

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. github-actions commented on Sep 2, 2026

    @github-actions
    Contributor

    Issue reopened

    The report now contains the information required by the automated check. Thanks for updating it.

  2. changed the title [-]GUI dashboard intermittently flips to "offline / cannot connect to proxy" with 401 on /api/* while proxy health is OK[/-] [+][BUG] GUI dashboard intermittently flips to "offline / cannot connect to proxy" with 401 on /api/* while proxy health is OK[/+] on Sep 2, 2026
  3. lidge-jun commented on Sep 2, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 58 / 80

    설명
    이 이슈는 OpenCodex 대시보드가 잠깐 "온라인"으로 보이다가, 프록시(/healthz)는 멀쩡한데 /api/*만 401을 받아 "오프라인 / 프록시에 연결할 수 없음"으로 뒤집히는 Windows 증상이다. 작성자 AI 분석과 현재 dev(bc8ea072d, 방금 #3273 번들 effort table이 들어온 상태) 코드를 같이 보면, 루프백인데도 호스트 철자(localhost vs 127.0.0.1)를 세션에 그대로 박아 두고, 프론트가 그 철자와 window.location.origin이 조금이라도 다르면 세션을 버리고 fetch 래퍼에 토큰을 안 실어 보내는 구조가 맞다.

    서버 쪽: src/server/gui-session.ts의 LOOPBACK_GUI_SESSION_TTL_MS = 5 * 60_000으로 루프백 GUI 세션을 만들고, issueGuiSession은 managementRequestOrigin(req, config) 결과를 serverOrigin/browserOrigin에 같이 넣는다. src/server/auth-cors.ts의 managementRequestOrigin은 루프백일 때 Host 헤더 철자를 정규화하지 않고 new URL(protocol + '//' + host).origin을 그대로 쓴다. 그래서 http://localhost:10100으로 받은 페이지와 http://127.0.0.1:10100으로 받은 페이지는 서로 다른 origin 세션이 된다. 이후 admitGuiSession은 x-opencodex-gui-origin과 Origin이 세션에 저장된 값과 정확히 같아야 하고, 아니면 browser-origin으로 거절한다.

    프론트 쪽: gui/src/api.ts storeSession은 browserOrigin !== window.location.origin이거나 serverOrigin !== state.target.serverOrigin이면 세션을 비운다. 401 뒤에는 resolveTokenAfter401 → reBootstrapSessionToken(/opencodex-session)을 시도하고, 실패하면 관리자 토큰 프롬프트로 넘어가며, 그것도 거절되면 promptCancelled = true로 고정되어 새로고침 전까지 복구가 멈춘다. 이게 "가끔은 오래 온라인, 가끔은 몇 초 만에 오프라인 고정"이라는 불안정 재현과 잘 맞는다.

    진입 경로 불일치도 코드에 있다. src/cli/dispatch.ts는 GUI를 열 때 127.0.0.1이면 일부러 localhost로 바꿔 열고, src/cli/status.ts 대시보드 URL도 localhost를 쓴다. 반면 Windows 트레이 src/tray/windows-tray.ps1은 기본 host를 127.0.0.1로 두고 origin을 그걸로 만든다. 트레이로 연 탭과 CLI/북마크로 연 탭이 같은 포트인데 origin 철자만 다르면, 이슈에 적힌 "같은 브라우저에서 localhost는 온라인 / 127.0.0.1은 오프라인" 재현이 그대로 나온다. 버전은 2.40.0 보고이지만, 해당 경로들은 현재 dev HEAD에도 그대로 있어 2.41.0 라인에서도 같은 클래스다. Cursor Private Inference effort-table 트레인(#3272/#3273 및 후속 #3275–#3278)과는 축이 다른 GUI 인증 버그다.

    라인 62 - src/server/gui-session.ts LOOPBACK_GUI_SESSION_TTL_MS — 5분 TTL 자체는 문제가 아니라, 만료·재발급이 host 철자 불일치와 겹치면 재민트가 실패하기 쉬운 배경이다
    src/server/auth-cors.ts managementRequestOrigin — 루프백인데 Host 철자(localhost/127.0.0.1/::1)를 정규화하지 않아 세션 origin이 갈라진다
    src/server/gui-session.ts admitGuiSession / x-opencodex-gui-origin — 저장된 browserOrigin과 한 글자라도 다르면 401; 루프백 동치 비교가 없다
    gui/src/api.ts storeSession — meta origin과 window.location.origin 불일치 시 세션을 즉시 비워 fetch에 토큰이 안 실림
    gui/src/api.ts resolveTokenAfter401 / promptCancelled — 재민트·프롬프트 실패 후 영구 취소 플래그로 새로고침 전까지 오프라인에 고정됨
    src/tray/windows-tray.ps1 기본 host 127.0.0.1 vs src/cli/dispatch.ts의 localhost 리맵 — 같은 머신에서 진입 URL 철자가 갈라지는 실사용 경로

    메인테이너의 판단이 필요한 지점

    • 루프백을 하나의 canonical origin으로 묶을지(localhost로 통일 vs 127.0.0.1로 통일 vs 세션 검증만 루프백 동치 허용), 보안상 Host 철자 엄격 유지를 우선할지
    • 트레이/ocx open/status 대시보드 URL을 한쪽으로 맞추는 제품 결정을 서버 정규화와 같이 할지, URL만 먼저 맞출지
    • 401 후 promptCancelled 고정을 완화할지(루프백에서는 조용히 재시도·메타 재주입), 아니면 사용자에게 "호스트 철자가 다름"을 명시할지
    • 이 이슈를 단독 PR로 잡을지, 기존 remote-hub/GUI 세션 스택·feat(server): persistent origin-scoped dashboard sessions via POST /api/auth/session #3080 persistent dashboard session 쪽과 묶을지

    너의 추천
    dev에 작은 버그픽스 PR을 열어 (1) 루프백 세션 발급·검증에서 localhost/127.0.0.1/::1를 동치로 보거나 한 철자로 canonical 하고, (2) Windows 트레이 open URL을 CLI와 같이 localhost로 맞추며, (3) 루프백 401 경로에서 promptCancelled로 영구 멈추지 않게 재민트를 한두 번 더 허용하는 쪽을 권한다. types.ts/config.ts 스플릿과 무관하니 닫지 말고 열어 두고, Cursor effort-table 후속(#3275–#3278)과는 병렬로 처리하면 된다. 재현 확인은 같은 브라우저에서 http://localhost:PORT/와 http://127.0.0.1:PORT/를 번갈아 열고 /api/system/health 401 여부를 보면 된다.

    이 댓글은 grok-bot이 작성했습니다

  4. Ingwannu commented on Sep 2, 2026

    @Ingwannu
    Owner

    Thanks for the detailed report. I confirmed that the dashboard becoming offline while /healthz remains healthy is a real symptom worth investigating, but one part of the proposed root cause does not match the current code, so I do not want to weaken the GUI origin boundary prematurely.

    What the current dev code actually does:

    • The Windows tray does not open http://127.0.0.1:PORT directly. Its Open Dashboard action runs ocx gui, and that CLI intentionally maps a 127.0.0.1 loopback bind to http://localhost:PORT before opening the browser.
    • A page opened directly on 127.0.0.1 should receive a newly minted session whose browser and server origins are both http://127.0.0.1:PORT. It does not reuse the localhost session from another page.
    • A temporary bootstrap/network failure does not permanently set promptCancelled. That flag is set only after the bootstrap returns a client-side refusal and the admin-token prompt is then cancelled.

    For that reason I am keeping the issue open as needs-info instead of making localhost, 127.0.0.1, and ::1 interchangeable inside session authorization. Those are different browser origins, and silently treating them as the same would weaken an intentional security boundary.

    Please capture the following from the failing tab in DevTools → Network, with all token and cookie values removed:

    1. The exact address shown in the browser bar when it flips offline.
    2. The status code for GET /opencodex-session immediately after the first /api/* 401.
    3. The values of the four session meta origins only: browser origin and server origin are enough; do not post the token or CSRF value.
    4. For the first failing /api/system/health request, whether X-OpenCodex-API-Key and X-OpenCodex-GUI-Origin are present. Redact the key value completely.
    5. Any browser console error at the same timestamp and whether a reverse proxy, browser extension, or saved/bookmarked URL is involved.

    With those five points we can distinguish session expiry/re-bootstrap failure, a stale frontend target, and an actual host-origin mismatch and then make a focused fix without reducing the management-session protections.

  5. added
    needs-infoWaiting on reporter for a concrete spec or reproduction
    on Sep 2, 2026
  6. lidge-jun commented on Sep 2, 2026

    @lidge-jun
    Owner

    Traced this through the session and dashboard auth paths. I could not find a mechanism that would invalidate a still-valid session intermittently, which is worth stating plainly rather than shipping a guess on an auth surface.

    What the code does: an unexpired session 401s only when it is absent/evicted, or when its exact server-origin, browser-origin, or CSRF binding fails (src/server/gui-session.ts:417). Loopback sessions otherwise expire deterministically after five minutes (src/server/gui-session.ts:62, :428). Management auth state initializes once per server process, so admin-token rotation is not in play (src/server/index.ts:653). On the GUI side the auth wrapper is installed before React renders and 401 recovery is single-flight, so a stale response cannot clear a newer token (gui/src/App.tsx:42, gui/src/api.ts:247, :299).

    One alternative reading of the symptom: the initial "online" may be cached health rather than proof of a working session, with the first authenticated poll being the first real test (gui/src/pages/use-dashboard-data.ts:97, :220, :307). That would look like a flip from online to offline without any session having been invalidated.

    On #3080: it is not the same fix. It persists a 12-hour opaque session for remote-dashboard refreshes, does not change the ordinary injected loopback-session path, and cannot preserve a process-local session across a proxy restart.

    To move this forward, from a failing moment: the exact failing request URL, the Host and Origin headers, whether an Authorization header was present, and the immediate result of GET /opencodex-session right after the 401. With that I can write a test that reproduces it; without it, the only available "fix" is weakening loopback-origin equality, which I am not willing to do speculatively.

  7. lidge-jun commented on Sep 3, 2026

    @lidge-jun
    Owner

    @Arkos123 — following up during a backlog review. This is not closed; it needs one piece of information from you to move.

    Where this stands. Sessions enforce exact origin binding at src/server/gui-session.ts and 401 recovery is single-flight at gui/src/api.ts, so the most likely cause is an origin mismatch specific to your setup.

    What would unblock it. Please add the exact failing request URL and its response headers, plus the immediate result of /opencodex-session.

    If that arrives, this gets picked up in the next triage pass. Without it, the report cannot be distinguished from configurations that already work, so it will keep sitting here rather than being wrong on purpose.

  8. lidge-jun commented on Sep 4, 2026

    @lidge-jun
    Owner

    Following up during the 260904 backlog review. Still open and still needs-info — the AI-assisted analysis in your report is largely right about the mechanism, which is why the missing piece is narrow.

    The session token injected into the GUI HTML has a bounded lifetime, and the dashboard is expected to recover a 401 by re-fetching rather than declaring the proxy offline. An intermittent flip to "offline" while /healthz returns 200 means either the recovery did not fire, or it fired and was itself refused. Those have different fixes, and the difference is visible only in the failing request.

    What would settle it, captured at the moment the dashboard flips:

    1. The browser devtools Network entry for the failing /api/* request: the full response body (it carries an error message that names which check refused it), and the request Origin header.
    2. Whether a manual reload immediately restores "online", or whether it stays offline until the proxy is restarted.
    3. How you are reaching the dashboard — localhost versus 127.0.0.1 versus a LAN address or a reverse proxy. Sessions are bound to an exact origin, so reaching the same server through two different names is a known way to produce exactly this symptom.

    Point 3 is where I would look first. If you opened the dashboard on one origin and something later re-issued against another, the flip is expected behavior from a session-binding standpoint and the bug is that we present it as "cannot connect to proxy", which is misleading — the proxy is fine, the session is not. That would be a real defect, just a different one than "the dashboard loses the proxy."

  9. Arkos123 commented on Sep 4, 2026

    @Arkos123
    Author

    Sorry, I asked AI to investigate this issue more carefully and found that it was actually caused by a browser extension I had installed 😅.
    The extension overwrote the window.fetch authentication wrapper installed by OpenCodex.


    from AI:

    Specific findings:

    • The page origin, session browser origin, and server origin were all http://localhost:10100.

    • At 0.5 and 2.7 seconds after a refresh, window.fetch was still the authentication wrapper installed by OpenCodex during startup, located at gui/src/api.ts#L283.

    • At 6.25 seconds, window.fetch was replaced by the request-interception function from the Tweak browser extension.

    • The first batch of API requests included both X-OpenCodex-API-Key and X-OpenCodex-GUI-Origin, and all of them returned 200.

    • The polling requests sent roughly five seconds later no longer included either header. As a result, /api/system/health and the other API endpoints returned 401:

      {"error":"opencodex admin token required"}
    • No /opencodex-session request was made after the 401 responses because the OpenCodex fetch wrapper was no longer in the call chain.

    • At the same time, /healthz continued to return 200. The proxy version was 2.40.0.

    • I also verified the dashboard in an isolated browser without Chrome extensions. It remained online after waiting for 12 seconds.

    Conclusion

    This reproduction was not caused by session expiration, an origin mismatch, or /opencodex-session being rejected. Instead, a browser extension overwrote window.fetch after OpenCodex had completed its initialization.

    Therefore, I agree that this issue should not be addressed by relaxing the loopback origin validation.

    Potential improvements on the OpenCodex side could include:

    • Avoid relying on a one-time replacement of the global window.fetch, and instead have internal API calls explicitly use an authenticated fetch function, while avoiding recursion or conflicts with third-party fetch wrappers.
    • When /healthz is healthy but /api/* returns 401, display a message such as “Management session authentication failed” instead of “Cannot connect to proxy.”
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingguiDashboard, tray, settings UIneeds-infoWaiting on reporter for a concrete spec or reproduction

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions