How do I define a Server-Sent Event(SSE) end point for servant. The docs don't seem to cover this case.
If Servant is not designed for the realtime use case, which Haskell server framework supports SSE?
How do I define a Server-Sent Event(SSE) end point for servant. The docs don't seem to cover this case.
If Servant is not designed for the realtime use case, which Haskell server framework supports SSE?
servant
uses WAI
, and you can always dip down into normal WAI
applications and all the libraries that exist for it with the Raw
combinator. So you can use Network.Wai.EventSource
from wai-extra
to create an Application
, which is the type of handlers for Raw
endpoints. Something like:
type MyApi = "normalapi" :> NormalApi
:<|> "sse" :> Raw
myServer :: Server MyAPI
myServer = normalServer :<|> eventSourceAppChan myChan
Thanks to the answer of user2141650 I managed to get a working example of a server-sent events that uses channels.
The gist of the solution is as follows. Assume that we have an echo server that just echoes messages:
newtype Message = Message { msgText :: Text }
Then we'll define three end-points, one for creating sessions, one for sending messages to a session, and the other for retrieving the messages of a session using server-sent events:
# Create a new session
curl -v -XPOST http://localhost:8081/session/new
# And subscribe to its events
curl -v http://localhost:8081/events/0
# And from another terminal
curl -v -XPOST http://localhost:8081/session/0/echo\
-H "Content-Type: application/json" -d '{"msgText": "Hello"}'
Now let's see how to implement the end-point to write a message for a given session, into a channel:
sendH :: SessionId -> Message -> Handler NoContent
sendH sid msg = do
-- lookupChannel :: Env -> SessionId -> IO (Maybe (Chan ServerEvent))
mCh <- liftIO $ lookupChannel env sid
case mCh of
Nothing ->
throwError err404
Just ch -> do
liftIO $ writeChan ch (asServerEvent msg)
return NoContent
The function to convert a Message
to a ServerEvent
is shown below:
import Data.Text.Encoding as TE
import qualified Data.Text.Lazy as T
asServerEvent :: Message -> ServerEvent
asServerEvent msg = ServerEvent
{ eventName = Just eName
, eventId = Nothing
, eventData = [msg']
}
where
eName :: Builder
eName = fromByteString "Message arrived"
msg' :: Builder
msg' = fromByteString $ TE.encodeUtf8 $ T.toStrict $ msgText msg
Finally, the handler for retrieving the messages from the server can be implemented using evetSourceAppChan
, as follows:
eventsH sid = Tagged $ \req respond -> do
mCh <- lookupChannel env sid
case mCh of
Nothing -> do
let msg = "Could not find session with id: "
<> TLE.encodeUtf8 (T.pack (show sid))
respond $ responseLBS status404 [] msg
Just ch -> do
ch' <- dupChan ch
eventSourceAppChan ch req respond
The full solution is available at my sanbox.
I hope that helps.
Servant can handle this well with just a bit of boilerplate. In this case you need a new content type (EventStream
) and a supporting class to render types into SSE format.
{-# LANGUAGE NoImplicitPrelude #-}
module Spencer.Web.Rest.ServerSentEvents where
import RIO
import qualified RIO.ByteString.Lazy as BL
import Servant
import qualified Network.HTTP.Media as M
-- imitate the Servant JSON and OctetStream implementations
data EventStream deriving Typeable
instance Accept EventStream where
contentType _ = "text" M.// "event-stream"
instance ToSSE a => MimeRender EventStream a where
mimeRender _ = toSSE
-- imitate the ToJSON type class
class ToSSE a where
toSSE :: a -> BL.ByteString
-- my custom type with simple SSE render
data Hello = Hello
instance ToSSE Hello where
toSSE _ = "data: hello!\n\n"
-- my simple SSE server
type MyApi = "sse" :> StreamGet NoFraming EventStream (SourceIO Hello)
myServer :: Server MyAPI
myServer = return $ source [Hello, Hello, Hello]
Browser result:
data: hello!
data: hello!
data: hello!
Yeah, I'm not sure about server sent events in servant, but more comprehensive Web frameworks like Yesod has support for that.
Take a look at the package yesod-eventsource
Yesod has pretty nice cookbook so you can event find there pretty nice example