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

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_at — ensure_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

This branch has not been deployed

No deployments
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