Plugin messages silently dropped during player join when getConnectedServer() is null
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 2.3k
- Forks
- 959
- Avg merge
- 5d 10h
- Merged PRs (30d)
- 2
Description
Expected Behavior
Plugin messages sent on registered channels during the player join process should be forwarded to the backend server via getConnectionInFlight() when getConnectedServer() is null, the same way Forge Legacy handshake messages are. Messages should not be silently discarded.
Actual Behavior
ClientPlaySessionHandler.handle(PluginMessagePacket) at lines 303 to 310 only falls back to getConnectionInFlight() when the channel is LegacyForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL. For all other channels, when getConnectedServer() returns null during the join window, serverConn is set to null and the handler silently skips the message. No warning, no error, no exception, etc.
VelocityServerConnection serverConn =
(player.getConnectedServer() == null
&& packet.getChannel().equals(
LegacyForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL))
? player.getConnectionInFlight() : player.getConnectedServer();
The null window exists between player.setConnectedServer(null) at TransitionSessionHandler line 100 and player.setConnectedServer(serverConn) at line 140 (inside a thenRunAsync callback). During this window the client connection continues processing packets, but any non-Forge plugin message is dropped.
This is the same null window documented in issue #569, where ServerConnectedEvent is described as "fatally flawed" because Player#getCurrentServer() returns null during it.
Steps to Reproduce
- Register a custom plugin message channel on Velocity
- Have a plugin or client mod send a plugin message on that channel during the join process, while
getConnectedServer()is still null (e.g. a fast async callback such as a cached response completing during the join window) - Observe that the message is silently dropped with no log output
The bug is intermittent. It only triggers when the message arrives during the null window between setConnectedServer(null) and setConnectedServer(serverConn). A delayed retry reliably succeeds.
Plugin List
N/A - reproducible with any plugin that sends messages on a custom channel during the join process.
Discovered via Geyser + Floodgate. When Floodgate's websocket subscriber fails to connect to the Global API (e.g. temporary unavailability during startup), Geyser's FloodgateSkinUploader falls back to sending skin data via plugin message on the floodgate:skin channel. If the Global API responds quickly (cached skin), this message arrives during the null window and is silently dropped. The player appears as default skins to Java players.
Velocity Version
[18:08:06 INFO]: Velocity 3.5.0-SNAPSHOT (git-ab99bde9-b585)
[18:08:06 INFO]: Copyright 2018-2026 Velocity Contributors. Velocity is licensed under the terms of the GNU General Public License v3.
[18:08:06 INFO]: PaperMC - GitHub
Additional Information
Proposed fix: Remove the Forge channel gate on the getConnectionInFlight() fallback:
VelocityServerConnection serverConn = player.getConnectedServer();
if (serverConn == null) {
serverConn = player.getConnectionInFlight();
}
This is safe because the downstream code already guards against edge cases:
- Backend state check (line 311) rejects writes if the backend isn't in PLAY
- Phase completeness checks (lines 352-353) queue messages if phases aren't complete
- The in-flight connection's backend is already in
StateRegistry.PLAY(installed viaLoginSessionHandlerline 158 /ConfigSessionHandlerline 252)
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 in ClientPlaySessionHandler.handle(PluginMessagePacket), focusing on the server connection selection around lines 303-310, then read the connected-server transitions in TransitionSessionHandler around lines 100 and 140. Verify that registered plugin messages arriving during the null window are forwarded through getConnectionInFlight() and are no longer silently discarded.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- backend, networking
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100