Managing Plugins

Managing Plugins

Complete guide to the Installed tab — viewing, filtering, updating, enabling/disabling, uninstalling, upload detection, ZIP export, and all possible errors.


Installed Plugins List

Two sources:

  1. Browser-installed — Full tracking (provider, version, plugin_id, download URL, update detection)
  2. Manually uploaded — Auto-detected by scanner, registered as "Manual / Upload" with limited tracking

Columns

Column Description
Icon Provider logo (empty for uploads)
Name From JAR plugin.yml or filename (version regex removed)
Status Active, Disabled, or Unknown
Provider SpigotMC, CurseForge, Modrinth, Hangar, GeyserMC, or Upload
Version Installed version number
Installed At First installation timestamp
Latest Version Latest available on platform (if tracked)

Upload Detection

scanAndRegisterUploadedPlugins() runs automatically:

  • Compares JAR files on disk (/plugins/) with database entries
  • New files: Registered with provider 'upload'. Opens JAR to extract plugin.yml for name, or parses version from filename regex -(\d+\.\d+(?:\.\d+)?)
  • Deleted files: Removed from database (cleanup)
  • Concurrency protection: Atomic lock with 10-second TTL
  • Limitation: Upload plugins have no auto-update detection

Display Name Resolution

Provider Name Source
GeyserMC Capitalize project name
PaperMC Slug part of owner/slug
CurseForge Slug if version-prefixed
Others JAR plugin.yml → filename fallback

Filtering

Filter Shows
Show All All plugins
Active Only enabled plugins
Disabled Only disabled plugins (with -bak suffix)
Updates Available Only plugins with newer platform versions

Plugins with updates float to the top, then alphabetical.

Filter Preservation

The plugin uses resetTableKeepingFilters() — saves filter state before Filament resets, restores after. This prevents losing the "has_updates" filter when navigating.


Updating Plugins

How Update Detection Works

  • Plugin queries source platform by stored plugin_id and provider
  • Update metadata 24 hours cached per plugin
  • 'upload' provider always skipped
  • String comparison for version IDs (not semantic versioning)

Special: Update-While-Disabled

If a plugin is disabled (has -bak suffix) when an update is installed:

  • The is_disabled state is remembered before the update
  • After update, the plugin is re-disabled automatically
  • Your disabled state is preserved across updates

Console Widget

  • Syncs plugins on load
  • Update check with 15-minute cache
  • Warning badge + redirect URL to Plugin Browser
  • Same pattern as Game Mods widget

Enable / Disable Plugins

Mechanism

File rename via read+write+delete (for Daemon API compatibility):

Action Steps Example
Disable 1. Read file 2. Write as {name}-bak 3. Delete original EssentialsX-2.20.jarEssentialsX-2.20.jar-bak
Enable 1. Read -bak file 2. Write as original name 3. Delete -bak EssentialsX-2.20.jar-bakEssentialsX-2.20.jar

Note: Three Daemon API calls per toggle. Server may need to be stopped first.

Notifications

Action Success Error
Disable "Plugin disabled: [Name]" "Disable failed" + error
Enable "Plugin enabled: [Name]" "Enable failed" + error

Uninstalling Plugins

Process

  1. Click Uninstall → confirm warning
  2. JAR file deleted from /plugins/
  3. Database entry removed
  4. Notification: "Plugin uninstalled"
  5. Restart server to fully unload

What's NOT Removed

Not removed Example Cleanup method
Config files plugins/Essentials/config.yml File manager
Data files plugins/LuckPerms/luckperms-*.db File manager
Permissions Permission nodes in LuckPerms etc. Permission plugin UI
World data Custom structures, blocks Not reversible

Open Plugins Folder

Jumps to /plugins/ in the file manager. Useful for:

  • Checking actual files on disk
  • Manual uploads
  • Cleaning up config directories after uninstall
  • Verifying -bak extensions on disabled plugins

ZIP Export

Process

  1. Click Download All as ZIP
  2. Notification: "ZIP download started"
  3. Server compresses all files in /plugins/
  4. JWT-signed URL generated (expires after 30 minutes)
  5. Download starts automatically

Contains: All plugins (active + disabled)


Complete Error Reference

"Failed to read installed plugins"

  • /plugins/ directory doesn't exist → start server once
  • Directory not readable → check Daemon permissions
  • Disk full
  • Daemon connection lost

"Failed to disable/enable plugin"

  • Permissions: Read + write + delete required on /plugins/
  • File locked: Running server locks files → stop server first
  • Concurrent access: Backup or file manager accessing simultaneously
  • File not found: Externally renamed or deleted
  • Daemon error: Any of the 3 steps (read/write/delete) can fail

"Failed to delete old versions"

  • Old file locked by running server → stop server
  • File permission issue
  • File already manually removed

Updates not showing

  • Update metadata 24 hours cached → clear cache
  • 'upload' plugins always skipped
  • Version comparison is string-based
  • Provider may not have propagated update yet

"Uninstall failed"

  • File already manually deleted
  • File locked by server process → stop and retry
  • Daemon connection issue

Plugin crashes server after install

  1. Check console output / crash report
  2. Disable the plugin (don't delete!) + restart to confirm
  3. Are all dependencies installed?
  4. Check plugin compatibility with your MC version/server software
  5. Try an older stable version
  6. Check Java version (17 for MC 1.18+, 21 for MC 1.21+)

"Cache clear failed"

  • Error notification shows $e->getMessage()
  • SafeCacheService flush() couldn't remove keys
  • storage/framework/cache/ must exist and be writable (0755)

"Cache health check failed"

  • Warning logged by PluginManager
  • Cache infrastructure may need repair
  • php artisan cache:clear as full reset

Provider Internal Functions Reference

This section documents the internal PHP code for each provider integration, including API parameters, response schemas, rate limits, and response processing logic.

SpigotMC / Spiget Provider — Complete Reference

API Endpoint Parameters — GET /v2/search/resources/{query}

Parameter Type Required Default Description
query string (path) Yes Search term (URL-encoded, in path)
size int No 10 Results per page (max 100)
page int No 1 1-based page number
sort string No Sort field, prefix - for descending (e.g. -downloads)
fields string No Comma-separated field names to include

API Endpoint Parameters — GET /v2/resources (browse without query)

Parameter Type Required Default Description
size int No 10 Results per page
page int No 1 Page number
sort string No -downloads Default sorted by most downloads

Response Schema — Search Results

Field Type Description
[].id int SpigotMC resource ID
[].name string Resource name
[].tag string Short description / tag line
[].downloads int Total download count
[].rating.average float Average user rating (0–5)
[].icon.url string Relative icon path (prepend Spiget CDN)
[].testedVersions string[] MC versions listed as compatible
[].premium bool True if paid resource
[].external bool True if hosted outside SpigotMC
[].updateDate int Unix timestamp of last update
[].releaseDate int Unix timestamp of initial release
Header X-Total int Total number of matching resources (for pagination)

Rate Limits

Tier Limit Notes
Unauthenticated ~600 req/10min per IP Spiget uses sliding window
When exceeded HTTP 429 No Retry-After header, wait ~60s
// Search SpigotMC resources — with total count from header
$response = Http::timeout($timeout)
    ->get("https://api.spiget.org/v2/search/resources/{$query}", [
        'size' => $perPage,
        'page' => $page,
        'sort' => '-downloads',
    ]);

// Total count is in the X-Total response header (not in JSON body!)
$total = (int) $response->header('X-Total');
// Response processing — how resources are transformed into plugin cards
$resources = $response->json();
$plugins = collect($resources)->map(function ($r) {
    return [
        'id'            => $r['id'],
        'name'          => $r['name'],
        'summary'       => $r['tag'] ?? '',
        'author'        => '', // Spiget requires separate /resources/{id}/author call
        'downloads'     => $r['downloads'] ?? 0,
        'icon_url'      => !empty($r['icon']['url'])
            ? "https://www.spigotmc.org/{$r['icon']['url']}"
            : null,
        'updated_at'    => date('c', $r['updateDate'] ?? 0),
        'versions'      => $r['testedVersions'] ?? [],
        'provider'      => 'spiget',
        'has_direct_download' => !($r['premium'] ?? false) && !($r['external'] ?? false),
    ];
});
// Download availability check — HEAD request before attempting download
$check = Http::head("https://api.spiget.org/v2/resources/{$id}/download");
if ($check->status() !== 200) {
    // Content-Type check: JAR = direct download, HTML = premium/external page
    $contentType = $check->header('Content-Type');
    if (str_contains($contentType, 'text/html')) {
        $plugin['has_direct_download'] = false;
        // Shows "External resource" or "Premium" badge
    }
}
// Version list retrieval with release date sorting
$versions = Http::timeout($timeout)
    ->get("https://api.spiget.org/v2/resources/{$id}/versions", [
        'size' => 20,
        'sort' => '-releaseDate',
    ])->json();

// Each version: { id, uuid, name, releaseDate, downloads, rating }
// No file hash available from Spiget API

Spiget-specific behaviours:

  • Resources with premium: true cannot be downloaded via API — requires SpigotMC purchase
  • External resources (external: true) are hosted outside SpigotMC — follow link manually
  • Icons use data/resource_icons/{id}/{id}.jpg format from Spiget CDN
  • Version filtering works by checking testedVersions includes your MC version
  • No dependency resolution — Spiget API does not expose dependency data
  • Author info requires separate GET /resources/{id}/author call — plugin does this lazily

CurseForge Provider (Plugins) — Complete Reference

Uses the same CurseForge API as Game Mods but with classId=5 (Bukkit Plugins). The classId is the critical difference.

API Endpoint Parameters — GET /v1/mods/search

Parameter Type Required Default Description
gameId int Yes 432 Always Minecraft for plugins
classId int Yes 5 Bukkit Plugins (NOT 9137 which is Mods!)
searchFilter string No Full-text search
sortField int No 2 1=Featured (with query), 2=Popularity (without)
sortOrder string No desc Sort direction
gameVersion string No MC version filter
pageSize int No 20 Max 50
index int No 0 Pagination offset

Response Schema — Same as Game Mods CurseForge (see Game Mods documentation), key differences:

  • classId = 5 in all results
  • categories[].classId matches plugin categories, not mod categories
  • latestFilesIndexes[] may include Bukkit/Spigot/Paper compatibility markers
// Plugin search — classId=5 is the critical difference from Mods (9137)
$response = Http::withHeaders(['x-api-key' => $apiKey])
    ->timeout($timeout)
    ->get('https://api.curseforge.com/v1/mods/search', [
        'gameId'        => 432,     // Minecraft
        'classId'       => 5,       // ⚠ Bukkit Plugins — NOT 9137 (Mods)!
        'searchFilter'  => $query,
        'sortField'     => $query ? 1 : 2,  // Dynamic sort
        'sortOrder'     => 'desc',
        'gameVersion'   => $mcVersion,
        'pageSize'      => $perPage,
        'index'         => ($page - 1) * $perPage,
    ]);
// Response processing — same structure as Mods, classId differs
$results = $response->json('data', []);
$plugins = collect($results)->map(function ($mod) {
    return [
        'id'         => $mod['id'],
        'name'       => $mod['name'],
        'slug'       => $mod['slug'],
        'summary'    => $mod['summary'],
        'author'     => $mod['authors'][0]['name'] ?? 'Unknown',
        'downloads'  => $mod['downloadCount'],
        'icon_url'   => $mod['logo']['url'] ?? null,
        'provider'   => 'curseforge',
    ];
});
// Recursive dependency resolution — same pattern as Game Mods
foreach ($file['dependencies'] as $dep) {
    if ($dep['relationType'] === 3) {  // Required dependency
        $depPlugin = $this->fetchMod($dep['modId']);
        $this->installPlugin($depPlugin);  // Recursive
    }
}
// Description enrichment — CurseForge may return empty summary
if (empty($mod['summary'])) {
    $html = Http::withHeaders(['x-api-key' => $apiKey])
        ->get("https://api.curseforge.com/v1/mods/{$mod['id']}/description")
        ->body();
    // HTML stripped to plain text for display, max 200 chars
}

CurseForge Plugin Provider Error Map

Error / Log Message Cause Impact Solution
classId=9137 results showing Code using mod classId Wrong content type Ensure classId=5 for plugins
Empty description Plugin has no CurseForge summary Missing info Enrichment API call fills this
Dependency loop Circular relationType=3 Install hangs Report on plugin page
downloadUrl null Author restricted download Manual download needed Download from CurseForge website

Modrinth Provider (Plugins) — Complete Reference

API Endpoint Parameters — GET /v2/search

Parameter Type Required Default Description
query string No Search terms
facets string (JSON) Yes see below Must include project_type:plugin
limit int No 20 Max 100
offset int No 0 Pagination offset

Plugin-Specific Facets

// The plugin builds these facets specifically for server plugins
$facets = [
    ['project_type:plugin'],                         // Must be a plugin, not mod
    ['categories:paper', 'categories:spigot',        // Server software compatibility
     'categories:bukkit', 'categories:purpur'],       // OR-combined within inner array
];
if ($mcVersion) $facets[] = ["versions:$mcVersion"];
if ($loader)    $facets[] = ["categories:$loader"];  // e.g., categories:paper

$params['facets'] = json_encode($facets);

Response Schema — Same as Game Mods Modrinth (see Game Mods documentation), key differences:

  • hits[].project_type = "plugin" (not "mod")
  • hits[].categories includes server types like bukkit, spigot, paper, purpur
  • hits[].loaders lists supported server platforms
// Response processing — identical to mods, project_type differs
$hits = $response->json('hits', []);
$plugins = collect($hits)->map(function ($hit) {
    return [
        'id'         => $hit['project_id'],
        'name'       => $hit['title'],
        'slug'       => $hit['slug'],
        'summary'    => $hit['description'],
        'author'     => $hit['author'],
        'downloads'  => $hit['downloads'],
        'icon_url'   => $hit['icon_url'] ?? null,
        'versions'   => $hit['versions'] ?? [],
        'provider'   => 'modrinth',
    ];
});

Modrinth Plugin Provider Error Map

Error / Log Message Cause Impact Solution
No results with project_type:plugin Plugin not tagged correctly on Modrinth Missing plugins Try searching on other providers
SHA-1 mismatch Download corrupted Integrity check fails Clear cache, retry download
Wrong project_type results Facets misconfigured Mods mixed with plugins Ensure project_type:plugin facet

PaperMC / Hangar Provider — Complete Reference

API Endpoint Parameters — GET /v1/projects

Parameter Type Required Default Description
q string No Search query
limit int No 25 Results per page (max 25)
offset int No 0 Pagination offset
sort string No -stars Sort: stars, downloads, views, newest, updated, recent (prefix - for desc)

API Endpoint Parameters — GET /v1/projects/{slug}/versions

Parameter Type Required Default Description
slug string (path) Yes Project slug (e.g. ViaVersion)
limit int No 10 Versions per page
offset int No 0 Pagination offset
platform string No Filter: PAPER, VELOCITY, WATERFALL

Response Schema — Projects Search

Field Type Description
result[].namespace.owner string Project owner username
result[].namespace.slug string Project URL slug
result[].name string Display name
result[].description string Short description
result[].stats.downloads int Total downloads
result[].stats.stars int Star count
result[].stats.watchers int Watcher count
result[].avatarUrl string Project icon URL
result[].lastUpdated string ISO 8601 timestamp
pagination.count int Total results
pagination.limit int Page size
pagination.offset int Current offset

Response Schema — Versions

Field Type Description
result[].name string Version display name
result[].createdAt string ISO 8601 publish date
result[].downloads.PAPER[].fileInfo.name string JAR filename
result[].downloads.PAPER[].fileInfo.sizeBytes int File size
result[].downloads.PAPER[].externalUrl string|null External download URL (if hosted externally)
result[].platformDependencies.PAPER string[] MC version range (e.g. ["1.8-1.21"])
result[].pluginDependencies.PAPER[].name string Required plugin names
// Project search — with pagination
$response = Http::timeout($timeout)
    ->get('https://hangar.papermc.io/api/v1/projects', [
        'q'      => $query,
        'limit'  => $perPage,
        'offset' => ($page - 1) * $perPage,
    ]);

// Response processing
$results = $response->json('result', []);
$plugins = collect($results)->map(function ($project) {
    return [
        'id'         => $project['namespace']['slug'],  // owner/slug not needed for Hangar
        'name'       => $project['name'],
        'summary'    => $project['description'],
        'author'     => $project['namespace']['owner'],
        'downloads'  => $project['stats']['downloads'] ?? 0,
        'icon_url'   => $project['avatarUrl'] ?? null,
        'updated_at' => $project['lastUpdated'],
        'provider'   => 'hangar',
    ];
});
// Version listing with platform priority
$versions = Http::timeout($timeout)
    ->get("https://hangar.papermc.io/api/v1/projects/{$slug}/versions", [
        'limit' => 20,
    ])->json('result', []);

// Platform priority: PAPER → VELOCITY → WATERFALL
// Each version has platformDependencies showing MC version ranges
foreach ($versions as $v) {
    $mcVersions = $v['platformDependencies']['PAPER'] ?? [];
    // Returns e.g. ["1.8-1.21"] — means 1.8 through 1.21 supported

    // External URL check — some plugins link to GitHub/Jenkins
    if (!empty($v['downloads']['PAPER'][0]['externalUrl'])) {
        $plugin['requires_manual_download'] = true;
    }
}

Hangar-specific behaviours:

  • Version-agnostic: Does NOT filter by specific MC version — shows all versions for all platforms
  • Projects use namespace.owner/namespace.slug format (e.g., ViaVersion/ViaVersion)
  • externalUrl in downloads → plugin flagged as requires_manual_download
  • Stats include downloads, stars, watchers — used for sort ranking
  • Plugin dependencies listed in pluginDependencies.PAPER[].name — informational only (not auto-resolved)
  • MC version filter is hidden in UI for Hangar since it doesn't support server-side filtering

Hangar Provider Error Map

Error / Log Message Cause Impact Solution
No results Query too specific or no Hangar plugins match Empty page Use shorter search terms
"Version not found" Project delisted from Hangar Download fails Project was removed by author
Wrong platform version Version for VELOCITY not PAPER Incompatible JAR Check platformDependencies before download
External URL redirect Plugin hosted on GitHub/Jenkins Can't auto-install Must download manually from external URL
CDN timeout Hangar download servers slow Download fails Increase timeout, retry later

GeyserMC Provider — Complete Reference

API Endpoint Parameters — GET /v2/projects

Parameter Type Required Default Description
(none) Returns all project names (always 6)

API Endpoint Parameters — GET /v2/projects/{project}/versions/latest/builds/latest

Parameter Type Required Default Description
project string (path) Yes One of the 6 project names

The 6 GeyserMC Projects

Project Description Primary Download Key
geyser Bedrock-to-Java bridge spigot
floodgate Bedrock account linking spigot
hurricane Performance enhancement spigot
geyserconnect Multi-server connector standalone
thirdpartycosmetics Bedrock cosmetics support spigot
emoteoffhand Emote offhand fix spigot

Response Schema — Latest Build

Field Type Description
build int Build number
version string Version string (e.g. 2.4.0)
downloads.{platform}.name string JAR filename (e.g. Geyser-Spigot.jar)
downloads.{platform}.sha256 string SHA-256 hash for integrity
changes[].commit string Commit hash
changes[].summary string Commit message
// List all GeyserMC projects (always returns the same 6)
$projects = Http::timeout($timeout)
    ->get('https://download.geysermc.org/v2/projects')
    ->json();
// ["geyser","floodgate","hurricane","geyserconnect","thirdpartycosmetics","emoteoffhand"]
// Get latest build with platform-specific downloads
$build = Http::timeout($timeout)
    ->get("https://download.geysermc.org/v2/projects/{$project}/versions/latest/builds/latest")
    ->json();

// Response processing — version ID is constructed from version + build
$versionId = $build['version'] . '-' . $build['build'];  // e.g. "2.4.0-670"

// Download key resolution — platform depends on project
$downloadKey = match ($project) {
    'geyserconnect' => 'standalone',
    default         => 'spigot',  // Paper/Spigot servers use 'spigot' key
};

$jarName  = $build['downloads'][$downloadKey]['name'];      // "Geyser-Spigot.jar"
$sha256   = $build['downloads'][$downloadKey]['sha256'];     // Integrity hash
$url      = "https://download.geysermc.org/v2/projects/{$project}/versions/{$build['version']}/builds/{$build['build']}/downloads/{$downloadKey}";
// GeyserMC special download — bypasses pull(), uses cURL directly
// This is because GeyserMC URLs require specific User-Agent handling
$tmpFile = tempnam(sys_get_temp_dir(), 'geyser_');
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => false,
    CURLOPT_FILE           => fopen($tmpFile, 'w'),
    CURLOPT_TIMEOUT        => 60,
    CURLOPT_FOLLOWLOCATION => true,
]);
curl_exec($ch);
curl_close($ch);
// File then written via daemon putContent()
$this->fileRepository->putContent("/plugins/{$jarName}", file_get_contents($tmpFile));
unlink($tmpFile);

GeyserMC-specific behaviours:

  • No search — always shows all 6 projects in a fixed list
  • Version-agnostic — always shows the latest build, no MC version filtering
  • SHA-256 checksums available for every download — used for integrity verification
  • Download URLs: https://download.geysermc.org/v2/projects/{project}/versions/{ver}/builds/{build}/downloads/{platform}
  • Available platforms vary per project: spigot, bungee, standalone, velocity, fabric
  • 24-hour metadata cache for build info
  • Uses Base64 SVG island icon for all projects (hardcoded, not from API)
  • Hardcoded download counts displayed (not fetched live)
  • MC version filter is hidden in UI for GeyserMC (irrelevant)

GeyserMC Provider Error Map

Error / Log Message Cause Impact Solution
Only 6 projects shown Expected — GeyserMC has exactly 6 Not an error Normal operation
"Build not found" GeyserMC build server issue Can't determine latest version Retry later, check download.geysermc.org
Wrong platform JAR Selected wrong downloadKey Incompatible JAR installed Choose spigot for Spigot/Paper servers
SHA-256 mismatch Download corrupted in transit Integrity check fails Re-download, check network stability
Old version installed 24-hour metadata cache Shows outdated build Clear cache for immediate update check
cURL download failed Temp file write error or timeout No JAR downloaded Check temp directory write permissions, increase timeout
GeyserMC download failed: {message} Any error in download pipeline Install fails Check server logs for specific error

Search Retry Logic

If the initial search with version filter returns 0 results, the plugin automatically retries without the version filter. This helps when a specific MC version isn't indexed yet on a provider.

// Automatic fallback search — applies to all searchable providers
$results = $this->search($query, $mcVersion, $loader);
if (empty($results) && $mcVersion) {
    // Retry without version filter — version may not be indexed yet
    $results = $this->search($query, null, $loader);
    // User sees all versions, can manually check compatibility
}

Cross-Provider Patterns

// Disable/Enable — write to -bak name, then delete original (daemon compatibility)
// Uses 3 steps: read → write new name → delete old
// NOT a simple rename — pterodactyl daemon doesn't support rename atomically
$content = $this->fileRepository->getContent("/plugins/{$filename}");
$this->fileRepository->putContent("/plugins/{$filename}-bak", $content);
$this->fileRepository->delete("/plugins/{$filename}");
// Update-while-disabled — remembers disabled state across updates
$wasDisabled = $plugin->is_disabled;
$this->updatePlugin($plugin);  // Installs new version
if ($wasDisabled) {
    $this->disablePlugin($plugin);  // Re-disable after update
}

Complete Notification Reference

Event Type Title Body
Version detected Success Detection "Minecraft version detected: X.X.X"
Version not detected Warning Detection "Could not detect Minecraft version"
Install started Info Installing "Plugin installation started"
Install success Success Installed "Successfully installed plugin" (logged)
Install failed Danger Failed "Installation failed" + error detail
Dependencies Info Dependencies "Installing X additional plugins…"
Plugin disabled Success Disabled "Plugin disabled: [name]"
Plugin enabled Success Enabled "Plugin enabled: [name]"
Disable failed Danger Failed "Disable failed" + error
Enable failed Danger Failed "Enable failed" + error
Plugin uninstalled Success Removed "Plugin uninstalled"
Uninstall failed Danger Failed error detail
Cache cleared Success Cache "Cache cleared" + scan results
Cache error Danger Error "Error clearing cache" + $e->getMessage()
ZIP started Info Export "ZIP download started"
ZIP failed Danger Export "ZIP download failed"
CurseForge key missing Warning Config "CurseForge API key not set"
External resource Warning Download "External resource — download manually"
Premium resource Warning Download "Premium resource — purchase required"
Filename detect fail Warning Install "Failed to detect filename" (logged)
JAR metadata fail Warning Install "Failed to extract plugin name from JAR" (logged)

HTTP Error Code Reference (All Providers)

HTTP Status Source Meaning Action
200 Any Success
400 CurseForge/Spiget Bad request Check game ID, class ID, query format
401 CurseForge API key invalid Replace key at console.curseforge.com
403 CurseForge Key lacks permissions Regenerate key
403 Plugin Feature flag plugins missing Add ["plugins"] to Egg Features
404 Any provider Resource not found ID/slug wrong or resource removed
404 Spiget Version not found Resource may have been deleted by author
429 CurseForge Rate limit Wait 60s, increase cache duration
429 Spiget Rate limit Reduce frequency, wait 60s
500 Any Server error Retry later
502/503 Any Gateway/maintenance Provider offline
0 / timeout Any Connection timeout Increase MODS_REQUEST_TIMEOUT, check DNS/firewall

Provider-Specific Error Table

SpigotMC / Spiget Errors

Error / Symptom Cause Solution
No download button Premium resource Purchase on SpigotMC website first
"External resource" label Plugin hosted externally Follow link to external site
No versions listed Very old or inactive resource Use File Manager for manual upload
Slow search Spiget API under load Increase timeout, retry
Missing icon No icon uploaded for resource Cosmetic — plugin works fine
HEAD check fails Spiget CDN timeout Retry, check connectivity

Hangar Errors

Error / Symptom Cause Solution
No results Search query too specific Use shorter/broader terms
"Version not found" Hangar project removed Project may have been delisted
Wrong platform Version for wrong server type Check platformDependencies (PAPER vs WATERFALL)
Download fails CDN timeout Retry, increase timeout
Empty stats New project No downloads recorded yet

GeyserMC Errors

Error / Symptom Cause Solution
Only 6 projects shown Expected — GeyserMC has exactly 6 Not an error
"Build not found" GeyserMC build server issue Retry later
Wrong platform JAR Selected wrong download platform Choose spigot for Spigot/Paper servers
SHA-256 mismatch Download corrupted Re-download, check connection
Old version installed Version cache is 24h Clear cache for immediate update check