HMCL-dev/HMCL · error · ArtifactMalformedException

Malformed forge installer file

Error message

Malformed forge installer file

What it means

Thrown when the NeoForge/Forge installer file cannot be read as a valid ZIP archive during preExecute. HMCL copies the installer's main JAR out of the downloaded archive via a zip filesystem, and a ZipException bubbling up means the installer download is corrupt or not a real zip.

Solutions

  1. Delete the corrupted installer/cache and re-download the installer (verify file size/checksum if available).
  2. Switch or retry the download provider/mirror candidates.
  3. Check free disk space and that the download URL is not behind an error page (open the URL in a browser).

Example fix

// before
catch (ZipException ex) {
    throw new ArtifactMalformedException("Malformed forge installer file", ex);
}
// after
// verify the downloaded installer before installing
if (!CompressingUtils.checkZip(installerPath)) {
    Files.deleteIfExists(installerPath); // force a fresh download
    throw new ArtifactMalformedException("Malformed forge installer file", null);
}
Defensive patterns

Strategy: validation

Validate before calling

if (!Files.exists(installerPath) || Files.size(installerPath) < 1024 || !CompressingUtils.checkZip(installerPath)) {
    Files.deleteIfExists(installerPath); // force re-download
    throw new IOException("Installer is not a valid zip: " + installerPath);
}

Try / catch

try {
    await(task);
} catch (ArtifactMalformedException e) {
    // delete cached installer and retry once
    Files.deleteIfExists(installerPath);
    await(task);
}

Prevention

When it happens

Trigger: Downloading a Forge/NeoForge installer whose bytes are a truncated transfer, an HTML error page saved as a .jar, or a corrupted cache entry; then FileUtils.copyFile(mainJar, dest) inside the opened zip filesystem throws ZipException.

Common situations: Unstable mirror/BMCLAPI candidates returning partial files, disk full during download, proxy intercepting the download, or reusing a stale cached installer after an interrupted download.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


AI-assisted analysis of HMCL-dev/HMCL@24702dc5a0 (2026-09-10). Data as JSON: /api/errors/40a1d8d8b94a88c3. Report an issue: GitHub.

Appendix: source

Thrown at HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java:310

            neoForgeVersion = JsonUtils.fromNonNullJson(Files.readString(fs.getPath(profile.getJson())), GameInstanceManifest.class);

            for (Library library : profile.getLibraries()) {
                Path file = fs.getPath("maven").resolve(library.getPath());
                if (Files.exists(file)) {
                    Path dest = gameRepository.getLayout().getLibraryFile(manifest.id(), library);
                    FileUtils.copyFile(file, dest);
                }
            }

            if (profile.getPath().isPresent()) {
                Path mainJar = profile.getPath().get().getPath(fs.getPath("maven"));
                if (Files.exists(mainJar)) {
                    Path dest = gameRepository.getLayout().getArtifactFile(profile.getPath().get());
                    FileUtils.copyFile(mainJar, dest);
                }
            }
        } catch (ZipException ex) {
            throw new ArtifactMalformedException("Malformed forge installer file", ex);
        }

        dependents.add(new GameLibrariesTask(dependencyManager, manifest, true, profile.getLibraries()));
    }

    private Map<String, String> parseOptions(List<String> args, Map<String, String> vars) {
        Map<String, String> options = new LinkedHashMap<>();
        String optionName = null;
        for (String arg : args) {
            if (arg.startsWith("--")) {
                if (optionName != null) {
                    options.put(optionName, "");
                }
                optionName = arg.substring(2);
            } else {
                if (optionName == null) {
                    // ignore
                } else {

View on GitHub (pinned to 24702dc5a0)