Repository navigation
UpdateChecker
Basti edited this page Sep 27, 2026
·
2 revisions
Poll Modrinth and/or Hangar for fresh releases, then let your plugin decide how to notify players.
- Keep fleets of servers informed about new builds without manual checks.
- Provide redundant providers so your update notices still work if one API is down.
- Control prerelease inclusion, console spam, and join reminders through config.
- Parallel polling of Modrinth + Hangar with per-provider error info.
- Filter by prerelease status or server version before nagging admins.
- Async API calls returning a
CompletableFuture<UpdateInfo>, run on a small dedicated daemon thread pool (not the shared common pool). The pool starts on first use, its idle threads end on their own, andUpdateChecker.shutdown()stops it right away. - Hardened HTTP: HTTPS only, no redirects, 4 s to connect and to get the response headers, 10 s for the whole response, and a 1 MiB cap on each response. A stalled or oversized response is cancelled, which frees the connection; no thread waits on it meanwhile.
- Providers: only Modrinth and Hangar, at their public API addresses. GitHub, SpigotMC, CurseForge or your own endpoint aren't supported.
-
Paper only: the checker reads the plugin version with
getPluginMeta()and, for the server-version filter, the server version withBukkit.getMinecraftVersion(), both Paper API. - Hangar reads up to 100 versions: its list is read in pages of 25, at most four, i.e. the 100 newest uploads. An older upload with a higher version number, or the only one for this server, isn't seen.
-
Paper family only, always: a Modrinth build counts only if its
loadersincludepaper,spigot,bukkit,purpurorfolia, a Hangar build only if itsplatformDependencieslistPAPER; builds without loader or platform data count too. This can't be switched off. -
Server-version filter: off unless you use the 6-argument constructor. The server version is cut at the first
-(26.3-pre1counts as26.3), and anything in the samemajor.minorseries matches, so a build listed only for26.3.5is offered on26.3.0. If the server version can't be read, the filter is off without a log line. -
Prereleases on Hangar are a best guess: Hangar has no version type, and its
Unstablechannel flag is often left off. A version counts as a prerelease if its channel has that flag, or if a channel not namedReleasehas a name likeSnapshot,BetaorDev Buildsor the version itself carries a prerelease label (2.1.0-SNAPSHOT). So a prerelease channel with another name (e.g.Bleeding) and plain version numbers is taken for releases. On Modrinth the version type decides; entries without a type or channel count as releases. -
Provider mode and IDs: a
providerModeother than0,1or2throws anIllegalArgumentException. In mode0, a provider whose slug or namespace is empty or invalid is skipped; in mode1or2it returns an error result on every check. -
ID format: Modrinth slugs may only contain letters, digits,
_and-(use the project ID if your slug has other characters); Hangar needs exactlyUser/Project. -
HTTP: HTTPS only. A redirect, any status other than
200(including429 Too Many Requests) or a reply over 1 MiB fails the check for that provider. Modrinth's whole version list is fetched at once, changelogs included, so a project with very many versions can hit the 1 MiB cap. -
No caching, retries or scheduling: every
checkAsync()sends fresh requests; scheduling and backing off are up to you. -
Own logging: by default the checker logs a
WARNINGin English for every failed provider on every check (offline,404,429, β¦);setLogFailures(false)turns that off. The user agent is always<PluginName>-UpdateChecker. -
Shared threads: all checkers of your plugin share two daemon threads, so a callback that blocks delays other checks;
shutdown()is static and stops all of them at once. -
Version names: a published version counts only if its name (Modrinth
version_number, Hangarname) has at most 64 characters, starts with a letter or digit and otherwise holds only letters, digits and.,-,+,_. Other entries are skipped, solatestVersionnever carries line breaks, control characters or huge texts into your logs or chat; a project that names its versions differently (e.g.2.0 beta) gets no update offered. -
Version format: the running version and every published version are compared with VersionComparator, with its limits (e.g. unknown prerelease labels rank after
-rc, a date with dashes below its year).
- Decide which providers to poll (both, Modrinth only, or Hangar only).
- Instantiate
UpdateCheckerwith provider flag, prerelease toggle, and provider identifiers. - Schedule
checkAsync()on a repeating task and act oninfo.hasUpdate().
UpdateChecker has no config of its own β it is a plain constructor-based API. Whether to check at all, how often, and which keys to read from your plugin's own config.yml is entirely up to the consuming plugin (see timberella-plugin/underwatertrees-plugin for one convention).
| Constructor Parameter | Type | Purpose |
|---|---|---|
plugin |
Plugin |
Owning plugin, used for logging, the current version, and the user agent (<PluginName>-UpdateChecker) |
providerMode |
int |
0 both, 1 Modrinth, 2 Hangar |
includePrereleases |
boolean | Whether to treat beta/rc builds as upgrades |
filterByServerVersion |
boolean | Optional overload (default false); restrict matches to the running server's major.minor Minecraft version |
modrinthSlug |
string | Project slug (letters, digits, _, -; max 64 chars) |
hangarNamespace |
string |
User/Project namespace (same character rules per part) |
Constructors: UpdateChecker(plugin, providerMode, includePrereleases, modrinthSlug, hangarNamespace) and UpdateChecker(plugin, providerMode, includePrereleases, filterByServerVersion, modrinthSlug, hangarNamespace).
setLogFailures(boolean) (default true): whether a failed provider is logged as a WARNING. Turn it off when you show ProviderResult#errorMessage() yourself.
| Result type | Contents |
|---|---|
UpdateInfo(currentVersion, latestVersion, providers) |
latestVersion is the higher of the providers' versions if it is greater than the running version, else null; hasUpdate() is true when it is set. |
ProviderResult(provider, latestVersion, url, errorMessage, metadata, compatibilityUnknown) |
Per-provider outcome; success() is errorMessage == null, otherwise errorMessage is a short English reason (HTTP 404, timeout, no connection, connection error, response over 1 MiB, unexpected response, cancelled, unexpected error, or No project slug configured / No project namespace configured when Modrinth / Hangar has no valid project set). A successful result with latestVersion == null means the provider answered but lists no eligible version (e.g. none for this Minecraft version yet) - show it as "no update", not as an error. metadata is the raw Gson JsonObject of the chosen version; url is the project page. latestVersion always follows the version-name rule under Limitations. |
Provider |
MODRINTH / HANGAR, with displayName(). |
-
Modrinth β supply the project slug (e.g.,
timberella). -
Hangar β supply the namespace
User/Project(case sensitive). - Set providers to
0to poll both simultaneously; the higher of the two versions wins. Each provider reports its highest eligible version by VersionComparator, not its newest upload, so a backport uploaded after2.0.0doesn't hide it. - An invalid slug/namespace is logged as a warning and treated as "not configured": in mode
0that provider is skipped, in mode1or2it reports an error result instead of failing startup. - Version numbers: publish them on Modrinth/Hangar in the VersionComparator format (
1.4.0,1.4.0-beta2), matching the plugin's own version; common variants likev1.4.0or1.4.0-beta.2are read the same way. - Prereleases: Modrinth versions with
prerelease: trueor aversion_typeother thanrelease; on Hangar see the limitation above. - Server-version filter: Modrinth uses
game_versions, Hangar uses the Paper list inplatformDependencies(e.g.{"PAPER": ["26.2"]}). A version matches if it lists the exact server version or anything in itsmajor.minorseries (26.3,26.3.1,26.3.x). Versions listing other game versions are skipped; if nothing matches, the highest entry without any compatibility data is used and flaggedcompatibilityUnknown = true.
import net.kroet.turtlelib.helper.UpdateChecker;
UpdateChecker checker = new UpdateChecker(
this,
0, // 0: both, 1: Modrinth, 2: Hangar
false, // include prereleases?
"my-slug",
"User/Plugin"
);
checker.checkAsync().thenAccept(info -> {
if (info.hasUpdate()) {
getLogger().info(() -> "Update available: " + info.latestVersion());
} else {
getLogger().fine("Already up to date.");
}
});- Each provider result includes its own success flag and error text. The checker already logs failed providers as
WARNING, so don't log them a second time, or callsetLogFailures(false)and showerrorMessage()in your own, translated line, as Timberella and UnderwaterTrees do. - When every provider fails, treat it as "no data" instead of crashing startup and retry later.
- Back off to hourly checks (
>= 1 hour) to avoid rate limits and HTTP 429 responses. - Uses only public REST endpoints; no API tokens required.
-
checkAsync()callbacks run on the checker's worker thread (turtle-lib-UpdateChecker), not the Bukkit main thread; hop back via the scheduler before touching Bukkit APIs. If the result is already complete when you attach the callback, it runs right away on your thread. - Call
UpdateChecker.shutdown()inonDisable(). It stops the HTTP client and the worker threads at once (also on/reload); a check still running then ends quietly with no update, and the nextcheckAsync()starts fresh ones. - Gson comes with the Paper API, so reading
ProviderResult#metadata()needs nothing beyond yourcompileOnlyPaper API dependency.
- Uses VersionComparator internally:
latestVersionis only set when a provider's version is newer than the running one. - Surface update summaries via MessageService so locale files control how notifications look.