quarkusio/quarkus · error · IllegalArgumentException
unterminated escape sequence at end of string: ${s}
Error message
unterminated escape sequence at end of string: ${s} What it means
URIDecoder.decodeURIComponent() percent-decodes a URI component. A '%' must be followed by two hex digits ('%XX'); if the string ends immediately after a '%', there is no escape sequence to decode, so this IllegalArgumentException is thrown. It is a strict-input guard: the decoder refuses to silently drop a truncated escape.
Source
Thrown at independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/URIDecoder.java:67
boolean modified = false;
int i;
for (i = 0; i < size; i++) {
final char c = s.charAt(i);
if (c == '%' || (plus && c == '+')) {
modified = true;
break;
}
}
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);View on GitHub (pinned to e1c734241f)
Solutions
- Encode literal percent signs as %25 before decoding
- Validate the input matches /^([^%]|%[0-9A-Fa-f]{2})*$/ before decoding
- Trim or repair the truncated escape character from the input string
- Catch IllegalArgumentException and return the input or a 400 response to the client
Example fix
// before
String v = URIDecoder.decodeURIComponent("discount=100%");
// after
String v = URIDecoder.decodeURIComponent("discount=100%25"); Defensive patterns
Strategy: validation
Validate before calling
private static final Pattern SAFE = Pattern.compile("^([^%]|%[0-9A-Fa-f]{2})*$");
static boolean isDecodable(String s) { return s != null && SAFE.matcher(s).matches(); }
// call: if (!isDecoded(input)) reject 400; Type guard
static boolean endsWithCompleteEscape(String s) {
return s != null && !s.endsWith("%");
} Try / catch
try {
decoded = URIDecoder.decodeURIComponent(raw);
} catch (IllegalArgumentException e) {
if (e.getMessage() != null && e.getMessage().contains("unterminated escape")) {
throw new BadRequestException("Malformed URI component: " + raw);
} else throw e;
} Prevention
- Always percent-encode literal '%' as %25 before decoding
- Never decode strings you did not encode or receive from a trusted encoder
- Validate user input with a percent-encoding regex before decoding
- Sanitize/truncate points: check that slicing operations never cut mid-escape
When it happens
Trigger: Calling decodeURIComponent (or the delegating decode overload) with a string whose last character is '%', e.g. "100%", "/path%", or a URL that was itself truncated or badly encoded.
Common situations: Decoding user-supplied query/path fragments that contain literal percent signs; truncation when storing/pasting URLs; double-encoding mistakes where '%' ended up unescaped in the input.
Related errors
- partial 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/a68fcfbe76431dd3.
Report an issue: GitHub.