{"record":{"id":"6ba209c3d3a35ebc","repo":"bytedance/deer-flow","slug":"failed-to-mint-installation-token-status-resp-st","errorCode":null,"errorMessage":"Failed to mint installation token (status={resp.status_code} body={resp.text!r})","messagePattern":"Failed to mint installation token \\(status=(.+?) body=(.+?)\\)","errorType":"exception","errorClass":"GitHubAppAuthError","httpStatus":null,"severity":"error","filePath":"backend/app/gateway/github/app_auth.py","lineNumber":154,"sourceCode":"\n\nasync def _request_new_installation_token(\n    installation_id: int,\n    *,\n    client: httpx.AsyncClient | None = None,\n) -> _CachedToken:\n    \"\"\"Hit ``POST /app/installations/{id}/access_tokens`` once.\"\"\"\n    headers = {\n        \"Authorization\": f\"Bearer {mint_app_jwt()}\",\n        \"Accept\": \"application/vnd.github+json\",\n        \"X-GitHub-Api-Version\": \"2022-11-28\",\n    }\n    url = f\"{_GITHUB_API_BASE}/app/installations/{installation_id}/access_tokens\"\n\n    async def _do(c: httpx.AsyncClient) -> _CachedToken:\n        resp = await c.post(url, headers=headers, timeout=15.0)\n        if resp.status_code != 201:\n            raise GitHubAppAuthError(f\"Failed to mint installation token (status={resp.status_code} body={resp.text!r})\")\n        data = resp.json()\n        token = data[\"token\"]\n        # GitHub returns ISO8601 expires_at; we just bake in a 60-minute\n        # life and let the leeway handle the rest. Trusting the wall\n        # clock instead of parsing ISO is fine here.\n        expires_at = time.time() + 60 * 60\n        return _CachedToken(token=token, expires_at=expires_at)\n\n    if client is None:\n        async with httpx.AsyncClient() as c:\n            return await _do(c)\n    return await _do(client)\n\n\nasync def mint_installation_token(\n    installation_id: int,\n    *,\n    client: httpx.AsyncClient | None = None,","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/app/gateway/github/app_auth.py#L136-L172","documentation":"GitHubAppAuthError raised when POST /app/installations/{id}/access_tokens returns a non-201 status. The error embeds both the HTTP status and the raw response body, so GitHub's own error message (expired JWT, wrong installation id, suspended installation, rate limit) is visible in the exception text.","triggerScenarios":"Minting an installation token while: the App JWT is expired or has iat in the future (clock skew); the installation_id does not belong to this App; the installation was suspended or uninstalled; the private key/App id pair no longer matches the App; secondary rate limit hit.","commonSituations":"Server clock drift beyond the JWT leeway (GitHub rejects iat/exp within 60s windows); rotated the App's key but the deployed env still has the old one; installation removed by the repo owner while your integration still references it; App id and key from two different Apps after a re-create; GitHub API outage returning 5xx.","solutions":["Read the embedded body in the exception message — GitHub states the exact reason ('Integration must have access to this installation', 'A JWT expiry/exp is required', etc.) and act on it directly","Check server time (ntpd/systemd-timesyncd); JWTs are time-sensitive, skew > 60s fails with 401","Verify installation_id matches an installation of THIS App (GET /app/installations with the App JWT) and that it is not suspended","If the App key was rotated, redeploy with the new private key; if GitHub returned 5xx/rate-limit, back off and retry (token minting is cached for ~60 min so retries are rare)"],"exampleFix":"null","handlingStrategy":"retry","validationCode":"null","typeGuard":"null","tryCatchPattern":"for attempt in range(3):\n    try:\n        token = await mint_installation_token(inst_id, client=client)\n        break\n    except GitHubAppAuthError as e:\n        if 'status=5' in str(e) or 'status=403' in str(e):  # transient/rate-limit\n            await asyncio.sleep(2 ** attempt); continue\n        raise  # 401/404 style errors are config problems — do not retry","preventionTips":["NTP-sync servers; GitHub JWT time windows are ~60s","Cache installation tokens (they last ~60 min) instead of minting per request","Verify App id + key + installation_id come from the same GitHub App","Parse the embedded status/body to classify retryable vs fatal"],"tags":["github-app","installation-token","clock-skew","http-api","retry"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}