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

  1. Ensure the second char is ASCII/Latin-1 (0–255) or use the \uXXXX (length 6) representation for non-ASCII
  2. Derive second via replacement.charAt(1) after a range check, and skip/restructure entries that exceed 255
  3. 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

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


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