fix(identify): handle multiple envelopes from go#3423
Conversation
|
Hi @wjmelements do you think you could reproduce this behavior inside of a test? |
byteStream will only push the read buffer back to the stream if there are bytes leftover. any idea what those bytes are? |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
I pushed a test that reproduces the same exception I was seeing.
It's To summarize that code, the identify response gets split up if there is a SignedPeerRecord and the message will be larger than |
This is not spec-compliant behaviour, the spec clearly says the remote should send To move forward here we should probably read multiple identify messages until the stream closes (with an appropriate timeout) and compose the final message from whatever we receive. We definitely want to prefer signed peer records if they are available. |
|
It looks like go iterates over 10 messages with a 5 second timeout for the identify stream. It collects all the messages and merges them before writing to the peer store. All identify responses are dropped if the stream times out. |
Ok, I plan to implement similar behavior for js. |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
There is an update to the Identify spec with message-splitting taking shape here. Any changes made here should make the js implementation conform to the newly more detailed spec. |
Assisted-by: Claude:claude-sonnet-4-6
…ge has addresses Assisted-by: Claude:claude-sonnet-4-6
Reverts buildIdentifyMessages, packItems, and the deferred-SPR path so identify and identify-push send a single message. Receive-side multi-message support is retained. Multi-message send is deferred to a follow-up so older js-libp2p receivers without the close-error fix in this PR don't regress on identify when this ships. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns listenAddrs merge with the existing protocols dedup. Duplicates across multiple identify messages would otherwise inflate the peerStore entry. Invisible in practice today (no senders duplicate) but worth locking in to prevent future drift. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Centralises the check used by both _identify and identify-push handlers to decide whether a read error after at least one successful message means "remote finished" vs "real failure". Treats UnexpectedEOFError and "remote write side no longer writable" as equivalent EOF signals so transport-level resets after success aren't dropped as failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
C2: wrap post-read close() in try/catch with trace log. The original bug in this area was that StreamStateError from byteStream.unwrap() pushing buffered trailing bytes back to a closed-read stream propagated through the outer catch and aborted the stream — discarding the successfully-read message. Swallowing the close error preserves the read result, which is what the caller actually wanted. I1: import MAX_IDENTIFY_MESSAGES from consts instead of shadowing it locally with a literal. I2: log when the cap is reached without seeing EOF, so silent truncation surfaces in operator logs. I4: replace UnexpectedEOFError-only break with isEofLike, which also treats "remote write side no longer writable" as EOF after at least one successful message — catches stream-reset-after-success as a success path. Co-Authored-By: William Morriss <wjmelements@gmail.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the changes to identify._identify so the push receive handler also: imports MAX_IDENTIFY_MESSAGES from consts, uses isEofLike for the break condition, logs at the cap-reached boundary, and swallows close errors with a trace log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds T1 (empty stream rejection), T2-cap (cap boundary), T3 (non-EOF mid-stream), T-reset (reset-after-success returns merged result), and T-close-swallow (C2 regression: close error after read does not discard the message). Equivalent push-receive tests added. Removes "should order public addresses before private in the identify response" — the assertion no longer guards real behavior since the in-protocol address sorter was removed in the multi-send revert. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move @libp2p/peer-record after @libp2p/peer-id to satisfy the existing import/order eslint rule. Pre-existing — was originally introduced in aa154eb but not flagged until lint ran on this branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The post-loop `if (messages.length === 0) throw new InvalidMessageError(...)` in both _identify and the push handleProtocol was unreachable: the read-loop catch only breaks on EOF when messages.length > 0, otherwise it re-throws the underlying UnexpectedEOFError. The empty-stream case is therefore handled by the propagating EOF, never by the post-loop check. Removes the dead block in both files. identify-push no longer needs the InvalidMessageError import. Behavior is unchanged — empty streams continue to reject with UnexpectedEOFError, matching the existing test assertions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PeerStore handles dedup of incoming listenAddrs downstream, so deduping in the merge step was redundant. Restores the prior plain-concat behavior for listenAddrs while keeping the Set-based dedup on protocols (string ids). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n close error Drops the isEofLike helper (and its standalone unit tests) — the check is inlined at both call sites, using err.name === 'UnexpectedEOFError' (the codebase's bundle-safe error-discrimination idiom) plus the stream-state fallback for transport-level resets. In identify._identify, the close call moves from pb.unwrap().unwrap().close to stream.close — same effect, simpler chain, matches identify-push. If close throws, abort the stream so transport-level cleanup runs even though the read result is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Any error after at least one successful read keeps the messages we got and breaks the loop. The error name and stream-state checks come out — the data we've accumulated is already-validated parsed envelopes regardless of why the next read failed, and applying it is consistent with the spec's "missing fields ignored" merge semantics. T3 updates from "rejects on garbage" to "returns the message before garbage" to match the new behavior. The trailing comment referencing the removed isEofLike helper is also cleaned up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both identify._identify and identify-push.handleProtocol had near-identical multi-message read + close + abort logic. Extracts the common shape to utils.ts so the two call sites collapse to a single helper call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ages units Adds 5 unit tests against readIdentifyMessages in utils.spec.ts: happy path, empty-stream rejection, cap boundary, lenient-catch on read error after success, and close-error preserves messages with abort. Removes the integration-level equivalents from index.spec.ts (5) and push.spec.ts (2) that were exercising the same helper paths through the full identify / push handler. Coverage of "helper is wired in correctly" stays in the existing "should merge multiple identify messages from the remote" and "should handle multiple push messages and merge them" tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ close-throws The recent dedup move pulled the helper-behavior tests into utils.spec.ts as direct readIdentifyMessages units, but in doing so dropped end-to-end verification that those error/recovery surfaces flow through the public caller (Identify.identify / IdentifyPush.handleProtocol). A future refactor of the caller wrapper could regress without any unit failure surfacing it. Adds 4 short integration assertions (~15 lines each, much smaller than the originals): empty-stream UnexpectedEOFError propagation and close-error recovery, on both identify and identify-push code paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
apologies for the delay. where this pr landed: receive: handles up to 10 lp-framed identify messages and merges them. lets us interop with go-libp2p which splits the signedPeerRecord into a second message when identify is over 2kb. original close-crash gets fixed by send: still single-message. sending multi-message would crash older js-libp2p receivers that still have the close-crash. this pr is what lets them stop crashing so once it's deployed we can turn on multi-send too, depending on how the spec changes solidify. cross-impl: looked at the 12 active libp2p stacks at libp2p/libp2p. 4kb is the practical outbound ceiling, rust-libp2p and litep2p both cap there. today's identify is comfortably under that without post-quantum keys so this deferred: multi-message send, outbound 4kb cap + trim, defaultMultiaddrSorter (only meaningful when splitting), partial-diff identify-push. spec pr libp2p/specs#709 still has open questions around envelope sizes for |
|
tested against:
using libp2p/test-plans. no failures. |
Reviewer @tabcat
This was causing failures with identify when I was doing a quick Helia bitswap with Kubo.
Kubo closes the stream fully immediately after sending the identify response.
The JS identify client calls pb.unwrap().unwrap().close() after pb.read(); this throws StreamStateError because byteStream.unwrap() tries to push the internal read buffer onto a stream whose readStatus is closed.
The error is caught and the stream aborted, discarding a successfully-read message, so consumeIdentifyMessage is never called, and peer:identify never fires.
Consequently, Helia never gets onConnect called for Kubo peers.
(I'm not too familiar with this code so maybe there is a better way to fix this.)
Changes