Skip to content

feat(net): migrate the JS connection on GOAWAY; refuse bad redirects in Rust - #4143

Merged
kixelated merged 14 commits into
quest/m1/drain/READMEfrom
quest/m1/drain/client-goaway
Sep 25, 2026
Merged

kixelated merged 14 commits into
quest/m1/drain/READMEfrom
quest/m1/drain/client-goaway

Conversation

@kixelated

@kixelated kixelated commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Completes /quest/m1/drain/client-goaway.md on the drain line (#4132).

Problem

A fleet drain withdraws a relay from DNS and then sends GOAWAY, expecting clients to redial through a fresh resolve. The JS client did not migrate: it logged the lite GOAWAY, closed the IETF 17+ session outright, and the draft-14 to -16 adapter tore down its control stream without decoding the message. The Rust client migrated, but nothing tested the empty-URI fleet-drain path, and a malformed or refused redirect quietly redialed the peer that asked us to leave.

Approach

Rust (moq-tokio)

  • A malformed or policy-refused GOAWAY redirect now ends the Connection with Error::RefusedRedirect instead of falling back to the old address list or a configured fallback. An empty URI still keeps the list, fallbacks included.
  • A tls.fingerprint pin refuses a host change, even under follow.
  • Fleet-drain regression test: two servers behind one name, a test-only resolver table (resolve::hosts, #[cfg(test)]) repointed between dials, and an empty-URI GOAWAY with a 30s deadline. It checks that the redial lands on the new server while the drained one is still accepting, the track resumes at the next group, and the old session closes at the 500ms cap. Removing the repoint makes it fail.

JS (@moq/net)

  • Every wire now surfaces GOAWAY through the package-private wire view (wireOf(session).goaway) and keeps the session serving: lite, IETF 17+, and the 14 to 16 adapter, which now decodes the body it had already read. A second GOAWAY is a protocol violation.
  • Reload migrates the way Rust Draining does. On GOAWAY it dials the target at once with no backoff, and the old session serves until it closes or reaches the handover cap. The cap is the configured one, lowered to the peer's timeout only when that is positive. Absence is never a zero-length handover. A failed replacement dial, or a GOAWAY within the initial delay, goes through backoff.
  • The guard is ported from Rust (connection/goaway.ts): same-host by default, with follow and ignore modes. It refuses scheme downgrades, widening to a local literal (IPv4-mapped and link-local forms included), and any host change under serverCertificateHashes or serverCertificate. A refusal is terminal (Error.RefusedRedirect) and never redials. The error message never includes the URI, since it can carry credentials.
  • Pool: an accepted redirect moves the entry's key to the target. If a live entry already holds the target, that entry wins, and the migrating one leaves the pool and serves only its existing handles. Removal is identity-guarded on the current key.
  • Origin: a request keeps an outranked route until its replacement answers. Before, the swap passed through undefined while the new session's route materialized, which unrouted the path during every migration.

Impact

  • moq-tokio: new Error::RefusedRedirect(String). Additive, since the enum is #[non_exhaustive].
  • moq-tokio: behavior change. A refused or malformed redirect is terminal, and a fingerprint pin holds the host. doc/bin/relay/config.md is updated. A relay cluster link that hits this ends that Connection, and the cluster's outer loop redials after its own backoff.
  • moq-tokio: Redirect::resolve keeps its signature and lenient fallback. Its doc now says Connection is stricter.
  • @moq/net: new ConnectionProps.goaway?: { redirect?: "follow" | "same-host" | "ignore"; handover?: Time.Milli }, mirroring Rust connection::Goaway. It selects a private loop, and share: true refuses it.
  • @moq/net: new types Connection.Goaway, Connection.Redirect and Error.RefusedRedirect.
  • @moq/net: behavior change. Connections migrate on GOAWAY, IETF 17+ no longer closes on it, the pool re-keys on redirect, and an origin request holds the outranked route until the new one answers.
  • Wire: none.

Judgment calls (maintainer review)

  • Base: the drain line (quest/m1/drain/README), not main or dev. Rebased onto feat(relay): hand the drain signal to embedders and drain arrivals to one deadline #4138 so the relay still exits at the deadline the trigger recorded. Everything here is additive.
  • Redirect::resolve: left lenient rather than made fallible, which would be a dev break. Proposed follow-up: make it return a Result or make it private on dev.
  • Public JS names: goaway, Goaway, Redirect and RefusedRedirect mirror the Rust names.
  • Drain signal: kept package-private instead of added to the public Established interface.
  • Status: no new "migrating" status, because adding it widens the public ReloadStatus union. Status stays "connected" while the old session serves.
  • WebSocket fallback: a redirect target drops a caller-pinned websocket.url, which would otherwise keep dialing the old relay.
  • Test harness: JS tests use mock transports, like the other connection/ tests, not a relay. The in-tree relay only ever sends an empty URI and cannot drain on demand until relay-drain-api lands, so the relay end-to-end test moves to the line README's Plan.

Alternatives

  • Swap origin wiring explicitly in Reload rather than let both sessions feed the origin. Rejected: the origin already scopes routes to the session that announced them. The only gap was the materialization swap, which the origin fix closes for every route change.
  • Keep the refused-redirect fallback. Rejected by the quest: it redials a peer that asked us to leave.

Follow-ups

  • Line README: a JS client watching through an in-tree relay drained via relay-drain-api.
  • dev: make Redirect::resolve fallible or private.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

Rebased onto #4138. The draft-17 GOAWAY test compares a branded millisecond, which is what Check rejected.

(Written by Grok 4.7)

@kixelated
kixelated force-pushed the quest/m1/drain/client-goaway branch from c2f2011 to c644f65 Compare September 25, 2026 15:22
@kixelated
kixelated marked this pull request as ready for review September 25, 2026 15:22

Copy link
Copy Markdown
Collaborator Author

Rebased onto quest/m1/drain/README (a40cc9a, #4138). The relay still exits at the deadline the trigger recorded. The line README keeps that deadline and the open drain-exit quest, and drops client-goaway now that this lands it.

Check was failing because the draft-17 GOAWAY test compared a branded Time.Milli to a bare number. Local just check against this base is green.

(Written by Grok 4.7)

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T18:54:48.233284Z e8ae2a6 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c644f65113

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


let next: URL | undefined;
try {
next = target(this.goaway.redirect ?? "same-host", drain.uri, dialing, pinned);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare redirects with the actual fallback endpoint

When an explicit websocket.url points at a different host or scheme and WebSocket wins the transport race, dialing is still the primary URL even though connectInner dialed the fallback URL. Consequently the default same-host policy can reject a legitimate redirect on the connected WebSocket host as cross-host or a downgrade, while accepting a redirect to the undialed primary host as same-host. Track the endpoint that established the session and use that URL for redirect validation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. dialed uses the WebSocket fallback URL when that transport won, and still uses the primary when WebTransport did. A redirect that already replaced the URL does not keep judging against the old pin.

(Written by Grok 4.7)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. dialed uses the WebSocket fallback URL when that transport won, and still uses the primary when WebTransport did. A redirect that already replaced the URL does not keep judging against the old pin.

(Written by Grok 4.7)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e206e942cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 251 to +255
} else if (typ === StreamId.Goaway) {
const msg = await Goaway.decode(stream.reader, this.#version);
console.info("received goaway:", msg.uri);
// A peer sends at most one; a second is a protocol violation.
if (this.#goaway.peek() !== undefined) throw new ProtocolViolation("duplicate GOAWAY");
this.#goaway.set({ uri: msg.uri });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop opening streams after a lite GOAWAY

Propagate this drain state into the lite subscriber before leaving the session alive. During the handover window, a new origin request can still reach Subscriber and open a Subscribe or Fetch stream on this session, even though drafts/draft-lcurley-moq-lite.md:505 requires the recipient not to open new streams after GOAWAY. A compliant draining relay will reject that request instead of letting the old session serve only its existing groups. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that a GOAWAY should refuse new requests. Rust does that in check_going_away on every new subscribe, fetch, and announce, in both lite and IETF, while existing subscriptions keep flowing. Doing only the lite open sites would leave the IETF session with the same hole, and the handover still serves in-flight groups through the old route until the replacement answers. Leaving that as a follow-up on the drain line rather than expanding this migration.

(Written by Grok 4.7)

Comment on lines +253 to +255
// A peer sends at most one; a second is a protocol violation.
if (this.#goaway.peek() !== undefined) throw new ProtocolViolation("duplicate GOAWAY");
this.#goaway.set({ uri: msg.uri });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Close the lite session on duplicate GOAWAY

Close the transport when this duplicate check raises. In the lite dispatcher, the rejection only reaches #runBidis' catch, which resets the individual stream and leaves the session running, whereas drafts/draft-lcurley-moq-lite.md:1268 requires a duplicate GOAWAY to close the session with a protocol violation. The peer can therefore keep using the same nonconforming session until the handover timer expires. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 25ddec2. A ProtocolViolation from the lite bidi dispatcher now closes the session, matching the IETF path. The regression covers one GOAWAY leaving the session up and a second closing it.

(Written by Grok 4.7)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Review

Verdict: MERGE

Positive improvement?

Yes. Fleet drain needs clients to leave a withdrawn relay: empty-URI GOAWAY plus a fresh resolve. JS was the hole. Lite only logged the signal, IETF 17+ closed the session instead of migrating, and the draft-14 to -16 adapter dropped GOAWAY after reading it. Rust already migrated, but a malformed or policy-refused redirect quietly fell back onto the peer that asked us to leave, which is exactly what the quest forbids. Closing that loop, and holding an outranked origin route until the replacement answers so the path never unroutes mid-handover, is a real reliability win.

Worth the complexity?

Yes. Most of the bulk is tests (goaway, migrate, adapter/IETF GOAWAY, Rust fleet-drain with a test-only resolver table). The production shape mirrors Rust on purpose: same-host / follow / ignore, certificate-pin host hold, refuse scheme downgrades and local widen, never put the URI in the error (credentials), handover as a cap not a zero-on-absence. Pool re-key with identity-guarded removal and “resident target wins” is the honest cost of the overlap. Leaving Redirect::resolve lenient and skipping a public "migrating" status are the right compatibility choices for this line.

Different approach?

The rejected alternatives in the description are the right ones. Explicit Reload-side origin rewiring would duplicate what origin already does once the materialization gap is fixed. Keeping the refused-redirect fallback would reintroduce the bug. Deferring the in-tree relay e2e until relay-drain-api can drain on demand is fine; mock-transport coverage already locks the client behavior.

Notes (non-blocking)

  • Codex’s WebSocket dialed() fix is already in; judge redirects against the transport that actually connected.
  • Optional polish later: after a lite GOAWAY, refuse new streams / make duplicate GOAWAY tear the transport down explicitly. Session-keeps-serving is intentional for handover, so this is hardening, not a quest gap.
  • CI: Check is green; Test was still pending when this ran. Worth a glance before merge.
  • Named follow-ups already in the PR (Redirect::resolve fallible/private on dev, relay-drain-api e2e) look right.

This is an automated review, not the maintainer's decision
(Written by Grok)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d178b713d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread js/net/src/connection/reload.ts Outdated
Comment on lines +444 to +445
const hashes = this.webtransport?.serverCertificateHashes?.length ?? 0;
const pinned = hashes > 0 || this.webtransport?.serverCertificate !== undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply certificate pins only to WebTransport winners

When webtransport.serverCertificateHashes or serverCertificate is configured but the WebSocket fallback wins, pinned is still true even though that session never used the WebTransport pin. A cross-host GOAWAY under the explicit follow policy is therefore rejected solely because an unused pin was configured. Derive this restriction from connection.transport so it applies only when WebTransport established the session. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. pinnedTransport holds the host only when WebTransport is the session that connected. A WebSocket winner ignores a pin it never used.

(Written by Grok 4.7)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e6272f1769

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-tokio/src/connection.rs Outdated
// refused redirect is terminal: the peer is leaving and named somewhere we
// won't go, so redialing the old address or a fallback would ignore it.
let assigned = match &ended {
Ended::Goaway(msg) => goaway.redirect.target(msg.uri(), &url, client.pinned)?,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate certificate pins on the transport that used them

client.pinned is config-wide, so a valid fingerprint also constrains sessions established by transports that never use the Rustls verifier, such as tcp://, unix://, iroh://, or a plaintext WebSocket winner for http://. With goaway.redirect = Follow, a cross-host GOAWAY from one of those sessions now terminates with RefusedRedirect solely because an unrelated fingerprint was configured. The analogous JS path is transport-sensitive via pinnedTransport; carry the winning Rust transport's pin usage into this check as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed for the schemes that never install the Rustls verifier. fingerprint_pins keeps the pin on https, wss, moqt, and moql (an https WebSocket winner still uses that verifier) and skips http, ws, tcp, unix, and iroh.

(Written by Grok 4.7)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3541c87eea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread js/net/src/connection/goaway.ts Outdated
Comment on lines +87 to +88
export function target(policy: Redirect, uri: string, current: URL, pinned: boolean): URL | undefined {
if (uri === "" || policy === "ignore") return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate malformed URIs before applying ignore policy

When goaway.redirect is "ignore", this early return accepts any nonempty string without parsing it, so a malformed GOAWAY such as "not a url" silently reconnects to the old endpoint instead of producing the documented terminal RefusedRedirect. The Rust path has the same ordering at rs/moq-tokio/src/connection.rs:231, despite both public docs stating that malformed redirects end the connection. Parse and reject a nonempty URI before applying the ignore policy in both implementations. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L16-L18

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Both clients parse a nonempty URI before ignore. A malformed one is RefusedRedirect; a URI that parses is still ignored. An empty URI stays "reconnect here".

(Written by Grok 4.7)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac65920fbf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 342 to +344
const msg = await GoAway.decode(controlStream.reader, version);
console.warn(`received GOAWAY with redirect URI: ${msg.newSessionUri}`);
} else {
console.warn(`unexpected message on setup stream: 0x${typeId.toString(16)}`);
if (this.#goaway.peek() !== undefined) throw new ProtocolViolation("duplicate GOAWAY");
this.#goaway.set(msg.drain());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject client redirect URIs on accepted IETF sessions

When acceptAlpn or acceptSetup constructs this connection with client: false, a client-sent GOAWAY with a nonempty New Session URI is now published and the server continues serving. MoQ only permits servers to name a replacement, so an accepted server must close such a session with a protocol violation; previously this reader returned and its finally closed the connection. Preserve the local role and reject the redirect before setting #goaway; the v14-v16 adapter path needs the same check.

AGENTS.md reference: AGENTS.md:L16-L18

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. An accepted session (client: false) now closes with a protocol violation when the peer names a New Session URI, on v17+ and on the draft-14 to -16 adapter. An empty URI still drains. A client session still follows a server redirect.

(Written by Grok 4.7)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5aa974767b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return Ok(None);
}

if scheme_tier(target.scheme()) < scheme_tier(current.scheme()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unsupported redirect schemes before migration

When the current session uses unix:, a syntactically valid but unsupported hostless URI such as file:///tmp/next passes the default same-host policy: both schemes receive tier 0, both URLs are local, and both have no host. The loop then installs the URI and retries transport failures until the backoff expires instead of producing the documented terminal RefusedRedirect. Reject schemes that Client cannot dial before accepting the target. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L16-L18

Useful? React with 👍 / 👎.

kixelated and others added 14 commits September 25, 2026 11:45
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A malformed or policy-refused redirect used to fall back to the current
address list, redialing a peer that asked us to leave. It now ends the
connection with Error::RefusedRedirect. A tls.fingerprint pin refuses a host
change even under follow. Adds the fleet-drain regression test: an empty-URI
GOAWAY redials through a fresh resolve and hands the track over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Surface the peer's GOAWAY on every wire (lite, IETF 17+, and the draft-14 to -16
control stream adapter, which used to drop it) without closing the session.
The reconnect loop dials the replacement at once while the old session drains
to a handover cap, follows redirects under the Rust client's guard, and ends
with Error.RefusedRedirect on a refused one. The pool re-keys an entry to its
redirect target. An origin request holds an outranked route until the new one
answers, so the swap never unroutes the path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The drain signal brands its deadline as Time.Milli. The test passed a bare
number, which the typecheck rejects.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
A pinned websocket.url can win the transport race. same-host was comparing
the peer's redirect to the primary URL, which was never dialed.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
The dispatcher already treats a second GOAWAY as a protocol violation, but
only reset that stream. The session stayed up, which the draft does not allow.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
The pin is a WebTransport option. A WebSocket that won the race never used it,
so follow must not refuse a host change because an unused pin was configured.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
tcp, unix, iroh, and plaintext WebSocket never install the verifier, so a
configured fingerprint must not refuse their GOAWAY host change.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
ignore keeps a URI it could parse. One it cannot parse is still terminal,
matching the documented RefusedRedirect in both clients.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
Only a server may name a replacement. An accepted IETF session, on v17+ and
on the draft-14 to -16 adapter, closes that as a protocol violation.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
Two IntoRendition impls made config.into() ambiguous after the catalog
binary tracks landed on the drain line.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
@kixelated
kixelated force-pushed the quest/m1/drain/client-goaway branch from 5aa9747 to e8ae2a6 Compare September 25, 2026 18:47

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8ae2a606c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +469 to +473
console.info("GOAWAY received; migrating");
// A newer GOAWAY retires an older predecessor rather than holding two open.
this.#draining?.retire();
const cap = handover(this.goaway.handover ?? DEFAULT_HANDOVER, drain.timeout);
const draining = new Draining(connection, cap, () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prefer the replacement before draining the old session

When the replacement is not already the preferred provider, this handover never warms it before retiring the old session. With discovery disabled, serveRequests skips the replacement because the old session still owns slot.answer; with discovery enabled, a replacement route whose cost is higher than the old route is likewise never selected or enqueued. Only after the handover cap closes the old session does the replacement subscription open, so live groups produced during that subscribe gap can be dropped. Propagate the drain state into origin routing so the replacement can answer while the old provider remains as standby. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

throw new RefusedRedirect("the GOAWAY redirect leaves the host a certificate pin verifies");
}

return next;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject redirect schemes the JavaScript client cannot dial

An HTTPS session can accept a same-host redirect such as moqt://relay.example/ because moqt: has the same scheme tier, but the JavaScript connector only has WebTransport/WebSocket dial paths and cannot establish moqt:, moql:, or tcp: URLs. The accepted assignment then becomes sticky and the pooled/private loops, which use an unlimited retry window, back off forever instead of reporting the documented terminal RefusedRedirect. Validate the redirect against the schemes connect() actually supports before returning it. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L16-L18

Useful? React with 👍 / 👎.

@kixelated
kixelated merged commit 4c44009 into quest/m1/drain/README Sep 25, 2026
20 checks passed
@kixelated
kixelated deleted the quest/m1/drain/client-goaway branch September 25, 2026 19:20
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