jhy/jsoup · error · UncheckedIOException

WTF: No buffer left to unconsume.

Error message

WTF: No buffer left to unconsume.

What it means

Internal invariant failure in CharacterReader.unconsume: it tries to move bufPos below 0, meaning more characters were unconsumed than consumed. Per its contract, unconsume() is only valid immediately after a consume() with no intervening bufferUp(); the message signals a parser bug rather than bad user input. It is reached when a caller (e.g. recursive unconsume chains or lookbehind helpers like matchesDigit) over-rewinds the buffer.

Solutions

  1. Ensure unconsume() is called only directly after a consume(), with no bufferUp() or other reads in between.
  2. Fix lookbehind logic to track how many characters were actually consumed before rewinding; never call unconsume() more times than consume().
  3. Replace fragile consume/unconsume pairs with reader.match(...) lookahead APIs, which do not mutate position on failure.
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at src/main/java/org/jsoup/parser/CharacterReader.java:286 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of jhy/jsoup@9851ac5d9c (2026-09-08). Data as JSON: /api/errors/1a3219ed7b39da5a. Report an issue: GitHub.

Appendix: source

Thrown at src/main/java/org/jsoup/parser/CharacterReader.java:286

    }

    /**
     Consume one character off the queue.
     @return first character on queue, or EOF if the queue is empty.
     */
    public char consume() {
        bufferUp();
        char val = isEmptyNoBufferUp() ? EOF : charBuf[bufPos];
        bufPos++;
        return val;
    }

    /**
     Unconsume one character (bufPos--). MUST only be called directly after a consume(), and no chance of a bufferUp.
     */
    void unconsume() {
        if (bufPos < 1)
            throw new UncheckedIOException(new IOException("WTF: No buffer left to unconsume.")); // a bug if this fires, need to trace it.

        bufPos--;
    }

    /**
     * Moves the current position by one.
     */
    public void advance() {
        bufPos++;
    }

    /**
     * Returns the number of characters between the current position and the next instance of the input char
     * @param c scan target
     * @return offset between current position and next instance of target. -1 if not found.
     */
    int nextIndexOf(char c) {
        // doesn't handle scanning for surrogates

View on GitHub (pinned to 9851ac5d9c)