Skip to content

Resolve WebSocket handshake headers per connect attempt - #106

Open
mikhailm-coder wants to merge 1 commit into
hotfix/restore-custom-headersfrom
hotfix/rotatable-handshake-headers
Open

mikhailm-coder wants to merge 1 commit into
hotfix/restore-custom-headersfrom
hotfix/rotatable-handshake-headers

Conversation

@mikhailm-coder

Copy link
Copy Markdown

Stacked on #105 — review that one first; this PR's diff is only the delta on top of it.
Base retargets to main automatically once #105 merges.

Why

The agent sends its bearer token as ?authorization=<jwt> in the NATS WebSocket URL. Anything
that logs a URL logs a live customer credential — in front of our agents that is the Google load
balancer, ~113k entries a day, retained 30 days. A handshake header reaches none of those sinks.

The catch: custom_header captures its value once, when ConnectOptions is built. That is fine
for x-machine-id, but a token that rotates while the client is connected can then never reach
the server — the only way to adopt a new one is to discard the client and rebuild it, along with
every subscription hanging off it. In openframe-client that meant a 133-line rotation
supervisor, a drain, and a rebind hazard across six JetStream listeners.

What

custom_header_provider(name, || async { ... }) — a closure the connector calls on every
connect attempt, so the next reconnect carries the current token by itself. custom_header keeps
the fixed-value form and now shares the same resolution path.

  • Empty value from a provider = header skipped for that attempt. A static empty value is still
    sent, exactly as before, so nothing already deployed changes behaviour.
  • Providers are bounded by connection_timeout: they run inline on the connection handler, so one
    that never returns would otherwise stop the client reconnecting entirely.
  • Header names appear in ConnectOptions' Debug; values never do, guarded by a test.

Effect on the caller

Applied to openframe-client, nats_connection_manager.rs goes +16 −93 — the supervisor,
the rebuild, the drain and the Notify all disappear, and rotation needs no client code at all.
Verified by building the client against this branch.

Verified

  • cargo build --all-targets and cargo check --no-default-features --features ring, both under
    RUSTFLAGS="-D warnings", plus --features websockets
  • both new doc examples compile as doctests; new debug_does_not_render_handshake_header_values
    test passes
  • no formatting churn on pre-existing code

🤖 Generated with Claude Code

https://claude.ai/code/session_0187LY8GpfSvFDqYN1cDAi9u

custom_header captures its value once, which is fine for x-machine-id but wrong
for a credential: a token that rotates while the client is connected can never
reach the server, because the header was frozen when ConnectOptions was built.
Adopting a new one means discarding the client and rebuilding it, along with
every subscription hanging off it.

custom_header_provider takes a closure the connector calls on every connect
attempt, so the next reconnect carries the current token on its own. Values
resolving to an empty string are skipped. custom_header keeps the fixed-value
form and now shares the same resolution path.

Header names appear in ConnectOptions' Debug output; values do not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187LY8GpfSvFDqYN1cDAi9u
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