Skip to content

relay: extra frame elements are ignored where the capture rejects them #961

Open

Description

@chappie-daemon

Every frame parser reads its fields by index and ignores anything past them, so ["CLOSE","c1","junk"] is accepted. The capture rejects it: src/schemas/message-schema.ts:56 is z.tuple([z.literal(MessageType.CLOSE), subscriptionSchema]), and a Zod tuple with two elements fails on a third.

The same shape recurs across the parser in crates/nostream-core/src/messages.rs — CLOSE at :359-365, and the other frame types follow it — so the fix is one arity check per frame, or a shared helper that takes the expected length.

Why it matters

Not for the extra element itself, but for what it says about the parser: a client that sends a malformed frame differing only in arity gets a different answer from each implementation, and the wire-parity oracle cannot catch it because no recorded session carries an over-long frame.

Deciding check

A table-driven test over wire-sessions.json's frame types plus one over-long variant each, asserting the same rejection the capture produces (Invalid input/NOTICE as the capture's adapter does), and one under-long variant to prove the lower bound was never checked either.

Measured on rewrite/main at 740ef69: pre-existing, outside #950–#952's diff.

Activity

  1. added
    bugSomething isn't working
    rewritenostream 4.0 Rust/LMDB rewrite
    rustRust implementation work
    factoryBuilt by the software factory
    on Oct 24, 2026
  2. chappie-daemon commented on Oct 25, 2026

    @chappie-daemon
    CollaboratorAuthor

    Fixed in factory/013-inbound-decode-parity (PR to follow), with three corrections to this issue's own text.

    Only three verbs ignored extra elements. EVENT, AUTH and CLOSE accepted them; REQ and COUNT already rejected extras and capped at twelve (filterSchema + superRefine).

    The lower bound was already checked on all five verbs, and matched the capture — so the "prove the lower bound was never checked either" half of the deciding check had nothing to prove.

    The rejection string is not Invalid input. For an over-long tuple the capture answers Array must contain at most 2 element(s), and Zod records that as an issue while a wrong-typed element aborts the union — different mechanism, different string. The fix asserts the measured one.

    The ordering matters and is now pinned both ways: the arity check runs after each arm's element checks, so ["CLOSE","c1","junk"] → Array must contain at most 2 element(s) while ["CLOSE",42,"junk"] stays Invalid input, as the capture does. A frame that is over-long and dirty keeps the element's message — that remaining difference is recorded in the decoder's doc and tracked in #979.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingfactoryBuilt by the software factoryrewritenostream 4.0 Rust/LMDB rewriterustRust implementation work

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions