Relationships
#1900 dashboard device-login poll checks for an error code serve never sends, so all failures show as an endless spinner
Opened by sntxrr · 8/28/2026· Shipped 8/30/2026
Summary
The dashboard's OAuth device-login poll tests for an error code that
swamp serve never sends. As a result no failure mode is ever displayed:
expiry, user denial, admission refusal and upstream errors all render
identically as an indefinite "Waiting for you to approve...".
Environment
- swamp
20260828.202201.0-sha.2a5bcb89 swamp serve --auth-mode oauth --dashboard
The mismatch
packages/dashboard/src/views/Login.tsx:173-178:
if (data.token) {
onToken(data.token);
} else if (data.error === "expired_token") {
setError("Login expired. Please try again.");
setState("error");
}
// every other response: silently ignored, keep pollingsrc/serve/device_auth_handler.ts emits these, and only these:
| Condition | Response |
|---|---|
| pending | 202 {"status":"pending"} |
| expired | 410 {"error":"Device code expired"} |
| denied by user | 403 {"error":"Authorization denied by user"} |
| not admitted | 403 {"error":"Not admitted","reason":...} |
| upstream failure | 502 {"error":"Upstream provider returned an unexpected error"} |
"expired_token" is the OAuth error code from the upstream provider, which
the handler translates before responding. The route in
src/cli/commands/serve.ts:3734 passes the handler's response through
unmodified, so the string the client checks for never appears on the wire.
The response status is also ignored — resp.ok is never consulted, so a 410,
403 or 502 is parsed as JSON and discarded exactly like a 202.
Impact
Every device-login failure looks identical to "still waiting". In particular a
user who is authenticated but outside --allowed-collectives gets 403 Not admitted — the one message that would tell them exactly what is wrong — and
sees a spinner instead, indefinitely.
This was hit in practice while diagnosing a server-side crash: the server was aborting mid-flow and the dashboard reported nothing at all, which made the crash considerably harder to find.
Suggested fix
Branch on the HTTP status rather than an error string, and surface
data.error / data.reason verbatim. Something like:
202→ keep polling403→ showdata.errorplusdata.reasonwhen present (terminal)410→ "Login expired, please try again" (terminal)5xx→ show the error and offer retry (terminal)
Continuing to poll should be the explicitly-handled case, not the default for everything unrecognised.
Shipped
Click a lifecycle step above to view its details.