HMCL-dev/HMCL · error · JsonParseException
Metadata response is empty
Error message
Metadata response is empty
What it means
AuthlibInjectorServer.setMetadataResponse parses the cached or freshly fetched authlib-injector metadata JSON into a JsonObject. If GSON.fromJson returns null — meaning the stored string is null, empty, or the literal 'null' — the server cannot be configured, so a JsonParseException is thrown to abort metadata application.
Solutions
- Check that GET <server-url>/ returns valid JSON metadata (try curl and inspect the body)
- Delete the corrupted cached metadata for this server in HMCL's account data and refresh metadata again
- Verify the server URL is the authlib-injector API root (not a webpage); re-add the server in HMCL
- If you are the server operator, fix the reverse proxy/backend so it returns the metadata JSON
Example fix
// before: trusting whatever was cached
server.restoreMetadataCache();
// after: validate before use
String cached = readMetadataCacheFile();
if (cached == null || cached.isBlank() || "null".equals(cached.trim())) {
server.refreshMetadata(); // force re-download instead of restoring empty cache
} else {
server.restoreMetadataCache();
} Defensive patterns
Strategy: validation
Validate before calling
String meta = readCachedMetadata();
if (meta == null || meta.isBlank() || "null".equals(meta.trim())) {
refreshMetadata(); // re-download instead of restoring empty cache
} else if (!meta.trim().startsWith("{")) {
throw new IllegalArgumentException("Cached metadata is not JSON");
} Type guard
boolean isValidMetadata(String s) {
return s != null && !s.isBlank() && !"null".equals(s.trim()) && s.trim().startsWith("{");
} Try / catch
try {
server.restoreMetadataCache();
} catch (JsonParseException e) {
if (e.getMessage().contains("Metadata response is empty")) {
server.refreshMetadata(); // fall back to network fetch
} else throw e;
} Prevention
- Validate cached metadata is non-empty JSON before restore
- Delete cache files on parse failure so they are re-downloaded
- Verify the server URL returns JSON with curl before adding it
When it happens
Trigger: refreshMetadata downloads metadata and the server returns an empty body (or 200 with no content); restoreMetadataCache loads a persisted metadata cache file that is empty, null, or contains the string "null"; a corrupt/zero-byte cache is read from disk.
Common situations: The authlib-injector server is misconfigured or behind a broken reverse proxy returning empty 200 responses; the local metadata cache file was truncated by a crash or disk-full condition; the metadata URL points at a plain-text endpoint rather than the JSON metadata root.
Related errors
- Illegal result:
- No download url is available
- Malformed response
- Malformed response\n" + text
- GameRemoteVersions.versions cannot be null
AI-assisted analysis of HMCL-dev/HMCL@24702dc5a0 (2026-09-10).
Data as JSON: /api/errors/94db081c024c5613.
Report an issue: GitHub.
Appendix: source
Thrown at HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorServer.java:168
}
private void refreshMetadata(String text) throws IOException {
long timestamp = System.currentTimeMillis();
try {
setMetadataResponse(text, timestamp);
} catch (JsonParseException e) {
throw new IOException("Malformed response\n" + text, e);
}
metadataRefreshed = true;
LOG.info("authlib-injector server metadata refreshed: " + url);
Platform.runLater(helper::invalidate);
}
private void setMetadataResponse(String metadataResponse, long metadataTimestamp) throws JsonParseException {
JsonObject response = GSON.fromJson(metadataResponse, JsonObject.class);
if (response == null) {
throw new JsonParseException("Metadata response is empty");
}
synchronized (this) {
this.metadataResponse = metadataResponse;
this.metadataTimestamp = metadataTimestamp;
Optional<JsonObject> metaObject = tryCast(response.get("meta"), JsonObject.class);
this.name = metaObject.flatMap(meta -> tryCast(meta.get("serverName"), JsonPrimitive.class).map(JsonPrimitive::getAsString))
.orElse(null);
this.links = metaObject.flatMap(meta -> tryCast(meta.get("links"), JsonObject.class))
.map(linksObject -> {
Map<String, String> converted = new LinkedHashMap<>();
linksObject.entrySet().forEach(
entry -> tryCast(entry.getValue(), JsonPrimitive.class).ifPresent(element -> {
converted.put(entry.getKey(), element.getAsString());
}));
return converted;View on GitHub (pinned to 24702dc5a0)