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
- Percent-encode literal '%' characters as '%25' before parsing: a version like '1.0%beta' must be '1.0%25beta'
- Verify every '%' in the input is followed by exactly two hex digits; fix or remove stray '%' characters
- 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
- Percent-encode all user-supplied versions and qualifiers with URLEncoder before embedding them in a PURL
- Never re-encode an already-encoded PURL string
- Lint PURL strings from external sources for stray '%' characters before parsing
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
- Invalid PURL:
- Invalid PURL qualifier
- type must not be empty
- name must not be empty
- Maven PURL is missing a namespace (groupId) for name (artifa
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/db9822689eb5dfee.
Report an issue: GitHub.