client: silent OAuth refresh-and-retry on 401 for Fjord-Account hosts #228
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/client-silent-refresh"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Bug
fj's silent OAuth refresh was wired into onlyfj instancesandfj auth. The sharedClient(src/client/mod.rs) that backsfj api,fj pr,fj issue,fj repo,fj branch, etc. resolved a static bearer at construction and had no 401/refresh/retry logic. So on a Fjord-Account (SSO /FjordBearer) host, once the short-lived access token expired, every command except instances/auth 401'd with the server's "Token expired; sign in again" despite a valid stored refresh token.Fix
Centralize refresh-and-retry in
Client's core request path (request_with_headers, split into an outer refresh wrapper +send_oncefor the transient-failure retry loop). On a 401 for a host we actually sent a Fjord bearer to:fjord::oidc::valid_refresh_tokengates on an identity-bound stored refresh token,fjord::oidc::refresh_sessionrefreshes + persists the rotated token and returns the fresh access token,PAT hosts, off-host (
fj api <url>) requests, and the no-refresh-token case surface the original 401 unchanged. Bounded to a single refresh+retry, so a persistently-401 endpoint can't loop.&selftoken mutabilityrequest_with_headerstakes&self, so the bearer moved behind anArc<RwLock<String>>(shared across clones, so a refresh on one is visible to later requests and every clone). The lock guard is never held across an await: the header is built from a cloned snapshot, and the post-refresh swap is a short write.Clientstays cheap to clone (Arc).fj apipathConfirmed: the raw
fj apipath (src/cli/api.rs) surfaces the response straight fromrequest_with_headers, and both the single-request and--paginatefollow-up requests funnel through it, so they all get the refresh. Not just the typedClient::json/sendhelpers.instances.rs call-site
Kept, not removed. The brief expected
fetch_instancesto become redundant, butfj instancestalks to the platform's/api/v1/me/forge-instancesviafjord::list_instances, which bypassesCliententirely (Clientis scoped to a single forge gateway). Removing its refresh would regressfj instances. Added a comment explaining the two refresh paths are independent.Tests
Four wiremock regressions (a
#[cfg(test)]refresh hook seams the keychain-backedfjord::oidcflow so the wiring is testable without a keychain):Verify
cargo build,cargo test(682 pass, 2 pre-existing ignored),cargo clippy --all-targets --all-features -- -D warnings,cargo fmt --checkall green. No em-dashes in comments.Do not merge. No tag pushed.