PaperMC / PaperMC/Velocity

Plugin messages silently dropped during player join when getConnectedServer() is null

Open Beginner friendly
#1,766 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

type: bug
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
  1. Register a custom plugin message channel on Velocity
  2. 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)
  3. 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 via LoginSessionHandler line 158 / ConfigSessionHandler line 252)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.