Skip to content

fix(github): retry without auth on 401 Unauthorized#9965

Closed
GeiserX wants to merge 1 commit into
jdx:mainfrom
GeiserX:fix/github-retry-401-unauthorized
Closed

fix(github): retry without auth on 401 Unauthorized#9965
GeiserX wants to merge 1 commit into
jdx:mainfrom
GeiserX:fix/github-retry-401-unauthorized

Conversation

@GeiserX

@GeiserX GeiserX commented May 18, 2026

Copy link
Copy Markdown

Summary

When GITHUB_TOKEN is set to a token scoped to a private GitHub Enterprise instance (e.g. github.example.com), requests to api.github.com return 401 Unauthorized because the token is not valid there.

This is common in enterprise environments where developers must have GITHUB_TOKEN set for internal tooling (make lint, CI scripts, etc.), but that same token is invalid for public GitHub. Since mise's token resolution chain (MISE_GITHUB_TOKEN > GITHUB_API_TOKEN > GITHUB_TOKEN) picks up the enterprise token, every api.github.com request fails with 401.

Fix

Mirror the existing 403 retry-without-auth logic from #9506: when api.github.com returns 401 and an Authorization header was sent, strip the header and retry unauthenticated. Public API endpoints (release listings, tags) work fine without auth — they just have lower rate limits (60 req/hr vs 5000).

The 401 block is placed after the existing 403 block in send_once_inner, using the same pattern:

  1. Check status + url + header presence
  2. Remove Authorization header
  3. Retry via send_once_inner with auth=false

Alternatives considered

  • MISE_GITHUB_TOKEN="" — empty strings are filtered out by .filter(|t| !t.is_empty()) in resolve_token, so this doesn't suppress the fallback to GITHUB_TOKEN.
  • use_versions_host=true — only covers version listing, not download URL resolution which still hits api.github.com.
  • Lockfiles — works but requires central maintenance whenever any tool version changes across repositories, which doesn't scale.
  • Wrapper scripts / direnv — fragile, breaks when developers forget to source them.

Related

@greptile-apps

greptile-apps Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

When a GITHUB_TOKEN scoped to a private GHE instance is present, every api.github.com request returns 401; this PR mirrors the existing 403 IP-allow-list retry to also strip the Authorization header and retry unauthenticated on 401.

  • Adds is_authenticated_github_unauthorized (matching structure of is_authenticated_github_forbidden) and a retry block in send_once_inner that removes the auth header and recurses with use_netrc=false.
  • Unlike the 403 path (which gates the retry on body.contains(\"IP allow list\")), the 401 path retries unconditionally on any 401 from api.github.com with an auth header — this means a genuinely expired or revoked PAT will silently fall back to the unauthenticated 60-req/hr tier instead of surfacing an error (noted in existing review thread).

Confidence Score: 4/5

The change is safe to merge for the stated enterprise token scenario; the retry logic is structurally sound with no infinite-loop risk, but a valid api.github.com token that has expired will silently fall back to unauthenticated instead of reporting an error.

The new 401 retry block unconditionally strips auth and retries for any 401 from api.github.com — unlike the 403 path which checks the response body before retrying. A user with a real but revoked/expired GitHub PAT would silently lose their authenticated rate-limit tier (5000 → 60 req/hr) with no visible error, making token problems harder to diagnose.

src/http.rs — specifically the unconditional nature of the 401 retry compared to the guarded 403 retry

Important Files Changed

Filename Overview
src/http.rs Adds a 401 retry-without-auth path for api.github.com, mirroring the existing 403 pattern but without a response-body guard; the retry correctly strips Authorization and passes use_netrc=false, with no infinite-loop risk

Reviews (2): Last reviewed commit: "fix(github): retry without auth on 401 U..." | Re-trigger Greptile

Comment thread src/http.rs
Comment thread src/http.rs

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a retry mechanism for GitHub API requests that return a 401 Unauthorized status. It adds a helper function, is_authenticated_github_unauthorized, to detect unauthorized responses from api.github.com when an Authorization header is present. In such cases, the client now removes the header and retries the request once. I have no feedback to provide as there were no review comments to evaluate.

@GeiserX

GeiserX commented May 18, 2026

Copy link
Copy Markdown
Author

Hi! I noticed the existing 403 retry path (#9506) doesn't have a dedicated unit test either — should I add one for this 401 case? Happy to write a quick spawn_canned_server-based test (401 → strip auth → retry → 200) if that'd be helpful.

When GITHUB_TOKEN is set to a token scoped to a private GitHub Enterprise
instance, requests to api.github.com return 401 Unauthorized. This is a
common scenario in enterprise environments where a single GITHUB_TOKEN
env var is required for internal tooling but is invalid for public GitHub.

This mirrors the existing 403 retry-without-auth logic from jdx#9506: when
api.github.com returns 401 and an Authorization header was sent, strip
the header and retry unauthenticated. Public API endpoints (release
listings, tags) work fine without auth — they just have lower rate limits.
@GeiserX GeiserX force-pushed the fix/github-retry-401-unauthorized branch from f5745c0 to b989f99 Compare May 18, 2026 13:55
@GeiserX

GeiserX commented May 18, 2026

Copy link
Copy Markdown
Author

@greptile-apps Good catch on the unconditional retry. A few thoughts on why I think this is fine as-is:

  1. mise only calls api.github.com for public data (release listings, tags). Unauthenticated requests still work — just at 60 req/hr instead of 5000. For version resolution this is more than sufficient.

  2. The primary use case is enterprise tokens that are structurally invalid for github.com (scoped to a GHE instance). These users can't remove GITHUB_TOKEN — they need it for internal tooling. A warning on every request would be noisy and unactionable.

  3. The existing 403 retry also uses debug!, not warn!. Same design choice for the same reason.

  4. Expired/revoked github.com tokens: true, these would silently downgrade. But the user experience is arguably better — mise still works (fetches versions, installs tools) rather than failing hard. If they hit rate limits, MISE_DEBUG=1 shows the retry.

That said, I'm open to alternatives if the maintainer prefers:

  • A single warn_once! per session on first 401 retry (less noisy, still surfaced)
  • A debug! only (current approach, matches 403 precedent)
  • Adding a body-content guard (though 401 bodies from GitHub are not as semantically useful as 403 bodies)

@jdx what's your preference here?

This comment was generated by an AI coding assistant.

@greptile-apps

greptile-apps Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

The reasoning in your comment is sound. Given the symmetry with the 403 handler and the specific scoping to api.github.com, debug! is the right call here. My preference:

Keep debug!, no change needed.

The key constraints that make this safe:

  1. The check is already tightly scoped: 401 + api.github.com + Authorization header present — this can't silently swallow 401s on other hosts or unauthenticated requests.
  2. The 403 precedent (fix(github): retry ip allow list errors without auth #9506) set this pattern intentionally. Diverging to warn! here would create an inconsistent UX with no real benefit.
  3. A warn_once! per session might sound appealing, but "your enterprise token doesn't work on github.com" is not actionable for users who can't unset GITHUB_TOKEN. It just generates noise.

The only edge case worth flagging is the expired/revoked github.com token scenario — but as you note, silently downgrading to unauthenticated (still functional) is better UX than hard-failing. Users who care can check MISE_DEBUG=1.

The implementation looks good as-is. The logic is clean and consistent with the existing codebase.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@jdx

jdx commented May 18, 2026

Copy link
Copy Markdown
Owner

I'm not sure this is a great idea, it makes things complicated to support invalid API keys

@GeiserX

GeiserX commented May 18, 2026

Copy link
Copy Markdown
Author

Thanks for the feedback @jdx — totally fair point. Retrying invalid keys is a workaround, not a proper solution.

The underlying problem: enterprise environments where GITHUB_TOKEN is scoped to a private GHE instance (required for internal tooling), and mise picks it up for api.github.com requests. I've researched how other tools in the ecosystem handle this and have a few alternative proposals — all based on the principle that credentials should be host-scoped, not ambient:


Proposal A: Empty MISE_GITHUB_TOKEN="" halts the resolution chain

Currently, setting MISE_GITHUB_TOKEN="" is treated as "not set" (filtered by .filter(|t| !t.is_empty())), so mise falls through to GITHUB_TOKEN. If an explicitly-set-but-empty value instead returned None immediately (stopping the chain), users could suppress auth for github.com with one env var.

  • Pros: ~2 lines of code, zero new settings, respects explicit user intent ("I set this, stop looking")
  • Cons: Subtle semantic change — someone might have MISE_GITHUB_TOKEN="" set accidentally

Proposal B: credential_command returning empty = "no token, stop looking"

The credential_command already receives MISE_CREDENTIAL_HOST. If it exits 0 with empty output, treat that as "explicitly no token for this host" rather than "nothing found, keep looking."

  • Pros: Per-host suppression via existing mechanism, no new settings
  • Cons: Requires users to write a script; slightly higher friction

Proposal C: github.token_hosts allowlist setting

A new setting that restricts where env-var tokens are sent:

[settings.github]
token_hosts = ["github.enterprise.com"]

When set, GITHUB_TOKEN / MISE_GITHUB_TOKEN would only be sent to listed hosts. Requests to api.github.com would skip env vars and fall through to github_tokens.toml or gh CLI.

  • Pros: Explicit, declarative, mirrors npm (//host/:_authToken) and Renovate (matchHost) patterns
  • Cons: New setting to maintain and document

Proposal D: github.ignore_env_tokens = true kill-switch

A boolean that skips the env-var sources entirely (steps 1-3 in resolve_token), forcing mise to rely on github_tokens.toml, gh CLI, or credential_command only.

  • Pros: Simple toggle, one setting
  • Cons: All-or-nothing — can't selectively suppress for one host

Any of these would solve the problem properly. Happy to implement whichever direction you prefer (or close this PR if none seem worth it). Proposal A is the smallest change if you want minimal surface area.

This comment was generated by an AI coding assistant.

@jdx

jdx commented May 18, 2026

Copy link
Copy Markdown
Owner

unauthenticated tokens is not a good idea in any situation. You should follow the github token guide in the docs.

@GeiserX

GeiserX commented May 20, 2026

Copy link
Copy Markdown
Author

Closing this — we found the documented solution works well for our use case. Setting MISE_GITHUB_TOKEN to a valid github.com token (fine-grained PAT with zero permissions) resolves the conflict cleanly. Thanks for pointing us in the right direction, @jdx.

This comment was AI-generated.

@GeiserX GeiserX closed this May 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants