8

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.

TylerH
  • 20,799
  • 66
  • 75
  • 101
Ecognium
  • 2,046
  • 1
  • 19
  • 35
  • 1
    For the first one, I think you need to add wai-app-static to the build-depends in your .cabal file. – Michael Snoyman Jun 24 '15 at 07:55
  • Thanks, Michael. I actually I had put the package in the wrong cabal file. So wai-middleware-static works well. I am playing with the code in the linked blog post and I noticed it chains middlewares so decided to use the static middleware. It probably will take me longer to figure out how to use `wai-app-static` in that context. – Ecognium Jun 24 '15 at 16:06
  • @Ecognium Have you seen [this part](https://haskell-servant.github.io/tutorial/server.html#serving-static-files) of the servant tutorial? – Alp Mestanogullari Jun 25 '15 at 15:20
  • @AlpMestanogullari Yup but I only understood some parts of it. I got the `:Raw` and `serveDirectory` parts and since I was building on top of the tutorial, which uses Wai and I tried using it with it directly. I just added `:<|> "static" :> Raw` and `:<|> serveDirectory "/static"` and I get the error listed above. May be I need to understand more of the setup to figure out how to integrate with Wai. I tried `lifting` the serveDirectory call but did not help. I am just guessing at this stage :) – Ecognium Jun 25 '15 at 16:31
  • Can you add a minimal but complete example? The error message suggests that this will need a well-placed `lift` but it's hard to tell without being able to look at the whole code. – Cactus Jun 26 '15 at 01:52
  • @Ecognium I think you're just using the wrong `serveDirectory`. The one from `Network.Wai.Middleware.Static` is actually "lifted" into a proper request handler that'll serve static files under some directory within *servant* already. Just do not import `Network.Wai.Middleware.Static` and simply write [serveDirectory](https://hackage.haskell.org/package/servant-server-0.4.2/docs/Servant-Utils-StaticFiles.html#v:serveDirectory) "/path/to/your/static/dir". As long as you import `Servant` or `Servant.Utils.StaticFiles`, this will work. – Alp Mestanogullari Jun 26 '15 at 09:15
  • @AlpMestanogullari @Cactus, here is toned down version of Matt's code from his blog. https://gist.github.com/anonymous/a99bc6b36ac2db64878e. 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. – Ecognium Jun 26 '15 at 18:10
  • 1
    @Ecognium The `enter` machinery converts your handlers from some monad to another. Your `Reader`-based server has a `Raw` in it for file serving, so `enter` tries to convert the file-serving thing from `ReaderT ...` to `EitherT ...`, which won't work because `serveDirectory` doesn't live in `ReaderT`. I'm not sure if this is a bug in servant or if it's really more meaningful to define the file-serving handler separately, apart from all the `ReaderT` ones. I have notified the other servant devs. [In the meantime.](https://gist.github.com/anonymous/a99bc6b36ac2db64878e#comment-1481760) – Alp Mestanogullari Jun 27 '15 at 09:13
  • @AlpMestanogullari Thank you. Do you mind putting that as the answer and I will accept it? I have not spent much time trying to understand how the whole sever was constructed and was just pattern matching based on what others have done. It has worked ok so far but shows I need to really learn how things work together to make changes to other people's code. – Ecognium Jun 27 '15 at 18:02
  • 1
    @Ecognium Sure, I'll try and write up a minimal complete answer. For more questions, feel free to drop by the **#servant** IRC channel on freenode, I would gladly give a shot at some more explanations in real time. – Alp Mestanogullari Jun 28 '15 at 10:37
  • Note that this is now [an issue](https://github.com/haskell-servant/servant/issues/157) with a very simple solution that should be included in the next release. – Alp Mestanogullari Jul 29 '15 at 16:26

1 Answers1

8

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).

Alp Mestanogullari
  • 1,042
  • 7
  • 13