The reason is that calling receiveBytes() with MSG_PEEK does not look behind the current TLS record. If the current record contains an incomplete WebSocket header, peekHeader() will not be able to ever return a full header.
The solution is to not use MSG_PEEK, and instead do a regular receiveBytes() and keep the bytes in an internal buffer.