quarkusio/quarkus · error · IllegalArgumentException
First char must be in range [0, 255] but was:
Error message
First char must be in range [0, 255] but was:
What it means
JsonEscaper bit-packs escape metadata into one int: 'first' occupies the low 8 bits (range [0,255]). Passing a char code above 255 or negative would collide with the 'second' and 'length' bit fields, corrupting the packed data, so IllegalArgumentException is thrown. Used when building JSON escape replacement tables.
Source
Thrown at independent-projects/qute/core/src/main/java/io/quarkus/qute/JsonEscaper.java:36
* The replacement data is packed as follows:<br>
* write an ASCII art of the int:<br>
* The visual order chosen reflect what Integer::toHexString would print since Java ints are stored big-endian.<br>
*
* <pre>
* |----------|-----------|-------------|------------|
* bits | 24-31 | 16-23 | 8-15 | 0-7 |
* field | length | padding | 2nd char | 1st char |
* values | {1,2,6} | [0] | [0-255] | [0-255] |
* |----------|-----------|-------------|------------|
* </pre>
*
*/
private static int packReplacementData(int first, int second, int length) {
if (length != 1 && length != 2 && length != 6) {
throw new IllegalArgumentException("Length must be 1, 2 or 6 but was: " + length);
}
if (first < 0 || first > 255) {
throw new IllegalArgumentException("First char must be in range [0, 255] but was: " + first);
}
if (second < 0 || second > 255) {
throw new IllegalArgumentException("Second char must be in range [0, 255] but was: " + second);
}
return (first | (second << SECOND_CHAR_OFFSET)) | (length << LENGTH_BITS_OFFSET);
}
private static int replacementLength(int replacementData) {
// length isn't bigger than 127, which means preserving sign (which is faster) won't affect the shift
return replacementData >> LENGTH_BITS_OFFSET;
}
private static char secondChar(int replacementData) {
// since past the second char we have padding === 0 we can just cast to char
return (char) (replacementData >> SECOND_CHAR_OFFSET);
}
private static char firstChar(int replacementData) {View on GitHub (pinned to e1c734241f)
Solutions
- Clamp/validate input: only chars 0–255 may go into 'first'; handle higher code points via the unicode escape (length 6) path
- Compute first from replacement.charAt(0) only after verifying it is <= 255
- If non-ASCII escapes are needed, extend the escaper to always emit \uXXXX form for those characters
Example fix
// before
packReplacementData('€', 'x', 1); // 0x20AC > 255, throws
// after
packReplacementData('\\', 'u20AC'.charAt(0), 6); // use unicode escape representation Defensive patterns
Strategy: validation
Validate before calling
if (first < 0 || first > 255) {
throw new IllegalArgumentException("first must be in [0,255]: " + first);
} Type guard
boolean isLatin1(char c) {
return c >= 0 && c <= 255;
} Try / catch
try {
int packed = packReplacementData(first, second, length);
} catch (IllegalArgumentException e) {
log.error("Escape char out of range: " + e.getMessage());
throw e;
} Prevention
- Encode non-Latin-1 characters as \uXXXX (length 6) instead of raw chars
- Range-check chars before packing
- Test escaper tables with ASCII-only fixtures
When it happens
Trigger: Calling the private packReplacementData with first < 0 or first > 255 — e.g. packing a non-ASCII char directly (code point > 255) instead of encoding it as a 6-char unicode escape.
Common situations: Adding a new escape entry for a non-Latin-1 character (e.g. '€') as a raw first char instead of going through the \uXXXX path; not reachable from user templates.
Related errors
- Length must be 1, 2 or 6 but was:
- Second char must be in range [0, 255] but was:
- Control characters not allowed in json string
- Not a hint info
- Control characters not allowed in json string
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/cc0257fe593a55ca.
Report an issue: GitHub.