使用 RIO 服务 Servant NoContent 响应

Serving a Servant NoContent response with RIO

在我尝试编写经过身份验证的 Servant API 时,处理程序使用 RIO monad 而不是 Servant 自己的 Handler monad,我被困在经过身份验证的路由上 return 无内容;即,Servant 的 NoContent 类型。当我尝试使用 hoistServerWithContextRIO 服务器提升到 Handler 时,我收到一个我不理解的类型错误。

这是简化的 API 和服务器设置:

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

其中 User 是一种记录类型,可以作为 cookie 的一部分序列化为 JWT。为了提升服务器,我按照示例 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

上述提升对于 return 某些内容的端点工作正常。但是,当 :<|> SV.DeleteNoContent 出现在 ProtectedEndpoint 中(以及服务器中的相应部分)时,我得到以下类型错误:

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’

没有认证的端点不会出现这个问题;例如,UnprotectedEndpoint 而不是 API 类型定义中的 (AS.Auth '[AS.Cookie] User :> ProtectedEndpoint)

hoistServerWithContextHasServer 类型 class 的函数,但我不确定这里关注的是哪个实例。如果我让 GHC 推断类型,我得到

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

对我来说,类型错误(加上我添加和删除无内容处理程序的实验)表明由 Servant 的类型机制派生的 protectedServer 不是 HasServer 类型的成员 class。但我的 Haskell 类型级编程技能似乎无法胜任这项任务。问题到底出在哪里?我是否缺少类型注释?语言扩展?

类型错误似乎是因为仆人当前不允许将 headers 添加到 NoContentVerb,因为缺少相应的类型实例。见 Servant-Auth issue here.

尽管我不完全理解细节,但上述问题评论中的以下解决方法避免了类型错误:

type instance ASC.AddSetCookieApi (SV.NoContentVerb 'SV.DELETE)
  = SV.Verb 'SV.DELETE 204 '[SV.JSON] (ASC.AddSetCookieApiVerb SV.NoContent)