This is partially guesswork, but the code to catchWSError looks dubious:
catchWsError :: WebSockets p a
-> (SomeException -> WebSockets p a)
-> WebSockets p a
catchWsError act c = WebSockets $ do
env <- ask
let it = peelWebSockets env $ act
cit = peelWebSockets env . c
lift $ it `E.catchError` cit
where
peelWebSockets env = flip runReaderT env . unWebSockets
Look at `cit`. It runs the recovery function, then hands the underlying Iteratee the existing environment. That's fine if `act` is at fault, but there are Iteratee- and IO-ish things in WebSocketsEnv---if one of `envSink` or `envSendBuilder` is causing the exception, it'll just get re-thrown after `E.catchError`. (I think. That's the guesswork part.)
So check how `envSendBuilder` is built up, and see if there's a way it could throw an exception on client disconnect.