3

I'm trying to understand the purpose of Servant's serveWithContext function. The documentation states that it's not a replacement for the ReaderT Monad, but I'm uncertain as to what problem it's trying to solve that isn't already addressed by ReaderT.

For example, this is from the example on the servant-auth GitHub page:

unprotected :: CookieSettings -> JWTSettings -> Server Unprotected
unprotected cs jwts = checkCreds cs jwts :<|> serveDirectory "example/static"

server :: CookieSettings -> JWTSettings -> Server (API auths)
server cs jwts = protected :<|> unprotected cs jwts
let jwtCfg = defaultJWTSettings myKey
      cfg = defaultCookieSettings :. jwtCfg :. EmptyContext
      api = Proxy :: Proxy (API '[JWT])
  _ <- forkIO $ run 7249 $ serveWithContext api cfg (server defaultCookieSettings jwtCfg)

It seems that serveWithContext is being used to pass Cookie and JWT settings to the handler, but I don't see why this couldn't be accomplished with a ReaderT. Furthermore, serveWithContext appears to be passing these values in twice: once as a Context object bound to cfg and again as parameters in the function call server defaultCookieSettings jwtCfg.

I'd appreciate it if someone could demystify Servant's Context type for me.

  • In the `mtl`, to a first approximation, each kind of effect can only be in the transformer stack once. One thing a custom monad (transformer) could do that `ReaderT` couldn't do is avoid taking up the one available `ReaderT` slot. I don't know if that's the actual motivation, but it's the only thing I can think of. – Daniel Wagner Nov 22 '21 at 21:47

1 Answers1

3

It seems like the machinery of Servant doesn't make assumptions about the underlying monad in which you choose to define the handlers. This means it can't force you to choose any particular monad (like, say, ReaderT) in response to some combinator present in the routes, and it doesn't "react" to your choice of monad in order to enable some behavior.

In the case of servant-auth, we need to provide some extra information to servant about how to handle cookies and the like.

What the Context system does it to collect that extra information in a positional parameter for route, hoistServerWithContext and serveWithContext, while still letting you choose whatever monad you wish. The exact type of the parameter depends on what route combinators are present in the API.

The servant tutorial has some paragraphs about Contexts:

When someone makes a request to our "private" API, we’re going to need to provide to servant the logic for validating usernames and passwords. [...] Servant 0.5 introduced Context to handle this. [...] the idea is simple: provide some data to the serve function, and that data is propagated to the functions that handle each combinator.

As for

Furthermore, serveWithContext appears to be passing these values in twice

I'm not sure, but I suspect checkCreds taking cs and jwts as parameters is done only as an example of how authentication would be performed if done purely in handlers, without help from Servant itself. In contrast, the protected endpoint already receives the result of the authentication as a parameter; it doesn't have to perform it itself.

In a real-world application, server wouldn't take those parameters, they would only be passed in the Context.

danidiaz
  • 26,936
  • 4
  • 45
  • 95
  • So as far as I understand it, `Context` is used to pass data to a combinator whereas a ReaderT would be used to pass data to a handler function. For example, the `HasServer` instance for the `Auth` combinator has the following typeclass constraint: `HasContextEntry ctxs CookieSettings, HasContextEntry ctxs JWTSettings`. This means that in order to use the `Auth` combinator in a route, we need to provide `CookieSettings` and `JWTSettings` through the `Context` object so that they can be accessed by the `HasServer` methods. Is this correct? – Joshua Billson Nov 27 '21 at 22:40
  • @JoshuaBillson Yes, that's the idea. – danidiaz Nov 28 '21 at 08:23