Perhaps you could wrap bare Sockets in an auxiliary datatype that allowedenabled buffering. Something like:
data BufferedSocket = BufferedSocket [ByteString] Socket
Then you could define your own recv function like
recv :: BufferedSocket -> Int -> IO (BufferedSocket,ByteString)
which looked at the buffer before actually trying to read data from the socket. Note that this version of recv returns a modified copy of the BufferedSocket, because now we carry some state that isn't captured in the mutable Socket reference.
(Perhaps this extra buffer state should be put in a separate mutable reference, an IORef for example. We are already in mutable-land after all.)
We also need a function
putBack :: ByteString -> BufferedSocket -> BufferedSocket
for prepending the data.
Another option could consist in using a streaming library like streaming or streaming-bytestring and build a Stream of ByteStrings out of the Socket. Prepending would consist simply in concatenating a pure Stream that yields the ByteString to the effectful stream that reads from the socket, using >> or *>.
let socketStream' = S.yield someByteStringValue *> socketStream
Note that the old socketStream value should not be reused!
This might have the disadvantage that you lose some control about how many bytes to "physically" read at each step, because typical Streams don't take "feedback" from downstream about the number of bytes to receive next.