HMCL-dev/HMCL · error · JsonParseException
Protected payload lane is not a string
Error message
Protected payload lane is not a string
What it means
OBFUSCATED_V1.joinObfuscatedPayload throws this when one of the 4 lane elements at the computed payload indexes is not a JSON string primitive. Lanes are written as strings (Base64 slices) with JsonNull padding; a non-string at a lane index means the envelope was corrupted or produced by an incompatible writer.
Solutions
- Restore the original envelope (backup or re-download the config) so lane slots hold their string values.
- Do not modify the null-padded 256-element array; if editing is needed, use 'plain' protection instead.
- Regenerate the setting through HMCL so the obfuscated envelope is rewritten correctly.
Example fix
// before: lane slot overwritten with null [... 63 nulls, null, ...] // lane index holds null // after: lane slot holds its Base64 string [... 63 nulls, "QmFzZTY0", ...]
Defensive patterns
Strategy: validation
Validate before calling
if (envelope.get("payload") instanceof com.google.gson.JsonArray lanes && lanes.size() >= 4) {
for (int i = 0; i < 4; i++) {
int idx = (i + 1) * (Integer.highestOneBit(lanes.size()) / 4) - 1;
com.google.gson.JsonElement lane = lanes.get(idx);
if (!lane.isJsonPrimitive() || !lane.getAsJsonPrimitive().isString()) {
throw new IllegalStateException("Lane " + i + " at index " + idx + " is not a string");
}
}
} Type guard
static boolean lanesAreStrings(com.google.gson.JsonObject envelope) {
if (!(envelope.get("payload") instanceof com.google.gson.JsonArray lanes)) return false;
int size = Integer.highestOneBit(lanes.size());
for (int i = 0; i < 4; i++) {
com.google.gson.JsonElement lane = lanes.get((i + 1) * (size / 4) - 1);
if (!lane.isJsonPrimitive() || !lane.getAsJsonPrimitive().isString()) return false;
}
return true;
} Try / catch
try {
return ProtectedPayload.read(envelope, JsonObject.class);
} catch (com.google.gson.JsonParseException e) {
logger.warning("Corrupted payload lanes; restoring from backup", e);
return readFromBackupOrDefaults();
} Prevention
- Treat the obfuscated payload array as opaque; never rewrite its element types.
- Use 'plain' protection if you need human-editable config values.
- Round-trip config files through JSON serializers carefully — null-padded arrays are easily damaged.
- Verify envelope integrity after sync/merge operations.
When it happens
Trigger: Calling ProtectedPayload.read on an 'hmcl-obfuscated-v1' envelope where lanes.get(lanePayloadIndex(i, effectiveSize)) yields JsonNull, a number, boolean, object, or array instead of a string — i.e. padding leaked into a lane slot or lanes were rewritten with wrong types.
Common situations: Hand-editing or programmatic re-serialization that replaced lane strings with nulls; re-encoding the array with wrong element ordering/size so lane indexes land on padding; third-party tools transforming the config JSON.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- json.toString()
- Missing payload or payload array is too small
- Protected payload is not a
- Theme background field must be a string:
- Theme color must be a string or object
AI-assisted analysis of HMCL-dev/HMCL@24702dc5a0 (2026-09-10).
Data as JSON: /api/errors/3801dd7e52ed2547.
Report an issue: GitHub.
Appendix: source
Thrown at HMCL/src/main/java/org/jackhuang/hmcl/setting/ProtectedPayload.java:196
/// Joins Base64 payload lanes from the envelope.
///
/// @param envelope the envelope object to read from
/// @return the restored Base64 payload
/// @throws JsonParseException if the payload lanes are missing or malformed
private static String joinObfuscatedPayload(JsonObject envelope) {
if (!(envelope.get(PROPERTY_PAYLOAD) instanceof JsonArray lanes)
|| lanes.size() < OBFUSCATED_LANE_COUNT) {
throw new JsonParseException("Missing payload or payload array is too small");
}
int effectivePayloadSize = Integer.highestOneBit(lanes.size());
String[] laneTexts = new String[OBFUSCATED_LANE_COUNT];
int totalLength = 0;
for (int i = 0; i < OBFUSCATED_LANE_COUNT; i++) {
int payloadIndex = lanePayloadIndex(i, effectivePayloadSize);
JsonElement lane = lanes.get(payloadIndex);
if (!lane.isJsonPrimitive() || !lane.getAsJsonPrimitive().isString()) {
throw new JsonParseException("Protected payload lane is not a string");
}
laneTexts[i] = lane.getAsString();
totalLength += laneTexts[i].length();
}
StringBuilder result = new StringBuilder(totalLength);
for (String laneText : laneTexts) {
result.append(laneText);
}
return result.toString();
}
/// Writes the payload into the given envelope.
@Override
protected void writePayload(JsonObject envelope, JsonElement payload) {
byte[] nonce = new byte[NONCE_SIZE];
SECURE_RANDOM.nextBytes(nonce);View on GitHub (pinned to 24702dc5a0)