Skip to content

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.

Why Use It? 🎯

  • 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.

Highlights ✨

  • 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, and UpdateChecker.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.

Limitations ⚠️

  • 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 with Bukkit.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 loaders include paper, spigot, bukkit, purpur or folia, a Hangar build only if its platformDependencies list PAPER; 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-pre1 counts as 26.3), and anything in the same major.minor series matches, so a build listed only for 26.3.5 is offered on 26.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 Unstable channel flag is often left off. A version counts as a prerelease if its channel has that flag, or if a channel not named Release has a name like Snapshot, Beta or Dev Builds or 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 providerMode other than 0, 1 or 2 throws an IllegalArgumentException. In mode 0, a provider whose slug or namespace is empty or invalid is skipped; in mode 1 or 2 it 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 exactly User/Project.
  • HTTP: HTTPS only. A redirect, any status other than 200 (including 429 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 WARNING in 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, Hangar name) has at most 64 characters, starts with a letter or digit and otherwise holds only letters, digits and ., -, +, _. Other entries are skipped, so latestVersion never 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).

Quick Start πŸš€

  1. Decide which providers to poll (both, Modrinth only, or Hangar only).
  2. Instantiate UpdateChecker with provider flag, prerelease toggle, and provider identifiers.
  3. Schedule checkAsync() on a repeating task and act on info.hasUpdate().

References πŸ“š

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().

Provider Details 🌐

  • Modrinth β†’ supply the project slug (e.g., timberella).
  • Hangar β†’ supply the namespace User/Project (case sensitive).
  • Set providers to 0 to 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 after 2.0.0 doesn't hide it.
  • An invalid slug/namespace is logged as a warning and treated as "not configured": in mode 0 that provider is skipped, in mode 1 or 2 it 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 like v1.4.0 or 1.4.0-beta.2 are read the same way.
  • Prereleases: Modrinth versions with prerelease: true or a version_type other than release; on Hangar see the limitation above.
  • Server-version filter: Modrinth uses game_versions, Hangar uses the Paper list in platformDependencies (e.g. {"PAPER": ["26.2"]}). A version matches if it lists the exact server version or anything in its major.minor series (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 flagged compatibilityUnknown = true.

Examples πŸ§ͺ

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.");
    }
});

Tips & Notes πŸ’‘

  • 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 call setLogFailures(false) and show errorMessage() 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() in onDisable(). 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 next checkAsync() starts fresh ones.
  • Gson comes with the Paper API, so reading ProviderResult#metadata() needs nothing beyond your compileOnly Paper API dependency.

Relationships to Other Helpers πŸ”—

  • Uses VersionComparator internally: latestVersion is 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.

Clone this wiki locally