Skip to content

ttyd /terminal/token route requires pod_ip query param, but ttyd client omits it -> 422 on every terminal load #90

Description

@dev-dull

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions