How does this work?

ReplayingDecoder passes a specialized ByteBuf
implementation which throws an Error of certain type when there's not
enough data in the buffer. In the IntegerHeaderFrameDecoder above,
you just assumed that there will be 4 or more bytes in the buffer when
you call buf.readInt(). If there's really 4 bytes in the buffer,
it will return the integer header as you expected. Otherwise, the
Error will be raised and the control will be returned to
ReplayingDecoder. If ReplayingDecoder catches the
Error, then it will rewind the readerIndex of the buffer
back to the 'initial' position (i.e. the beginning of the buffer) and call
the decode(..) method again when more data is received into the
buffer.

Please note that ReplayingDecoder always throws the same cached
Error instance to avoid the overhead of creating a new Error
and filling its stack trace for every throw.

Limitations

Performance can be worse if the network is slow and the message
format is complicated unlike the example above. In this case, your
decoder might have to decode the same part of the message over and over
again.

You must keep in mind that decode(..) method can be called many
times to decode a single message. For example, the following code will
not work:

Improving the performance

Fortunately, the performance of a complex decoder implementation can be
improved significantly with the checkpoint() method. The
checkpoint() method updates the 'initial' position of the buffer so
that ReplayingDecoder rewinds the readerIndex of the buffer
to the last position where you called the checkpoint() method.

Calling checkpoint(T) with an Enum

Although you can just use checkpoint() method and manage the state
of the decoder by yourself, the easiest way to manage the state of the
decoder is to create an Enum type which represents the current state
of the decoder and to call checkpoint(T) method whenever the state
changes. You can have as many states as you want depending on the
complexity of the message you want to decode: