fix(xai-oauth): show "not received" page when loopback callback has no code

When xAI's auth backend fails to redirect (e.g. the German "We couldn't reach
your app" fallback shown in #27385), users sometimes navigate manually to the
bare loopback callback URL — `http://127.0.0.1:<port>/callback` with no query
string. The handler used to return 200 "xAI authorization received" for any
GET that hit the expected path, because `parse_qs("")` yields no `code` and no
`error`, leaving `result` untouched while the success page was still served.

The CLI's wait loop, of course, still saw no code and timed out with
`AuthError: xAI authorization timed out waiting for the local callback.`
The user is left looking at a browser tab that claims success and a terminal
that says failure — exactly the contradiction in #27385.

This change makes the empty-callback case return 400 with an explicit
"not received" page and a hint to retry `hermes auth add xai-oauth`. The
wait-loop semantics are unchanged: `result["code"]` and `result["error"]`
both stay None, so the CLI still raises a real timeout rather than treating
the bare hit as a successful callback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
briandevans 2026-05-17 06:14:46 -07:00 committed by Teknium
parent 1fabd6e100
commit bf6eeb3f93
2 changed files with 104 additions and 4 deletions

View file

@ -2405,15 +2405,37 @@ def _make_xai_callback_handler(expected_path: str) -> tuple[type[BaseHTTPRequest
"error": params.get("error", [None])[0],
"error_description": params.get("error_description", [None])[0],
}
# Treat a hit on the callback path with neither `code` nor `error`
# as a missing OAuth callback (e.g. xAI's auth backend failed to
# redirect and the user navigated to the bare loopback URL by hand).
# Show an explicit "not received" page rather than the success page —
# otherwise the browser claims authorization succeeded while the CLI
# is still waiting for a real callback and eventually times out.
if incoming["code"] is None and incoming["error"] is None:
self.send_response(400)
self._maybe_write_cors_headers()
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
body = (
"<html><body>"
"<h1>xAI authorization not received.</h1>"
"<p>No authorization code was present in this callback URL. "
"Return to the terminal and re-run "
"<code>hermes auth add xai-oauth</code> to retry.</p>"
"</body></html>"
)
self.wfile.write(body.encode("utf-8"))
return
# ThreadingHTTPServer allows a fallback/manual callback to complete
# while a browser connection is stuck. Once we have a terminal
# OAuth result (code or error), keep the first one so a later
# concurrent/invalid callback cannot overwrite state before
# validation in _xai_oauth_loopback_login().
if incoming["code"] or incoming["error"]:
with result_lock:
if not (result["code"] or result["error"]):
result.update(incoming)
with result_lock:
if not (result["code"] or result["error"]):
result.update(incoming)
self.send_response(200)
self._maybe_write_cors_headers()