Skip to content

Skin plugin message dropped during join when subscribersCount == 1 #6288

@SendableMetatype

Description

@SendableMetatype

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

  1. Run Geyser + Floodgate on Velocity
  2. Create a condition where subscribersCount == 1 (e.g. Floodgate's websocket fails to connect to the Global API during startup)
  3. Have a Bedrock player join. The skin upload is initiated in JavaLoginFinishedTranslator (line 69) during the LOGIN to CONFIGURATION transition
  4. If the Global API responds quickly (cached skin), the SKIN_UPLOADED websocket callback in FloodgateSkinUploader fires PluginMessageUtils.sendMessage() while the player is still connecting
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions