Intermittent "Unknown node type 3" decoding AvailableCommandsPacket for OP players — colliding argument-type ids resolved by non-deterministic identity-hash order
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 2.3k
- Forks
- 960
- Avg merge
- 5d 10h
- Merged PRs (30d)
- 2
Description
Velocity — intermittent "Unknown node type 3" decoding AvailableCommandsPacket for OP players (per-JVM-start, cured by restart)
Summary
On Velocity 3.5.1, operator players on the proxy's highest native protocol (1.21.11 / 774)
are sometimes kicked when receiving the full command tree from any backend:
io.netty.handler.codec.CorruptedFrameException: Error decoding AvailableCommandsPacket
Direction CLIENTBOUND Protocol 1.21.11 State PLAY ID 0x10
Caused by: java.lang.IllegalArgumentException: Unknown node type 3
at ...AvailableCommandsPacket.deserializeNode(AvailableCommandsPacket.java:224)
Whether it happens is decided at proxy startup and stays fixed for the whole JVM session:
some starts are fine, some kick every OP on 774 from every 774 backend until restart.
A restart "cures" it (probabilistically). Non-OP players and lower (Via-translated) versions
are never affected.
Reproduction matrix (verified live): OP+1.21.11 → kick; deop+1.21.11 → fine; OP+1.17.1 → fine.
The captured failing packet decodes cleanly in a freshly-started instance (bytes are valid).
Root cause (confirmed from bytecode + two heap dumps)
ArgumentPropertyRegistry maps a numeric argument id → ArgumentIdentifier via:
public static ArgumentIdentifier readIdentifier(ByteBuf buf, ProtocolVersion v) {
int id = readVarInt(buf);
for (ArgumentIdentifier i : byIdentifier.keySet()) { // HashMap iteration order
Integer x = i.getIdByProtocolVersion(v);
if (x == null || x != id) continue;
return i; // returns the FIRST match
}
throw new IllegalArgumentException("Argument type identifier " + id + " unknown.");
}
Two problems combine:
- Structural id collisions. Velocity's Forge/CrossStitch argument identifiers reuse the same
registry ids as vanilla ones for recent protocols. Verified in the heap for both a healthy and a
corrupted proxy:forge:enum and minecraft:template_rotation -> id 50 (protocols 771,773,774,776...) forge:modid and minecraft:heightmap -> id 51 - Non-deterministic resolution.
ArgumentIdentifierdoes not overridehashCode/equals,
sobyIdentifier.keySet()iteration order follows identity hashes, which differ on every JVM
start.readIdentifierreturns the first identifier whose id matches — so for a colliding id it
returnsforge:enumon some starts andminecraft:template_rotationon others.
When a start happens to iterate the Forge identifier first, readIdentifier(50, 774) returns
forge:enum instead of template_rotation → the wrong ArgumentPropertySerializer reads the
argument properties with the wrong length → the parser desyncs → the next node's flags byte is
garbage → node type == 3 → exception. This only surfaces for OP players (only the full command
tree contains a node using a colliding argument id) and on the proxy's top native version, and it is
global (shared static registry). A restart re-randomizes identity hashes, hence the ~1-in-N
"lucky vs unlucky" boot and why restarting cures it.
Why the daily restart doesn't fix it
The scheduled restart re-rolls the ordering each morning; most days are "lucky". On an "unlucky"
boot every OP is kicked until the next restart (up to ~24h). It's a probabilistic boot condition,
not a time-based degradation.
Suggested fix (upstream)
- Make
readIdentifierdeterministic on id collisions (e.g. prefer the vanillaminecraft:*
identifier over Forge/CrossStitch), and/or - give
ArgumentIdentifiera stablehashCode/equalsplus a deterministic tie-break, and/or - ensure Forge/CrossStitch
VersionSets don't overlap vanilla ids for a given protocol.
Environment
- Velocity 3.5.1, Java 25.0.3 (Eclipse Adoptium), Linux 6.1 amd64
- ViaVersion 5.11.0 + ViaBackwards 5.11.0 (advertises up to 26.2/776)
- Backends: Purpur 1.21.11 (protocol 774); mixed-version network
Evidence
- Two heap dumps (an "unlucky/corrupt" session and a "lucky/healthy" one)
- The exact failing AvailableCommandsPacket (decodes OK on a clean instance)
- Extracted registry showing the structural id collisions (forge:enum/template_rotation=50, forge:modid/heightmap=51)
/velocity dumpoutput
Related
- #913 (modded command argument types causing decode errors)
- #1365 (Error decoding AvailableCommandsPacket)
Heap dumps and the raw failing packet are available on request (kept off the public issue as they contain server data). Happy to run any additional diagnostics.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with ArgumentPropertyRegistry.readIdentifier and inspect how ArgumentIdentifier entries are selected for protocol 774; then trace the result into AvailableCommandsPacket.deserializeNode. Confirm the handling of colliding identifiers is deterministic and verify that the full OP command tree decodes without the reported node-type error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- backend, networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100