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

  1. Restore the original envelope (backup or re-download the config) so lane slots hold their string values.
  2. Do not modify the null-padded 256-element array; if editing is needed, use 'plain' protection instead.
  3. 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

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


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)