quarkusio/quarkus · error · IllegalArgumentException
Replacement ID must be in range [0, 15] but was:
Error message
Replacement ID must be in range [0, 15] but was:
What it means
The HtmlEscaper replacement table stores 4-bit replacement ids in each byte; setLatinReplacementId() validates that the id is in [0, 15] and throws IllegalArgumentException otherwise. It is an internal invariant check while building the escape table.
Source
Thrown at independent-projects/qute/core/src/main/java/io/quarkus/qute/HtmlEscaper.java:40
setLatinReplacementId(data, '"', 1);
setLatinReplacementId(data, '\'', 2);
setLatinReplacementId(data, '&', 3);
setLatinReplacementId(data, '<', 4);
setLatinReplacementId(data, '>', 5);
assert getLatinReplacementId(data, '"') == 1;
assert getLatinReplacementId(data, '\'') == 2;
assert getLatinReplacementId(data, '&') == 3;
assert getLatinReplacementId(data, '<') == 4;
assert getLatinReplacementId(data, '>') == 5;
return data;
}
private static void setLatinReplacementId(byte[] data, int c, int id) {
if (c > 255) {
throw new IllegalArgumentException("Only Latin characters are supported: " + c);
}
if (id < 0 || id > 15) {
throw new IllegalArgumentException("Replacement ID must be in range [0, 15] but was: " + id);
}
data[c] = (byte) id;
}
private static int getLatinReplacementId(byte[] data, int c) {
return data[c] & REPLACEMENT_ID_MASK;
}
private static String replacementOf(char c) {
if (c > 255) {
return null;
}
int replacementId = getLatinReplacementId(LATIN_REPLACEMENT_ID_TABLE, c & 0xFF);
// in the super class we still have to perform a null check vs String, which means
// we can have a branch misprediction there.
// Here we anticipate such cost and if this method is going to be inlined we could still
// correctly predict if the subsequent null check is going to be taken or not.
if (replacementId == 0) {View on GitHub (pinned to e1c734241f)
Solutions
- Keep the number of distinct replacement ids at or below 16 (ids 0-15).
- Widen the encoding (e.g. use short[] instead of byte[]) if more replacements are required.
- Deduplicate identical replacement strings to share ids.
Example fix
// before setLatinReplacementId(data, '"', 16); // out of range -> throws // after setLatinReplacementId(data, '"', 7); // reuse an id within [0, 15], or dedupe replacements
Defensive patterns
Strategy: validation
Validate before calling
if (id < 0 || id > 15) {
throw new IllegalArgumentException("replacement id must fit in 4 bits");
} Type guard
boolean isValidReplacementId(int id) { return id >= 0 && id <= 15; } Try / catch
try {
setLatinReplacementId(data, c, id);
} catch (IllegalArgumentException ex) {
LOGGER.error("too many distinct replacements in escape table");
throw ex;
} Prevention
- Keep at most 16 distinct replacement ids in the escape table
- Deduplicate identical replacement strings
- Widen storage (short[]/int[]) if more replacements are ever needed
When it happens
Trigger: Passing an escape-replacement id < 0 or > 15 to setLatinReplacementId — only possible when modifying/extending createLatinReplacementData with too many distinct replacements.
Common situations: Patching HtmlEscaper to add more escape characters than the 4-bit encoding supports (max 16 distinct replacement ids).
Related errors
- Only Latin characters are supported:
- Invalid identifier found: [id]
- Literal must not be null
- Unexpected tag: " + tag
- Expresion command must be an ASCII char: " + command
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/719874471c5bd73e.
Report an issue: GitHub.