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

  1. Clamp/validate input: only chars 0–255 may go into 'first'; handle higher code points via the unicode escape (length 6) path
  2. Compute first from replacement.charAt(0) only after verifying it is <= 255
  3. 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

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


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/cc0257fe593a55ca. Report an issue: GitHub.