quarkusio/quarkus · error · java.lang.IllegalArgumentException

Invalid percent-encoding at index

Error message

Invalid percent-encoding at index 

What it means

percentDecode() throws this IllegalArgumentException when it encounters a '%' character that is not followed by two valid hex digits, so no byte can be decoded. It fails fast rather than silently passing malformed sequences through. Called by parse, version, and value while decoding PURL components.

Source

Thrown at independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/Purl.java:392

            byte[] bytes = null;
            while (pct + 2 < input.length() && input.charAt(pct) == '%') {
                int hi = Character.digit(input.charAt(pct + 1), 16);
                int lo = Character.digit(input.charAt(pct + 2), 16);
                if (hi < 0 || lo < 0) {
                    break;
                }
                if (bytes == null) {
                    // each %XX triplet is 3 chars, so the max number of decoded bytes is the remaining length / 3
                    bytes = new byte[(input.length() - tripletStart) / 3];
                }
                bytes[byteCount++] = (byte) ((hi << 4) | lo);
                pct += 3;
            }
            if (byteCount > 0) {
                sb.append(new String(bytes, 0, byteCount, StandardCharsets.UTF_8));
                pos = pct;
            } else {
                throw new IllegalArgumentException(
                        "Invalid percent-encoding at index " + tripletStart + " in: " + input);
            }
        }
        sb.append(input, pos, input.length());
        return sb.toString();
    }

    /**
     * Percent-encodes each segment of a {@code /}-delimited path individually,
     * preserving literal {@code /} separators. A single pass determines whether
     * the path contains any {@code /} (i.e. is multi-segment) and whether any
     * character requires encoding, avoiding redundant scans.
     *
     * @param path the decoded path (namespace or subpath)
     * @return the encoded path with each segment percent-encoded
     */
    private static String encodePath(String path) {
        // Single pass: find the first '/' (to know if the path is multi-segment)

View on GitHub (pinned to e1c734241f)

Solutions

  1. Percent-encode literal '%' characters as '%25' before parsing: a version like '1.0%beta' must be '1.0%25beta'
  2. Verify every '%' in the input is followed by exactly two hex digits; fix or remove stray '%' characters
  3. If the string was double-encoded, decode once less (do not re-encode before calling Purl.parse)

Example fix

// before
Purl p = Purl.parse("pkg:maven/org.acme/app@1.0%beta");
// after
Purl p = Purl.parse("pkg:maven/org.acme/app@1.0%25beta");
Defensive patterns

Strategy: validation

Validate before calling

static boolean hasValidPercentEncoding(String s) {
    for (int i = 0; i < s.length(); i++) {
        if (s.charAt(i) == '%') {
            if (i + 2 >= s.length()) return false;
            if (!isHex(s.charAt(i + 1)) || !isHex(s.charAt(i + 2))) return false;
            i += 2;
        }
    }
    return true;
}

Type guard

String safePurlSegment(String raw) {
    return URLEncoder.encode(raw, StandardCharsets.UTF_8);
}

Try / catch

try {
    return Purl.parse(input);
} catch (IllegalArgumentException e) {
    if (e.getMessage().startsWith("Invalid percent-encoding")) {
        throw new IllegalArgumentException("Malformed PURL input: " + input, e);
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling Purl.parse(), Purl.version(), or qualifier value() with strings containing malformed escapes such as '%A', '100%', '%%', or '%GG' (non-hex characters).

Common situations: Hand-editing or truncating a PURL string; passing pre-URL-encoded text through an extra encoding step leaving stray '%'; copy-pasting versions or qualifiers containing literal percent signs (e.g. coverage values or 'distro=alpine%3' typos).

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/db9822689eb5dfee. Report an issue: GitHub.