Skip to content

Fix NoClassDefFoundError: CommandProtocol$1 on Paper plugin classloader - #3673

Merged
dmulloy2 merged 1 commit into
dmulloy2:masterfrom
1wairesd:fix/command-protocol-anonymous-class-classloader
Oct 6, 2026
Merged

dmulloy2 merged 1 commit into
dmulloy2:masterfrom
1wairesd:fix/command-protocol-anonymous-class-classloader

Conversation

@1wairesd

@1wairesd 1wairesd commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Problem

When ProtocolLib's update checker runs on Paper 1.19.3+ (and Purpur), the following exception is thrown repeatedly:

java.lang.NoClassDefFoundError: com/comphenix/protocol/CommandProtocol$1
at CommandProtocol.performUpdate(CommandProtocol.java:124)
at CommandProtocol.checkVersion(CommandProtocol.java:94)
at ProtocolLib.checkUpdates(ProtocolLib.java:524)
Caused by: java.lang.ClassNotFoundException: com.comphenix.protocol.CommandProtocol$1
at PaperPluginClassLoader.loadClass(...)

Root cause: performUpdate() creates an anonymous Runnable (compiled as
CommandProtocol$1.class) and registers it as an update listener.
SpigotUpdateRunnable dispatches it back to the main thread via
BukkitScheduler.runTask(). CraftTask.run() executes it under the server's
classloader instead of ProtocolLib's PaperPluginClassLoader — the anonymous
Outer$N class entry is not resolvable through the server classloader.

Fix

Extract the anonymous Runnable into a named top-level package-private class
UpdateNotifyRunnable — the same pattern applied in dda284c for
SpigotUpdateRunnable ("top-level class so the runtime always loads a single
.class entry"). CommandProtocol.java was not updated at that time.

No API changes. Behaviour is identical.

…ader

When BukkitScheduler.runTask() dispatches the update-notify Runnable back
onto the main thread, CraftTask.run() executes it under the server's
classloader rather than ProtocolLib's PaperPluginClassLoader. Anonymous
inner classes compiled as Outer\.class entries are not resolvable through
the server classloader, causing:

  java.lang.NoClassDefFoundError: com/comphenix/protocol/CommandProtocol\

The fix follows the pattern already applied in commit dda284c for
SpigotUpdateRunnable: extract the anonymous Runnable from performUpdate()
into a named top-level package-private class (UpdateNotifyRunnable) so
the runtime always loads a single .class entry through the plugin's own
classloader.
@dmulloy2
dmulloy2 enabled auto-merge (squash) October 6, 2026 22:00
@dmulloy2
dmulloy2 merged commit d89a5de into dmulloy2:master Oct 6, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants