quarkusio/quarkus · error · IllegalArgumentException
partial escape sequence at end of string: ${s}
Error message
partial escape sequence at end of string: ${s} What it means
URIDecoder.decodeURIComponent() requires two hex digits after '%'. When the first character after '%' is not '%' but there are fewer than two remaining characters (i >= size - 1), the escape sequence is incomplete and this IllegalArgumentException is thrown. It prevents decoding a half-specified byte value.
Source
Thrown at independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/URIDecoder.java:76
if (!modified) {
return s;
}
final byte[] buf = s.getBytes(StandardCharsets.UTF_8);
int pos = i; // position in `buf'.
for (; i < size; i++) {
char c = s.charAt(i);
if (c == '%') {
if (i == size - 1) {
throw new IllegalArgumentException("unterminated escape"
+ " sequence at end of string: " + s);
}
c = s.charAt(++i);
if (c == '%') {
buf[pos++] = '%'; // "%%" -> "%"
break;
}
if (i >= size - 1) {
throw new IllegalArgumentException("partial escape"
+ " sequence at end of string: " + s);
}
c = decodeHexNibble(c);
final char c2 = decodeHexNibble(s.charAt(++i));
if (c == Character.MAX_VALUE || c2 == Character.MAX_VALUE) {
throw new IllegalArgumentException(
"invalid escape sequence `%" + s.charAt(i - 1)
+ s.charAt(i) + "' at index " + (i - 2)
+ " of: " + s);
}
c = (char) (c * 16 + c2);
// shouldn't check for plus since it would be a double decoding
buf[pos++] = (byte) c;
} else {
buf[pos++] = (byte) (plus && c == '+' ? ' ' : c);
}
}
return new String(buf, 0, pos, StandardCharsets.UTF_8);View on GitHub (pinned to e1c734241f)
Solutions
- Provide both hex digits for every escape (e.g. %2F not %2)
- Validate input with a regex like /^([^%]|%[0-9A-Fa-f]{2})*$/ before decoding
- Re-encode the source string properly (encodeURIComponent / UriBuilder) instead of hand-assembling
- Catch IllegalArgumentException and reject the request with 400 Bad Request
Example fix
// before
URIDecoder.decodeURIComponent("/files/a%2");
// after
URIDecoder.decodeURIComponent("/files/a%2F"); Defensive patterns
Strategy: validation
Validate before calling
private static final Pattern PCT = Pattern.compile("^([^%]|%[0-9A-Fa-f]{2})*$");
static void requireValidPctEncoding(String s) {
if (!PCT.matcher(s).matches()) throw new IllegalArgumentException("bad percent-encoding: " + s);
} Type guard
static boolean hasCompleteTrailingEscape(String s) {
if (s == null || !s.contains("%")) return true;
int i = s.lastIndexOf('%');
return s.length() - i > 2 && isHex(s.charAt(i + 1)) && isHex(s.charAt(i + 2));
} Try / catch
try {
decoded = URIDecoder.decodeURIComponent(raw);
} catch (IllegalArgumentException e) {
if (e.getMessage() != null && e.getMessage().contains("partial escape")) {
throw new BadRequestException("Incomplete percent-escape in: " + raw);
} else throw e;
} Prevention
- Use two hex digits for every escape (%2F, not %2)
- Build URLs with UriBuilder/URLEncoder rather than string concatenation
- Check truncation sources (length limits, log cutting) that split escapes
- Unit-test decoders with edge-case strings ending in '%X'
When it happens
Trigger: Calling decodeURIComponent with input ending in '%X' (one hex digit), e.g. "/a%C" or "value=%2" — the percent escape is cut short at the end of the string.
Common situations: URLs truncated by length limits or copy/paste; hand-built query strings where a two-digit hex code was typed with only one digit; log lines or substrings cut mid-escape.
Related errors
- unterminated escape sequence at end of string: ${s}
- invalid escape sequence `%${c}${c2}' at index ${i-2} of: ${s
- Specified path can not contain '..' or '%'. Path was
- Specified path is an invalid URI. Path was
- The value of URL was invalid " + baseUrl
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/dab9f51a19d3467a.
Report an issue: GitHub.