Describe the bug
When subscribersCount == 1 (Floodgate's websocket is not connected), FloodgateSkinUploader sends skin data to the backend via PluginMessageUtils.sendMessage() on the floodgate:skin channel. This message is sent during the player join process and arrives at Velocity while getConnectedServer() is still null, causing Velocity to silently drop it. The skin never reaches the backend and the player appears as Steve/Alex to Java players.
The subscribersCount == 1 condition occurs when Floodgate's websocket subscriber fails to connect to the Global API, for example due to the Global API being temporarily unavailable during Floodgate's startup. In the standard Geyser + Floodgate setup, subscribersCount is typically 2+ (Geyser + Floodgate proxy + Floodgate backend), so this fallback path is skipped and skin delivery happens through Floodgate's websocket directly. This is why the bug has gone unnoticed.
I've filed a separate Velocity bug report for the underlying silent drop issue (PaperMC/Velocity#1766), but regardless of whether Velocity fixes it, Geyser should not be sending plugin messages during the join window when there is no guarantee the proxy has a connected server to forward them to.
To Reproduce
- Run Geyser + Floodgate on Velocity
- Create a condition where
subscribersCount == 1 (e.g. Floodgate's websocket fails to connect to the Global API during startup)
- Have a Bedrock player join. The skin upload is initiated in
JavaLoginFinishedTranslator (line 69) during the LOGIN to CONFIGURATION transition
- If the Global API responds quickly (cached skin), the
SKIN_UPLOADED websocket callback in FloodgateSkinUploader fires PluginMessageUtils.sendMessage() while the player is still connecting
- The plugin message is silently dropped by Velocity and the skin never reaches the backend
The bug is intermittent. It only triggers when the Global API responds fast enough that the plugin message arrives during the null window between setConnectedServer(null) and setConnectedServer(serverConn) in Velocity's TransitionSessionHandler.
Expected behaviour
The skin plugin message should reliably reach the backend server. The subscribersCount == 1 fallback path should account for the fact that the proxy may not have a connected server ready when the message is sent.
Screenshots / Videos
No response
Server Version and Plugins
This server is running Paper version 1.21.11-127-main@bd74bf6 (2026-03-10T02:55:23Z) (Implementing API version 1.21.11-R0.1-SNAPSHOT)
Velocity 3.5.0-SNAPSHOT (git-ab99bde9-b585)
Geyser Dump
No response
Geyser Version
2.9.5-SNAPSHOT
Minecraft: Bedrock Edition Device/Version
No response
Additional Context
Affected code: FloodgateSkinUploader.java, SKIN_UPLOADED case (line ~124-148). PluginMessageUtils.sendMessage() calls session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(...)) which goes through the proxy and hits the silent drop.
Proposed fix: Wrap the sendMessage call in a delay to ensure the server connection is established:
geyser.getScheduledThread().schedule(() -> {
PluginMessageUtils.sendMessage(session, PluginMessageChannels.SKIN, bytes);
}, 5, TimeUnit.SECONDS);
5 seconds is well past the join window in all cases. Nobody notices a skin appearing a few seconds after join, as skins regularly load with a short delay via the normal player list update mechanism.
Describe the bug
When
subscribersCount == 1(Floodgate's websocket is not connected),FloodgateSkinUploadersends skin data to the backend viaPluginMessageUtils.sendMessage()on thefloodgate:skinchannel. This message is sent during the player join process and arrives at Velocity whilegetConnectedServer()is still null, causing Velocity to silently drop it. The skin never reaches the backend and the player appears as Steve/Alex to Java players.The
subscribersCount == 1condition occurs when Floodgate's websocket subscriber fails to connect to the Global API, for example due to the Global API being temporarily unavailable during Floodgate's startup. In the standard Geyser + Floodgate setup,subscribersCountis typically 2+ (Geyser + Floodgate proxy + Floodgate backend), so this fallback path is skipped and skin delivery happens through Floodgate's websocket directly. This is why the bug has gone unnoticed.I've filed a separate Velocity bug report for the underlying silent drop issue (PaperMC/Velocity#1766), but regardless of whether Velocity fixes it, Geyser should not be sending plugin messages during the join window when there is no guarantee the proxy has a connected server to forward them to.
To Reproduce
subscribersCount == 1(e.g. Floodgate's websocket fails to connect to the Global API during startup)JavaLoginFinishedTranslator(line 69) during the LOGIN to CONFIGURATION transitionSKIN_UPLOADEDwebsocket callback inFloodgateSkinUploaderfiresPluginMessageUtils.sendMessage()while the player is still connectingThe bug is intermittent. It only triggers when the Global API responds fast enough that the plugin message arrives during the null window between
setConnectedServer(null)andsetConnectedServer(serverConn)in Velocity'sTransitionSessionHandler.Expected behaviour
The skin plugin message should reliably reach the backend server. The
subscribersCount == 1fallback path should account for the fact that the proxy may not have a connected server ready when the message is sent.Screenshots / Videos
No response
Server Version and Plugins
This server is running Paper version 1.21.11-127-main@bd74bf6 (2026-03-10T02:55:23Z) (Implementing API version 1.21.11-R0.1-SNAPSHOT)
Velocity 3.5.0-SNAPSHOT (git-ab99bde9-b585)
Geyser Dump
No response
Geyser Version
2.9.5-SNAPSHOT
Minecraft: Bedrock Edition Device/Version
No response
Additional Context
Affected code:
FloodgateSkinUploader.java,SKIN_UPLOADEDcase (line ~124-148).PluginMessageUtils.sendMessage()callssession.sendDownstreamPacket(new ServerboundCustomPayloadPacket(...))which goes through the proxy and hits the silent drop.Proposed fix: Wrap the
sendMessagecall in a delay to ensure the server connection is established:5 seconds is well past the join window in all cases. Nobody notices a skin appearing a few seconds after join, as skins regularly load with a short delay via the normal player list update mechanism.