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

  1. Keep the number of distinct replacement ids at or below 16 (ids 0-15).
  2. Widen the encoding (e.g. use short[] instead of byte[]) if more replacements are required.
  3. 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

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


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