Skip to content

fix(auth): refresh the token before a subscription (re)connect - #1103

Open
codyde wants to merge 1 commit into
masterfrom
fix/subscription-token-refresh
Open

fix(auth): refresh the token before a subscription (re)connect#1103
codyde wants to merge 1 commit into
masterfrom
fix/subscription-token-refresh

Conversation

@codyde

@codyde codyde commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #1102, which fixed bearer staleness on the HTTP path and left the WebSocket path untouched.

Problem

subscribe_graphql reads the bearer straight off disk and never calls ensure_valid_token.

A WebSocket authenticates once, at the upgrade. So a connection opened with a good token keeps working past its expiry — the staleness only bites on reconnect, and stream_http_logs_inner retries a dropped stream up to twelve times. Every attempt re-presents the same expired bearer and fails the handshake identically.

Today: a railway logs -f that outlives its access token dies at the first network blip, twelve identical failures deep, with no path back.

Doing this now rather than later because an always-on cloud-agent subscription — a socket expected to outlive many token lifetimes — would make it load-bearing rather than an edge case.

Fix

subscription.rs Credential resolution moves into connect_auth_header, which refreshes before picking the header. Testable without a WebSocket server.
client.rs ensure_valid_token_atensure_valid_token against an explicit token endpoint. Lets the test point at a scripted one; ensure_valid_token is now a thin wrapper that derives the URL.

A refresh failure stays non-fatal. It only fires once local expiry has passed, the stored token may still be good, and the handshake reports a genuinely dead credential better than a speculative refresh does. RAILWAY_TOKEN still short-circuits to project-access-token untouched.

Testing

1124 tests pass, 3 consecutive clean runs, 0 clippy errors.

Two new tests in auth_sim.rs:

  • an expired token on disk connects with the refreshed bearer (fails with Bearer stale-access if the refresh line is removed — verified)
  • a live token costs no refresh, so reconnects don't rotate needlessly

Not covered: the live WebSocket handshake. This checkout has no linked project, so logs -f couldn't be exercised end to end. The credential resolution is tested both ways; the remaining change is mechanical — the same two headers, applied from a resolved (name, value) pair instead of two inline branches.

Context

Came out of a feasibility review for putting railway ca's agent/session state on GraphQL subscriptions. That work is blocked on backboard exposing a cloud-agent subscription at all — this fix is the piece that's independent of it and worth landing on its own.

🤖 Generated with Claude Code

`subscribe_graphql` read the bearer straight off disk and never called
`ensure_valid_token`. A WebSocket authenticates once, at the upgrade, so a
connection opened with a good token keeps working past its expiry and the
staleness only shows up on reconnect — where `stream_http_logs_inner` retries
a dropped stream up to twelve times, re-presenting the same dead bearer and
failing the handshake identically every time.

Net effect today: a `railway logs -f` that outlives its access token dies at
the first network blip, with twelve identical failures and no path back.

#1102 fixed this staleness for the HTTP path and did not touch the WebSocket
path. Doing it now because an always-on cloud-agent subscription — a session
that is *expected* to outlive many token lifetimes — makes it load-bearing.

Credential resolution moves into `connect_auth_header`, which refreshes first
and then picks the header, so it can be tested without standing up a
WebSocket server. `ensure_valid_token_at` is `ensure_valid_token` against an
explicit token endpoint, which is what lets the test point at a scripted one.

A refresh failure stays non-fatal: it only fires once local expiry has passed,
the stored token may still be good, and the handshake reports a genuinely dead
credential better than a speculative refresh does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codyde codyde added the release/patch Author patch release label Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release/patch Author patch release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant