Skip to content

Keep the refresh token when a token refresh fails transiently - #55

Open
puncsky wants to merge 1 commit into
render-oss:mainfrom
puncsky:fix/keep-refresh-token-on-transient-failure
Open

puncsky wants to merge 1 commit into
render-oss:mainfrom
puncsky:fix/keep-refresh-token-on-transient-failure

Conversation

@puncsky

@puncsky puncsky commented Oct 10, 2026 •

Copy link
Copy Markdown

Summary

maybeRefreshAPIToken cleared the stored refresh token on any refresh error. One transient 503, 429, network error or malformed response permanently removed the only credential that could recover the session. Because the access token can still be valid, the triggering command may even succeed — the user only finds out later, when the access token expires and every command demands a new render login.

This change:

  • makes pkg/client/oauth return a typed *ResponseError carrying the HTTP status, OAuth error code and Retry-After (the Error() strings are unchanged, so the device flow's authorization_pending handling is unaffected);
  • clears the refresh token only for a definitive invalid_grant (HTTP 400/401). Transport errors, deadlines, 429, 5xx, temporarily_unavailable, slow_down and malformed 200 responses keep it;
  • keeps the existing behaviour of exactly one bounded refresh attempt per command.

Motivation

Reproduced with a token endpoint that briefly answered 503: after one command, the config no longer had a refresh token, and the next command after access-token expiry required a fresh login even though the endpoint was healthy again.

How to verify

  • go test ./pkg/client/... ./pkg/tui/views/...
    • pkg/client/clientrefresh_test.go: 503/429/500/malformed/other-400 preserve the refresh token; invalid_grant clears it; a transport error leaves the config untouched; a later process recovers and rotates both tokens.
    • pkg/client/oauth/oauth_test.go: each failure keeps its status, code and Retry-After and is classified correctly.
    • pkg/tui/views/login_test.go: device polling still continues through authorization_pending.
  • Reverting the client.go change fails all three refresh tests.
  • Full go test ./... passes except e2e/TestE2E, which fails identically on main without credentials.

Open questions, deliberately unchanged here: device-grant slow_down still aborts login, and Retry-After is recorded but not yet used for backoff.

Release notes

A temporary token-refresh failure (network error, 429 or 5xx) no longer signs you out; only a rejected refresh token requires `render login`.

maybeRefreshAPIToken cleared the stored refresh token on any refresh error.
A single 503, 429, network error or malformed response therefore removed the
only credential that could recover the session, forcing a new `render login`
once the access token expired - even though the server would have accepted
the refresh moments later.

pkg/client/oauth now returns a typed *ResponseError carrying the HTTP status,
OAuth error code and Retry-After (Error() strings are unchanged, so the device
flow's authorization_pending check still works). The refresh path clears the
refresh token only for a definitive invalid_grant (400/401); transport
errors, deadlines, 429, 5xx, temporarily_unavailable, slow_down and malformed
200s keep it. There is still exactly one bounded refresh attempt.
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.

1 participant