Title: Password-auth + reverse-proxy host header can make /codex-api/* return HTML login page with HTTP 200, which frontend surfaces as “RPC … returned malformed envelope”
Summary
When codexUI is run with --password behind a reverse proxy / Tailscale Serve that forwards to 127.0.0.1, unauthorized API requests to /codex-api/* can return the HTML login page with HTTP 200 instead of a 401 JSON/auth response.
The frontend RPC client expects JSON shaped like { result: ... }, so the next RPC call fails with:
RPC turn/start returned malformed envelope
This is easy to hit after a restart or any flow where the browser does not have a valid auth cookie for the codexUI process.
Why this matters
From the browser, the page can look like codexUI is loaded normally, but the next action fails with a misleading low-level RPC error instead of a clear “login required / session expired” message.
For remote/mobile use behind Tailscale Serve, this looks like the app randomly breaks after a restart.
Actual behavior
/codex-api/rpc responds with HTTP 200
Content-Type: text/html; charset=utf-8
- body is the login page HTML
- frontend tries
response.json() and/or expects a top-level result
- frontend throws
RPC <method> returned malformed envelope
Expected behavior
For unauthorized API requests under /codex-api/* (especially /codex-api/rpc), codexUI should return a machine-readable auth failure, e.g.:
- HTTP 401 or 403
- JSON such as
{ "error": "Unauthorized" } or { "code": "auth_required", ... }
The frontend should then show a clear auth/session-expired state instead of an RPC envelope error.
Reproduction
My deployment is:
- codexUI 0.1.78
- running on
127.0.0.1:15999
- fronted by Tailscale Serve:
https://<machine>.tail*.ts.net:3443 -> http://127.0.0.1:15999
- codexUI started with
--password ... --no-open --no-tunnel --no-login
Minimal repro:
- Start codexUI with
--password.
- Put it behind a reverse proxy / Tailscale Serve that forwards to
127.0.0.1:15999 while preserving a non-localhost Host header.
- Make an API request without a valid auth cookie.
- Observe that
/codex-api/rpc or /codex-api/accounts returns the login page HTML with HTTP 200.
- In the browser UI, the next action fails with
RPC ... returned malformed envelope.
Concrete proof from the live deployment:
python3 - <<'PY'
import urllib.request, json
host='netcup-clawd.tail5bf34d.ts.net:3443'
body=json.dumps({'method':'thread/start','params':{}}).encode()
req=urllib.request.Request(
'http://127.0.0.1:15999/codex-api/rpc',
data=body,
headers={
'Host': host,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
)
with urllib.request.urlopen(req, timeout=20) as r:
print(r.status)
print(r.headers.get('content-type'))
print(r.read(200).decode('utf-8', 'replace'))
PY
Observed result:
- status:
200
- content-type:
text/html; charset=utf-8
- body starts with
<!DOCTYPE html> ... <title>Codex Web</title> ...
The same behavior reproduces for /codex-api/accounts.
Root cause in code
-
Frontend RPC client treats any non-{ result: ... } JSON as a malformed envelope
- packaged frontend bundle:
dist/assets/index-DRmRl2NH.js
- function
Mv(...)
- if parsed response does not contain top-level
result, it throws:
RPC ${method} returned malformed envelope
-
Backend /codex-api/rpc handler normally does wrap RPC responses correctly
- packaged backend bundle:
dist-cli/index.js:5505-5523
- normal success path is
setJson4(res, 200, { result })
-
Password auth middleware returns HTML login page with HTTP 200 for unauthorized requests
dist-cli/index.js:6243-6253
- request is only auto-authorized for:
- localhost remote + localhost host header, or
- trusted Tailscale remote, or
- cookie token found in
validTokens
dist-cli/index.js:6296-6339
validTokens is an in-memory Set()
- unauthorized requests that do not hit
/auth/login fall through to:
res.status(200).send(LOGIN_PAGE_HTML)
-
Because the token store is in-memory, a restart recreates the empty Set()
- so any browser that no longer has a currently-valid cookie for this process instance can hit the HTML-200 path again
-
Reverse-proxy / Tailscale-Serve shape makes this easier to hit
- when codexUI is bound to
127.0.0.1 and fronted by a reverse proxy, the app may see:
- local/proxy-side socket address
- non-localhost
Host header
- that means it can fail the localhost fast-path while still serving a remote browser session
Suggested fix direction
Primary fix:
- For unauthorized requests under
/codex-api/*, never return HTML with HTTP 200.
- Return a proper auth error (401/403 JSON) instead.
Recommended follow-up:
- Teach the frontend RPC wrapper to recognize auth failures cleanly and show a login/session-expired state.
- Consider using a signed stateless cookie or another restart-safe session mechanism if password auth is meant to survive process restarts more gracefully.
Why this is not a duplicate
I searched the current issues before filing.
The closest things I found were:
I did not find an existing issue covering:
- HTML login page returned from
/codex-api/rpc with HTTP 200
- frontend surfacing that as
RPC ... returned malformed envelope
- the reverse-proxy / Tailscale Serve interaction
Environment
- codexUI: 0.1.78
- Codex CLI: 0.120.0
- Node: v22.22.2
- OS: Linux netcup-clawd 6.17.0-14-generic x86_64 (Ubuntu)
- service start shape:
/usr/bin/node .../dist-cli/index.js --port 15999 --password <set> --no-open --no-tunnel --no-login /root/.hermes/workspace
- proxy shape:
https://netcup-clawd.tail5bf34d.ts.net:3443 (tailnet only)
- proxied to
http://127.0.0.1:15999
Title: Password-auth + reverse-proxy host header can make /codex-api/* return HTML login page with HTTP 200, which frontend surfaces as “RPC … returned malformed envelope”
Summary
When codexUI is run with
--passwordbehind a reverse proxy / Tailscale Serve that forwards to127.0.0.1, unauthorized API requests to/codex-api/*can return the HTML login page with HTTP 200 instead of a 401 JSON/auth response.The frontend RPC client expects JSON shaped like
{ result: ... }, so the next RPC call fails with:RPC turn/start returned malformed envelopeThis is easy to hit after a restart or any flow where the browser does not have a valid auth cookie for the codexUI process.
Why this matters
From the browser, the page can look like codexUI is loaded normally, but the next action fails with a misleading low-level RPC error instead of a clear “login required / session expired” message.
For remote/mobile use behind Tailscale Serve, this looks like the app randomly breaks after a restart.
Actual behavior
/codex-api/rpcresponds with HTTP 200Content-Type: text/html; charset=utf-8response.json()and/or expects a top-levelresultRPC <method> returned malformed envelopeExpected behavior
For unauthorized API requests under
/codex-api/*(especially/codex-api/rpc), codexUI should return a machine-readable auth failure, e.g.:{ "error": "Unauthorized" }or{ "code": "auth_required", ... }The frontend should then show a clear auth/session-expired state instead of an RPC envelope error.
Reproduction
My deployment is:
127.0.0.1:15999https://<machine>.tail*.ts.net:3443 -> http://127.0.0.1:15999--password ... --no-open --no-tunnel --no-loginMinimal repro:
--password.127.0.0.1:15999while preserving a non-localhostHostheader./codex-api/rpcor/codex-api/accountsreturns the login page HTML with HTTP 200.RPC ... returned malformed envelope.Concrete proof from the live deployment:
Observed result:
200text/html; charset=utf-8<!DOCTYPE html> ... <title>Codex Web</title> ...The same behavior reproduces for
/codex-api/accounts.Root cause in code
Frontend RPC client treats any non-
{ result: ... }JSON as a malformed envelopedist/assets/index-DRmRl2NH.jsMv(...)result, it throws:RPC ${method} returned malformed envelopeBackend
/codex-api/rpchandler normally does wrap RPC responses correctlydist-cli/index.js:5505-5523setJson4(res, 200, { result })Password auth middleware returns HTML login page with HTTP 200 for unauthorized requests
dist-cli/index.js:6243-6253validTokensdist-cli/index.js:6296-6339validTokensis an in-memorySet()/auth/loginfall through to:res.status(200).send(LOGIN_PAGE_HTML)Because the token store is in-memory, a restart recreates the empty
Set()Reverse-proxy / Tailscale-Serve shape makes this easier to hit
127.0.0.1and fronted by a reverse proxy, the app may see:HostheaderSuggested fix direction
Primary fix:
/codex-api/*, never return HTML with HTTP 200.Recommended follow-up:
Why this is not a duplicate
I searched the current issues before filing.
The closest things I found were:
How to use without login— about skippingcodex login, not HTTP password-auth / reverse-proxy behaviorPressing the stop button causes the session to disappear— different bugI did not find an existing issue covering:
/codex-api/rpcwith HTTP 200RPC ... returned malformed envelopeEnvironment
/usr/bin/node .../dist-cli/index.js --port 15999 --password <set> --no-open --no-tunnel --no-login /root/.hermes/workspacehttps://netcup-clawd.tail5bf34d.ts.net:3443(tailnet only)http://127.0.0.1:15999