HMCL-dev/HMCL · error · IOException

"Mod " + modFile + " ` ` is…

Error message

"Mod " + modFile + " `%s` is malformed..".formatted(modToml.getName())

What it means

Thrown by ForgeNewModMetadata.fromFile0 in two cases: the mods.toml file fails TOML parsing (the IOException carries the parse errors as suppressed exceptions), or the file parses but yields no mods table entries. Either way HMCL cannot build LocalModFile metadata from the descriptor.

Solutions

  1. Inspect the suppressed exceptions on the thrown IOException — they pinpoint the exact TOML syntax error and line.
  2. Fix the syntax in META-INF/mods.toml (or neoforge.mods.toml) inside the jar and repackage.
  3. Ensure the descriptor contains a non-empty [[mods]] table with a valid modId.
  4. Re-download an untouched copy of the mod from its official source.

Example fix

// before (broken mods.toml)
[[mods]
modId = "example
// after
[[mods]]
modId = "example"
version = "1.0.0"
Defensive patterns

Strategy: validation

Validate before calling

TomlParseResult toml = Toml.parse(tomlText);
if (toml.hasErrors()) {
    toml.errors().forEach(err -> LOG.warning(err.toString()));
    throw new IllegalArgumentException("malformed mods.toml");
}

Try / catch

try {
    LocalModFile mod = ForgeNewModMetadata.fromFile(modManager, modFile, tree, loaderType);
} catch (IOException e) {
    for (Throwable suppressed : e.getSuppressed()) {
        LOG.warning("TOML error: " + suppressed);
    }
}

Prevention

When it happens

Trigger: 1) Toml.parse reports errors on the contents of mods.toml/neoforge.mods.toml. 2) The parsed ForgeNewModMetadata is null or metadata.getMods() is empty.

Common situations: A mods.toml with a syntax typo (unclosed string, bad table header); a hand-edited descriptor that dropped the [[mods]] block; a descriptor written for a different TOML dialect or corrupted during repackaging.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeNewModMetadata.java:230

    private static LocalModFile fromFile0(
            String tomlPath,
            ModLoaderType modLoaderType,
            ModManager modManager,
            Path modFile,
            ZipFileTree tree) throws IOException, JsonParseException {
        ZipArchiveEntry modToml = tree.getEntry(tomlPath);
        if (modToml == null)
            throw new IOException("File " + modFile + " is not a Forge 1.13+ or NeoForge mod.");
        TomlParseResult tomlParseResult = Toml.parse(tree.readTextEntry(modToml));
        if (tomlParseResult.hasErrors()) {
            var ioException = new IOException("Mod " + modFile + " `%s` is malformed..".formatted(modToml.getName()));
            tomlParseResult.errors().forEach(ioException::addSuppressed);
            throw ioException;
        }
        ForgeNewModMetadata metadata = JsonUtils.GSON.fromJson(tomlParseResult.toJson(), ForgeNewModMetadata.class);
        if (metadata == null || metadata.getMods().isEmpty())
            throw new IOException("Mod " + modFile + " `%s` is malformed..".formatted(modToml.getName()));
        Mod mod = metadata.getMods().get(0);
        ZipArchiveEntry manifestMF = tree.getEntry("META-INF/MANIFEST.MF");
        String jarVersion = "";
        if (manifestMF != null) {
            try (InputStream is = tree.getInputStream(manifestMF)) {
                Manifest manifest = new Manifest(is);
                jarVersion = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
            } catch (IOException e) {
                LOG.warning("Failed to parse MANIFEST.MF in file " + modFile);
            }
        }

        ModLoaderType type = analyzeLoader(tomlParseResult, mod.getModId(), modLoaderType);

        String logoPath = StringUtils.isNotBlank(mod.getLogoFile()) ? mod.getLogoFile() : metadata.getLogoFile();

        return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), type), modFile, mod.getDisplayName(), new LocalAddonFile.Description(mod.getDescription()),
                mod.getAuthors(), jarVersion == null ? mod.getVersion() : mod.getVersion().replace("${file.jarVersion}", jarVersion), "",

View on GitHub (pinned to 24702dc5a0)