Repository navigation
[BUG] GUI dashboard intermittently flips to "offline / cannot connect to proxy" with 401 on /api/* while proxy health is OK #3279
Description
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.
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
- 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). - 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).
-
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. -
Reproduction of the host mismatch:
- Opening http://localhost:10100/ in a browser -> dashboard shows 在线 (online).
- Opening http://127.0.0.1:10100/ in the SAME browser -> dashboard shows 离线 (offline).
- The tray opens the GUI at http://127.0.0.1:10100 (opencodex-tray.ps1 defaults
host = "127.0.0.1"), so the common user path hits the failing host spelling.
-
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
- Start ocx and open the dashboard, wait for a few seconds.
- 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
github-actions commentedon Sep 2, 2026
Issue reopened
The report now contains the information required by the automated check. Thanks for updating it.
[-]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[/+]lidge-jun commentedon Sep 2, 2026
리뷰 · 우선순위 58 / 80
설명
이 이슈는 OpenCodex 대시보드가 잠깐 "온라인"으로 보이다가, 프록시(
/healthz)는 멀쩡한데/api/*만 401을 받아 "오프라인 / 프록시에 연결할 수 없음"으로 뒤집히는 Windows 증상이다. 작성자 AI 분석과 현재dev(bc8ea072d, 방금 #3273 번들 effort table이 들어온 상태) 코드를 같이 보면, 루프백인데도 호스트 철자(localhostvs127.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.tsstoreSession은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 보고이지만, 해당 경로들은 현재devHEAD에도 그대로 있어 2.41.0 라인에서도 같은 클래스다. Cursor Private Inference effort-table 트레인(#3272/#3273 및 후속 #3275–#3278)과는 축이 다른 GUI 인증 버그다.라인 62 -
src/server/gui-session.tsLOOPBACK_GUI_SESSION_TTL_MS— 5분 TTL 자체는 문제가 아니라, 만료·재발급이 host 철자 불일치와 겹치면 재민트가 실패하기 쉬운 배경이다src/server/auth-cors.tsmanagementRequestOrigin— 루프백인데 Host 철자(localhost/127.0.0.1/::1)를 정규화하지 않아 세션 origin이 갈라진다src/server/gui-session.tsadmitGuiSession/x-opencodex-gui-origin— 저장된 browserOrigin과 한 글자라도 다르면 401; 루프백 동치 비교가 없다gui/src/api.tsstoreSession— meta origin과window.location.origin불일치 시 세션을 즉시 비워 fetch에 토큰이 안 실림gui/src/api.tsresolveTokenAfter401/promptCancelled— 재민트·프롬프트 실패 후 영구 취소 플래그로 새로고침 전까지 오프라인에 고정됨src/tray/windows-tray.ps1기본 host127.0.0.1vssrc/cli/dispatch.ts의 localhost 리맵 — 같은 머신에서 진입 URL 철자가 갈라지는 실사용 경로메인테이너의 판단이 필요한 지점
localhost로 통일 vs127.0.0.1로 통일 vs 세션 검증만 루프백 동치 허용), 보안상 Host 철자 엄격 유지를 우선할지ocxopen/status 대시보드 URL을 한쪽으로 맞추는 제품 결정을 서버 정규화와 같이 할지, URL만 먼저 맞출지promptCancelled고정을 완화할지(루프백에서는 조용히 재시도·메타 재주입), 아니면 사용자에게 "호스트 철자가 다름"을 명시할지너의 추천
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/health401 여부를 보면 된다.이 댓글은 grok-bot이 작성했습니다
Ingwannu commentedon Sep 2, 2026
Thanks for the detailed report. I confirmed that the dashboard becoming offline while
/healthzremains 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
devcode actually does:http://127.0.0.1:PORTdirectly. Its Open Dashboard action runsocx gui, and that CLI intentionally maps a127.0.0.1loopback bind tohttp://localhost:PORTbefore opening the browser.127.0.0.1should receive a newly minted session whose browser and server origins are bothhttp://127.0.0.1:PORT. It does not reuse thelocalhostsession from another page.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-infoinstead of makinglocalhost,127.0.0.1, and::1interchangeable 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:
GET /opencodex-sessionimmediately after the first/api/*401./api/system/healthrequest, whetherX-OpenCodex-API-KeyandX-OpenCodex-GUI-Originare present. Redact the key value completely.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.
lidge-jun commentedon Sep 2, 2026
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
HostandOriginheaders, whether anAuthorizationheader was present, and the immediate result ofGET /opencodex-sessionright 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.lidge-jun commentedon Sep 3, 2026
@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.tsand 401 recovery is single-flight atgui/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.
lidge-jun commentedon Sep 4, 2026
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
/healthzreturns 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:
/api/*request: the full response body (it carries an error message that names which check refused it), and the requestOriginheader.localhostversus127.0.0.1versus 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."
Arkos123 commentedon Sep 4, 2026
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.fetchauthentication 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.fetchwas still the authentication wrapper installed by OpenCodex during startup, located at gui/src/api.ts#L283.At 6.25 seconds,
window.fetchwas replaced by the request-interception function from the Tweak browser extension.The first batch of API requests included both
X-OpenCodex-API-KeyandX-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/healthand the other API endpoints returned 401:{"error":"opencodex admin token required"}No
/opencodex-sessionrequest was made after the 401 responses because the OpenCodex fetch wrapper was no longer in the call chain.At the same time,
/healthzcontinued to return 200. The proxy version was2.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-sessionbeing rejected. Instead, a browser extension overwrotewindow.fetchafter 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:
window.fetch, and instead have internal API calls explicitly use an authenticated fetch function, while avoiding recursion or conflicts with third-party fetch wrappers./healthzis healthy but/api/*returns 401, display a message such as “Management session authentication failed” instead of “Cannot connect to proxy.”