odysseus-dev/odysseus · error · HTTPException

GitHub did not return a device code

Error message

GitHub did not return a device code

What it means

Raised (HTTP 502) by the Copilot device-flow start helper when the device-code endpoint returned HTTP 2xx but the JSON body has no 'device_code' field. It means GitHub (or GHES) accepted the request but responded with an unexpected payload — the app cannot proceed without a device_code to poll against.

Source

Thrown at routes/copilot_routes.py:117

    return result


def _start_device_flow(request: Request, form) -> DeviceFlowStart:
    host = copilot.GITHUB_HOST
    ent = str(form.get("enterprise_url") or "").strip()
    if ent:
        host = copilot.normalize_domain(ent)
    try:
        data = copilot.request_device_code(host)
    except httpx.HTTPStatusError as e:
        status = e.response.status_code if e.response is not None else "unknown"
        raise HTTPException(502, f"GitHub device-code request failed (HTTP {status})")
    except Exception as e:
        raise HTTPException(502, f"GitHub device-code request failed: {e}")

    device_code = data.get("device_code")
    if not device_code:
        raise HTTPException(502, "GitHub did not return a device code")

    # verification_uri_complete embeds the user code, so the browser tab we
    # open lands the user straight on GitHub's "Authorize" screen with the
    # code pre-filled — one click, no manual code entry.
    return DeviceFlowStart(
        pending={
            "device_code": device_code,
            "host": host,
            "enterprise_url": ent,
            "owner": get_current_user(request) or None,
        },
        response={
            "user_code": data.get("user_code"),
            "verification_uri": data.get("verification_uri"),
            "verification_uri_complete": data.get("verification_uri_complete"),
        },
        interval=int(data.get("interval") or 5),
        expires_in=int(data.get("expires_in") or 900),

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Reproduce the raw request to see the actual body: curl -s https://github.com/login/device/code -d 'client_id=<id>' -d 'scope=read:user'.
  2. If a proxy intercepts HTTPS, bypass it for the GitHub/enterprise host.
  3. On GHES, confirm the version supports the device flow grant and the OAuth app is allowed to use it.
  4. Retry once — a transient 200-with-error from a load balancer is possible.
Defensive patterns

Strategy: retry

Try / catch

try { start = await post('/copilot/device/start', form); } catch (e) { if (e.status === 502 && /did not return a device code/.test(e.message)) { await sleep(2000); return retryOnce(); } throw e; }

Prevention

When it happens

Trigger: POST to the device-flow start endpoint where the provider responds 200 with an error-shaped body (e.g. {'error': 'unauthorized_client'}), an HTML error page that happened to parse, or an API contract change / GHES version that returns a different schema. Rare on github.com, more plausible on older GitHub Enterprise Server versions.

Common situations: GHES instance on an older/newer version whose device endpoint returns an error body with 200; an intermediate proxy (captive portal, corporate MITM) returning a 200 HTML page instead of the OAuth response.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/243eec197cbb835f. Report an issue: GitHub.