These days it's not uncommon to need to return a file (say, index.html) from the backend if the requested route doesn't match an existing API endpoint or another static asset. This is especially handy when using react-router and browserHistory.
I'm a bit stumped as to how I might approach this with Servant. I did wonder if intercepting 404's might be the way to go, but then of course sometimes the API will need to legitimately issue 404. Here's the kind of thing I've been using to experiment:
data Wombat = Wombat
{ id :: Int
, name :: String
} deriving (Eq, Show, Generic)
instance ToJSON Wombat
wombatStore :: [Wombat]
wombatStore =
[ Wombat 0 "Gertrude"
, Wombat 1 "Horace"
, Wombat 2 "Maisie"
, Wombat 3 "Julius"
]
wombats :: Handler [Wombat]
wombats = return wombatStore
wombat :: Int -> Handler Wombat
wombat wid = do
case find (\w -> Main.id w == wid) wombatStore of
Just x -> return x
Nothing -> throwE err404
type API =
"api" :> "wombats" :> Get '[JSON] [Wombat] :<|>
"api" :> "wombats" :> Capture "id" Int :> Get '[JSON] Wombat :<|>
Raw
api :: Proxy API
api = Proxy
server :: Server API
server = wombats
:<|> wombat
:<|> serveDirectory "static"
app :: Application
app = serve api server
main :: IO ()
main = run 3000 app
I'd love to see an example of how I could go about adding a 'default route' that sends an HTML response if the request doesn't match an API endpoint or anything in the static directory. Toy repo here.
You got it, basically. serveDirectory "static" can be replaced by any wai Application, so for instance, we could have:
...
{-# LANGUAGE OverloadedStrings #-}
...
import Network.Wai (responseLBS)
import Network.HTTP.Types (status200)
...
server :: Server API
server = wombats
:<|> wombat
:<|> hello
hello :: Application
hello req respond = respond $
responseLBS
status200 --
[("Content-Type", "text/plain")] -- headers
"Hello, World!" -- content
...
To a first approximation, wai applications are simply Request -> Response, but the docs tell a fuller story:
Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
So since you've got access to IO, you can check if the file exists and if so serve it, otherwise do whatever you like. In fact, wai defines type Middleware = Application -> Application, so you might think up a handy Middleware that wraps up hello (or any other Application!) in a file existence-checker-and-server.
Here is another route:
serveDirectory is defined as
serveDirectory = staticApp . defaultFileServerSettings . addTrailingPathSeparator
defaultFileServerSettings contains a field ssLookupFile which you can change to serve what you want if the file was not found. Perhaps:
import WaiAppStatic.Types
import WaiAppStatic.Storage.Filesystem
import Network.Wai.Application.Static
import System.FilePath
fileOrIndex root pieces = do
res <- ssLookupFile (defaultFileServerSettings root) pieces
case res of
LRNotFound -> undefined -- index.html here
_ -> return res
serveStatic root =
let root' = addTrailingPathSeparator root in
staticApp $ (defaultFileServerSettings root') {ssLookupFile = fileOrIndex root'}
Related
In my attempt to write an authenticated Servant API where handlers use the RIO monad instead of Servant's own Handler monad, I am stuck on authenticated routes that return no content; i.e., Servant's NoContent type. When I try to hoist the RIO server into the Handler using hoistServerWithContext, I get a type error that I don't grok.
Here is the simplified API and server setup:
import qualified Servant as SV
import qualified Servant.Auth.Server as AS
-- A login endpoint that sets authentication and XSRF cookies upon success.
-- Login is a credentials record.
type LoginEndpoint
= "login" :> SV.ReqBody '[SV.JSON] Login :> SV.Verb 'SV.POST 204 '[SV.JSON] CookieHeader
loginServer
:: AS.CookieSettings -> AS.JWTSettings -> SV.ServerT LoginEndpoint (RIO m)
loginServer = ... -- Perform credential check here.
-- A protected endpoint that requires cookie authentication
-- The no-content handler is causing the problem described below.
type ProtectedEndpoint = "api" :> SV.Get '[SV.JSON] Text :<|> SV.DeleteNoContent
protectedServer (AS.Authenticated _) =
return "Authenticated" :<|> return SV.NoContent
protectedServer _ = throwIO SV.err401 :<|> throwIO SV.err401
-- The overall API, with cookie authentication on the protected endpoint
type Api
= LoginEndpoint :<|> (AS.Auth '[AS.Cookie] User :> ProtectedEndpoint)
-- | The overall server for all endpoints.
server :: AS.CookieSettings -> AS.JWTSettings -> SV.ServerT Api (RIO m)
server cs jwt = loginServer cs jwt :<|> protectedServer
Where User is a record type that can be serialized as JWT as part of a cookie. To hoist the server, I follow the example here:
apiProxy :: Proxy Api
apiProxy = Proxy
contextProxy :: Proxy '[AS.CookieSettings, AS.JWTSettings]
contextProxy = Proxy
newtype Env = Env
{ config :: Text }
-- Helper function to hoist our RIO handler into a Servant Handler.
hoistAppServer :: AS.CookieSettings -> AS.JWTSettings -> Env -> SV.Server Api
hoistAppServer cookieSettings jwtSettings env = SV.hoistServerWithContext
apiProxy
contextProxy
(nt env)
(server cookieSettings jwtSettings)
where
-- Natural transformation to map the RIO monad stack to Servant's Handler.
nt :: Env -> RIO Env a -> SV.Handler a
nt e m = SV.Handler $ ExceptT $ try $ runRIO e m
main :: IO ()
main = do
myKey <- AS.generateKey -- Key for encrypting the JWT.
let jwtCfg = AS.defaultJWTSettings myKey
cfg = cookieConf :. jwtCfg :. SV.EmptyContext -- cookieConf sets XSRF handling
env = Env { config = "Some configuration string" }
Warp.run 8081 $ SV.serveWithContext apiProxy cfg $ hoistAppServer cookieConf jwtCfg env
The above hoisting works fine for endpoints that return some content. However, when :<|> SV.DeleteNoContent is present in the ProtectedEndpoint (and the corresponding parts in the server), I get the following type error:
No instance for (HasServer
(Servant.Auth.Server.Internal.AddSetCookie.AddSetCookieApi
(Servant.Auth.Server.Internal.AddSetCookie.AddSetCookieApi
(NoContentVerb 'DELETE)))
'[CookieSettings, JWTSettings])
arising from a use of ‘hoistServerWithContext’
The problem does not arise on an endpoint without authentication; e.g., UnprotectedEndpoint instead of (AS.Auth '[AS.Cookie] User :> ProtectedEndpoint) in the API type definition.
hoistServerWithContext is a function of the HasServer type class, but I'm not sure which instance is of concern here. If I let GHC infer the type, I get
hoistServerWithContext :: forall k (api :: k) (context :: [*]) (m :: * -> *) (n :: * -> *).
HasServer api context =>
Proxy api
-> Proxy context
-> (forall x. m x -> n x)
-> ServerT api m
-> ServerT api n
To me, the type error (plus my experiments adding and removing the no-content handler) indicate that the protectedServer derived by Servant's type machinery is not a member of the HasServer type class. But my Haskell type-level programming skills are not up to the task, it seems. Where exactly is the problem? Am I missing a type annotation? A language extension?
The type error seems to result because servant currently does not allow adding headers to a NoContentVerb because the corresponding type instance is missing. See the Servant-Auth issue here.
Even though I don't fully understand the details, the following workaround from the above issue comment avoids the type error:
type instance ASC.AddSetCookieApi (SV.NoContentVerb 'SV.DELETE)
= SV.Verb 'SV.DELETE 204 '[SV.JSON] (ASC.AddSetCookieApiVerb SV.NoContent)
I'm trying to build a web server in Haskell with Servant where part of the api works as a reverse proxy to another api.
I found an example of how to achieve this. But it seems that it doesn't work:
type API
= "cat" :> Get '[JSON] Cat
newtype Cat = Cat { cat :: String }
instance ToJSON Cat where
toJSON (Cat mew) =
object [ "cat" .= mew ]
server :: Server API
server = pure (Cat { cat = "mrowl" })
api :: Proxy (API :<|> Raw)
api = Proxy
app :: Manager -> Application
app manager =
serve api $ server :<|> waiProxyTo forwardRequest defaultOnExc manager
forwardRequest :: Request -> IO WaiProxyResponse
forwardRequest _ =
pure . WPRProxyDest . ProxyDest "127.0.0.1" $ 4567
startApp :: IO ()
startApp = do
manager <- newManager defaultManagerSettings
run 8080 (app manager)
It gives the following type error (when I try it in my own code base):
• Couldn't match type ‘Request
-> (Response -> IO ResponseReceived) -> IO ResponseReceived’
with ‘Tagged Handler Application’
Expected type: Server (API :<|> Raw)
Actual type: Handler Cat :<|> Application
• In the second argument of ‘($)’, namely
‘server :<|> waiProxyTo forwardRequest defaultOnExc manager’
In the expression:
serve api
$ server :<|> waiProxyTo forwardRequest defaultOnExc manager
In an equation for ‘app’:
app manager
= serve api
$ server :<|> waiProxyTo forwardRequest defaultOnExc manager
|
32 | serve api $ server :<|> waiProxyTo forwardRequest defaultOnExc manager
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(My interpretation is that :<|> has changed to not accept combing Server and Application since the example was written.)
What can I replace waiProxyTo forwardRequest defaultOnExc manager with to make this work?
I don't fully understand why this works but I got it working by mimicking how serveDirectoryWith works:
import Servant.Server (ServerT, Tagged)
import Network.HTTP.Client (Manager)
import Network.HTTP.ReverseProxy
( WaiProxyResponse, WaiProxyResponse(WPRProxyDest)
, ProxyDest(ProxyDest), waiProxyTo, defaultOnExc)
{- ... -}
forwardServer :: Manager -> ServerT Raw m
forwardServer manager =
Tagged $ waiProxyTo forwardRequest defaultOnExc manager
forwardRequest :: Request -> IO WaiProxyResponse
forwardRequest _ =
pure . WPRProxyDest . ProxyDest "127.0.0.1" $ 4567
app :: Manager -> Application
app manager =
serve api $ server :<|> (forwardServer manager)
I was using servant-generic-0.1.0.3 and servant-server-0.13.0.1 to do the following:
data Site route = Site
{ page :: route :-
"page" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] [Int]
, home :: route :-
Raw
} deriving (Generic)
type API = ToServant (Site AsApi)
siteServer :: Pool Connection -> Site AsServer
siteServer pool = Site
{ page = \x y ->
liftIO $ withResource pool $ \conn -> someDbFunction conn x y
, home = serveDirectoryWebApp "static"
}
api :: Proxy API
api = Proxy
app :: Pool Connection -> Application
app pool = serve api (toServant $ siteServer pool)
That worked fine, then I tried to use ReaderT to avoid passing Pool Connection to siteServer, so I added AppM and replaced siteServer like this:
type AppM = ReaderT (Pool Connection) IO
siteServer :: ServerT API AppM
siteServer = Site
{ page = do
pool <- ask
\x y ->
liftIO $ withResource pool $ \conn -> someDbFunction conn x y
, home = serveDirectoryWebApp "static"
}
but I got a bunch of errors when I tried to compile it.
I followed the same steps shown in the servant cookbook, but I couldn't make this work with generic routes, although it works when using regular routes.
Am I missing something that could make this work?
At least for the record-style routes supported by servant-* >= 0.14 (see here), if you want to work with another monad than Handler, you will want to look at AsServerT and genericServerT.
Applied to your example, this means siteServer should be defined as follows (not typechecked, but should be very close to correct).
siteServer :: Site (AsServerT AppM)
siteServer = Site
{ page = ... something in AppM ...
, home = ... something in AppM ...
}
-- turning that into a usual chain of :<|>-separated handlers
oldStyleServer :: ServerT API AppM
oldStyleServer = genericServerT siteServer
-- bringing it all back in Handler
oldStyleServerInHandler :: Pool Connection -> Server API -- same as ServerT API Handler
oldStyleServerInHandler conns = hoistServer (Proxy #API) appToHandler oldStyleServer
where appToHandler = liftIO . flip runReaderT conns
-- or something along those lines
-- serving it
app :: Pool Connection -> Application
app conns = serve (Proxy #API) (oldStyleServerInHandler conns)
Edit: Since you're using servant-* < 0.14 with servant-generic, you should replace genericServerT with toServant.
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.
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
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 = source [Hello, Hello, Hello]
Browser result:
data: hello!
data: hello!
data: hello!
I am following this tutorial http://www.parsonsmatt.org/programming/2015/06/07/servant-persistent.html to create APIs through servant. I want to customize the server to serve static files as well but couldn't find a way to do it.
I am using the stack build tool.
I modified the Main.hs file's run to include static (run port $ static $ logger $ app cfg) and I imported Network.Wai.Middleware.Static (static). I also added wai-middleware-static >=0.7.0 && < 0.71 to my cabal file.
When I run stack build I get: (Update: This part is totally my error. I added the the package to the wrong cabal file.. lame. Importing Network.Wai.Middleware.Static works and serves static files. Leaving the error below in case anyone searches for it and finds it useful.)
Could not find module ‘Network.Wai.Middleware.Static’
Perhaps you meant
Network.Wai.Middleware.Gzip (from wai-extra-3.0.7.1#waiex_GpotceEdscHD6hq9p0wPOJ)
Network.Wai.Middleware.Jsonp (from wai-extra-3.0.7.1#waiex_GpotceEdscHD6hq9p0wPOJ)
Network.Wai.Middleware.Local (from wai-extra-3.0.7.1#waiex_GpotceEdscHD6hq9p0wPOJ)
Next I tried using servant's serveDirectory as follows (simplified):
type API = "users" :> Get '[JSON] [Person]
:<|> "static" :> Raw
server = createPerson :<|> serveDirectory "/static"
I get this error:
Couldn't match type ‘IO’ with ‘EitherT ServantErr IO’
arising from a functional dependency between:
constraint ‘Servant.Server.Internal.Enter.Enter
(IO Network.Wai.Internal.ResponseReceived)
(AppM :~> EitherT ServantErr IO)
(IO Network.Wai.Internal.ResponseReceived)’
arising from a use of ‘enter’
instance ‘Servant.Server.Internal.Enter.Enter
(m a) (m :~> n) (n a)’
at <no location info>
In the expression: enter (readerToEither cfg) server
In an equation for ‘readerServer’:
readerServer cfg = enter (readerToEither cfg) server
I am a Haskell beginner and I am not familiar with Wai so unsure where to even begin. What changes do I need to make the example code in the Blog post to serve static files?
Edit: Since the comments get hidden from the default view, I am pasting my last comment here:
Here is toned down version of Matt's code from his blog. I consolidated all his modules into a single file, removed all the database stuff but did not clean up the extensions/imports. When I run this code I get the above type mismatch error. Please note that this code does not use Network.Wai.Middleware.Static and I am using qualified import of Servant StaticFiles.
As described in the relevant section of servant's tutorial, the whole deal with enter is to have your request handlers use some monad m (in your case some ReaderT monad) and to provide a way to convert a computation in m to a computation in servant's standard EitherT ServantErr IO monad.
The problem here though is that you define a bunch of request handlers in ReaderT and an additional one to serve static files, and call enter on all of these. The ReaderT handlers are converted to EitherT ... handlers just fine, but enter tries to convert the serveDirectory call from ReaderT ... to EitherT .... This is of course not going to happen anytime soon, since serveDirectory isn't a computation in ReaderT ... to begin with!
servant could arguably just leave serveDirectory alone -- at this point I don't have a definite opinion on whether we should do that or not, or if it's better to just have the file-serving handler be glued separately, to the result of calling enter on all the other endpoints. Here's how this would look like (look for -- NEW to see the changes):
type PersonAPI =
"users" :> Capture "name" String :> Get '[JSON] Person
-- NEW: removed Raw from here
-- NEW
type WholeAPI = PersonAPI :<|> Raw
type AppM = ReaderT Config (EitherT ServantErr IO)
userAPI :: Proxy PersonAPI
userAPI = Proxy
-- NEW
wholeAPI :: Proxy WholeAPI
wholeAPI = Proxy
-- NEW: changed 'userAPI' to 'wholeAPI'
app :: Config -> Application
app cfg = serve wholeAPI (readerServer cfg)
readerServer :: Config -> Server WholeAPI
readerServer cfg = enter (readerToEither cfg) server
:<|> S.serveDirectory "/static" -- NEW
readerToEither :: Config -> AppM :~> EitherT ServantErr IO
readerToEither cfg = Nat $ \x -> runReaderT x cfg
server :: ServerT PersonAPI AppM
server = singlePerson
singlePerson :: String -> AppM Person
singlePerson str = do
let person = Person { name = "Joe", email = "joe#example.com" }
return person
I have brought this topic to the attention of the other servant developers anyway, thanks! We hadn't really thought about the interaction between enter and serveDirectory so far (well, I did not).