quarkusio/quarkus · error · IllegalArgumentException
Second char must be in range [0, 255] but was:
Error message
Second char must be in range [0, 255] but was:
What it means
JsonEscaper bit-packs escape metadata into one int: 'second' occupies bits SECOND_CHAR_OFFSET..LENGTH_BITS_OFFSET-1, requiring a value in [0,255]. A negative or >255 second char would overflow into the length field and corrupt the packed replacement data, so IllegalArgumentException is thrown.
Source
Thrown at independent-projects/qute/core/src/main/java/io/quarkus/qute/JsonEscaper.java:39
*
* <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) {
// we need to filter the first byte
return (char) (replacementData & 0xFF);
}View on GitHub (pinned to e1c734241f)
Solutions
- Ensure the second char is ASCII/Latin-1 (0–255) or use the \uXXXX (length 6) representation for non-ASCII
- Derive second via replacement.charAt(1) after a range check, and skip/restructure entries that exceed 255
- If wider escapes are needed, redesign the packing offsets rather than passing out-of-range values
Example fix
// before
packReplacementData('\\', 'é', 2); // 0xE9 fits, but '€' (0x20AC) would throw
// after
packReplacementData('\\', 'u', 6); // encode as \uXXXX instead Defensive patterns
Strategy: validation
Validate before calling
if (second < 0 || second > 255) {
throw new IllegalArgumentException("second must be in [0,255]: " + second);
} 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
- Keep replacement characters within Latin-1 or use \uXXXX form
- Range-check every char of a replacement before packing
- Cover multi-char replacements in tests
When it happens
Trigger: Calling the private packReplacementData with second < 0 or second > 255 — e.g. passing a non-ASCII character as the second char of a replacement pair.
Common situations: Extending Qute's JSON escape tables with multi-char replacements containing non-Latin-1 characters; only reachable when modifying escaper internals, not from templates.
Related errors
- Length must be 1, 2 or 6 but was:
- First 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/22e1e50d81eecf0f.
Report an issue: GitHub.