Problem
The ttyd terminal proxy route requires pod_ip as a mandatory query param:
# landing/app/routes/build.py
@router.get("/{team}/{app_slug}/terminal/token")
async def build_terminal_token(..., pod_ip: str = Query(..., alias="pod_ip")):
return await http_proxy(request, pod_ip=pod_ip, pod_port=8080, path="/token")
But ttyd's bundled client builds its token URL without the page's query string, while it builds the ws URL with it:
// ttyd client (minified)
// token URL: [protocol, "//", host, pathname, "/token"].join("") <-- no location.search
// ws URL: [proto, "//", host, pathname, "/ws", window.location.search] <-- includes ?pod_ip
So when the terminal iframe is served at /build/<team>/<app>/terminal/?pod_ip=<ip>, ttyd requests:
GET /build/<team>/<app>/terminal/token -> 422 {"detail":[{"loc":["query","pod_ip"],"type":"missing"}]}
GET /build/<team>/<app>/terminal/ws?pod_ip=<ip> -> 101 (works, search preserved)
The token endpoint 422s on every terminal load.
Impact
Low-but-real. ttyd's refreshToken() guards with if (e.ok), so a 422 doesn't throw — it just leaves the token unset (empty), and since the backend ttyd runs with no auth token, the ws still connects. So it's not fatal today, but: (a) it's a guaranteed 422 on every terminal load (noise, and misleading when debugging), and (b) it's fragile — anything that makes ttyd require a successful token fetch would black-screen the terminal.
Suggested fix
Make pod_ip optional on the ttyd sub-resource routes (/terminal/token and /terminal/{path}) and resolve it another way (session/referer), or have the terminal iframe pass pod_ip via a path segment/cookie that ttyd's relative requests preserve. The /ws route already works because ttyd appends location.search; the /token (and any relative asset) route needs the same value without relying on the query string.
Context
Found while debugging a black terminal on a Traefik + Authelia (forward-auth) deployment. Ruled out auth (ws upgrade returns 101 with a valid session) and confirmed the backend (ttyd + Claude Code) works — the actual black screen in my case was build-pod startup churn, but this 422 surfaced during the investigation and is a clean, separate bug.
Problem
The ttyd terminal proxy route requires
pod_ipas a mandatory query param:But ttyd's bundled client builds its token URL without the page's query string, while it builds the ws URL with it:
So when the terminal iframe is served at
/build/<team>/<app>/terminal/?pod_ip=<ip>, ttyd requests:The token endpoint 422s on every terminal load.
Impact
Low-but-real. ttyd's
refreshToken()guards withif (e.ok), so a 422 doesn't throw — it just leaves the token unset (empty), and since the backend ttyd runs with no auth token, the ws still connects. So it's not fatal today, but: (a) it's a guaranteed 422 on every terminal load (noise, and misleading when debugging), and (b) it's fragile — anything that makes ttyd require a successful token fetch would black-screen the terminal.Suggested fix
Make
pod_ipoptional on the ttyd sub-resource routes (/terminal/tokenand/terminal/{path}) and resolve it another way (session/referer), or have the terminal iframe passpod_ipvia a path segment/cookie that ttyd's relative requests preserve. The/wsroute already works because ttyd appendslocation.search; the/token(and any relative asset) route needs the same value without relying on the query string.Context
Found while debugging a black terminal on a Traefik + Authelia (forward-auth) deployment. Ruled out auth (ws upgrade returns 101 with a valid session) and confirmed the backend (ttyd + Claude Code) works — the actual black screen in my case was build-pod startup churn, but this 422 surfaced during the investigation and is a clean, separate bug.