eclipse-vertx/vert.x · error · IllegalArgumentException
Invalid position for escape character: ${start}
Error message
Invalid position for escape character: ${start} What it means
decodeUnreserved expects a '%' escape to start exactly where the caller says: path.charAt(start) must be '%'. If the character at the given position is not '%', the method throws this IllegalArgumentException. It indicates a caller/index bug or a string that was mutated between locating the escape and decoding it.
Source
Thrown at vertx-core/src/main/java/io/vertx/core/internal/net/RFC3986.java:321
// ALPHA
(unescaped >= 0x41 && unescaped <= 0x5A) ||
(unescaped >= 0x61 && unescaped <= 0x7A) ||
// DIGIT
(unescaped >= 0x30 && unescaped <= 0x39) ||
// HYPHEN
(unescaped == 0x2D) ||
// PERIOD
(unescaped == 0x2E) ||
// UNDERSCORE
(unescaped == 0x5F) ||
// TILDE
(unescaped == 0x7E)) {
path.setCharAt(start, (char) unescaped);
path.delete(start + 1, start + 3);
}
} else {
throw new IllegalArgumentException("Invalid position for escape character: " + start);
}
}
}
View on GitHub (pinned to fb308bd8c3)
Solutions
- Recompute the '%' index immediately before each decode call on the current string
- Iterate from the returned next position rather than assuming fixed 3-character advances when decoding in place
- Prefer calling the public decodeUnreserved(String)/decodeUnreservedChars entry point, which scans escapes itself, instead of invoking the position-based method manually
- Guard the call: only invoke when path.charAt(start) == '%'
Example fix
// before
pool.decodeUnreserved(path, idx); // idx from an earlier scan, may no longer point at '%'
// after
if (idx < path.length() && path.charAt(idx) == '%') {
pool.decodeUnreserved(path, idx);
} Defensive patterns
Strategy: validation
Validate before calling
if (start >= 0 && start < path.length() && path.charAt(start) == '%') {
pool.decodeUnreserved(path, start);
} Type guard
boolean pointsAtEscape(StringBuilder path, int start) {
return start >= 0 && start < path.length() && path.charAt(start) == '%';
} Try / catch
try {
pool.decodeUnreserved(path, start);
} catch (IllegalArgumentException e) {
if (e.getMessage() != null && e.getMessage().contains("Invalid position for escape character")) {
start = path.indexOf("%", start); // recompute index and retry once
} else throw e;
} Prevention
- Recompute the '%' index on the current buffer right before decoding — never reuse stale indices
- After in-place mutation (setCharAt/delete), rescan rather than advancing by a fixed stride
- Prefer public decode entry points over the position-based internal method
- Log the offending index and surrounding characters to catch off-by-one loops early
When it happens
Trigger: Passing a start index that does not point at '%' — e.g. an index computed with indexOf('%') on a different string, an off-by-one when iterating (skipping 3 vs restarting at start+1), or decoding a string that was already modified so escapes shifted position.
Common situations: Custom loops that scan for '%' and call decode per match with a stale index after earlier in-place setCharAt/delete mutations; reusing cached indices after the buffer changed; calling the internal method directly with an offset from a substring operation.
Understand the failure class
Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.
Related errors
- Invalid authority host portion:
- Can't register a system codec
- Cannot open file for neither reading nor writing
- Invalid setting
- maxWebSockets must be > 0 or -1 (disabled)
AI-assisted analysis of eclipse-vertx/vert.x@fb308bd8c3 (2026-09-06).
Data as JSON: /api/errors/4b8fbb040a5f7d50.
Report an issue: GitHub.