snapframework / snap-core

Core type definitions (Snap monad, HTTP types, etc) and utilities for web handlers.
http://snapframework.com/
BSD 3-Clause "New" or "Revised" License
317 stars 85 forks source link

IO exception: A web handler threw an exception. Details: no value #318

Closed rubenmoor closed 2 years ago

rubenmoor commented 2 years ago

I got this error message and can't make out the cause:

[22/Jun/2022:21:43:28 -0500] During processing of request from 127.0.0.1:35772
request:
"GET /api/user/app/get HTTP/1.1\naccept-language: en-US,en;q=0.9\nreferer: http://localhost:8000/\nsec-fetch-dest: empty\nsec-fetch-mode: cors\nsec-fetch-site: same-origin\naccept: */*\nsec-ch-ua-platform: \"Linux\"\nx-alias: rubenmoor\nuser-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36\nsec-ch-ua-mobile: ?0\nauthorization: Bearer eyJhbGciOiJFUzM4NCJ9.eyJzdWIiOiJydWJlbm1vb3IiLCJleHAiOjEuNjU1OTQzNjM4NTY0Mzg1Mjc5ZTksImlhdCI6MS42NTU5NDM2MDg1NjQzODUyNzllOSwiYXVkIjoiaHR0cHM6Ly9wYWxhbnR5cGUuY29tIn0.td1jCKoE5QSciU2gUde3FqFrCljSCQzJUwTS09EvfA9K0YY6YZrlfxZakcwCTPLo7c8jI3c5xI32maQok5lQUp50WUcxXz_k1NNeeDmuQfPq0P34HmEJBmcH7S-Dog8G\nsec-ch-ua: \" Not A;Brand\";v=\"99\", \"Chromium\";v=\"102\", \"Google Chrome\";v=\"102\"\nconnection: keep-alive\nhost: localhost:8000\nx-real-ip: 127.0.0.1\naccept-encoding: gzip\n\nsn=\"localhost:8000\" c=127.0.0.1:35772 s=127.0.0.1:56117 ctx=/ clen=n/a"
A web handler threw an exception. Details:
no value
127.0.0.1 - - [22/Jun/2022:21:43:28 -0500] "GET /api/user/app/get HTTP/1.1" 500 51 "http://localhost:8000/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"

The only hint I get from Snap is

A web handler threw an exception. Details:
no value

I first checked the handler for /api/usr/app/get but it's a really lean handler that doesn't do much. I then checked the authentication middleware which is custom code of mine. It looks Snap throws an IO exception somewhere while verifying my Json web token ... but I do proper error handling, and by adding catch, I couldn't find the offending piece of code.

Also, this my authentication middle ware is tested and worked fine about a year ago.

The only thing that I could find in the snap code on github and that resembles the error is this:

-- https://github.com/snapframework/snap-core/blob/04295679ccf8316fcf4944f2eb65d1b5266587ef/src/Snap/Internal/Core.hs
evalSnap :: Snap a
         -> (ByteString -> IO ())
         -> ((Int -> Int) -> IO ())
         -> Request
         -> IO a
evalSnap (Snap m) logerr timeoutAction req =
    m (\v _ -> return v) diediedie ss
  where
    diediedie z _ = case z of
      PassOnProcessing     -> throwIO $ NoHandlerException "pass"
      (EarlyTermination _) -> throwIO $ ErrorCall "no value"
      (EscapeSnap e)       -> throwIO e

    dresp = emptyResponse
    ss = SnapState req dresp logerr timeoutAction
{-# INLINE evalSnap #-}

This is my code:

mkContext :: JWK -> Pool SqlBackend -> Context '[Snap UserInfo]
mkContext jwk pool =
    let
        authHandler :: Snap UserInfo
        authHandler = do
            let toServerError e =
                    Servant.throwError $ err500 { errBody = BSU.fromString e }
            mAuth <- getHeader "Authorization" <$> getRequest
            auth  <- maybe (toServerError "authorization header missing")
                           pure
                           mAuth
            mAlias  <- getHeader "X-Alias" <$> getRequest
            aliasBs <- maybe (toServerError "X-Alias header missing")
                             pure
                             mAlias
            uiAliasName <-
                either
                        (\s -> toServerError $ "parseHeader alias: " <> show s)
                        pure
                    $ parseHeader aliasBs
            jwt <-
                either (\s -> toServerError $ "parseHeader jwt: " <> show s)
                       pure
                    $ parseHeader auth

            eSub       <- liftIO $ catch (runExceptT $ verifyCompactJWT jwk jwt) handler
            uiUserName <- either
                (\s -> toServerError $ "verify jwt: " <> show s)
                (liftIO (putStrLn "jwt verified successfully") $>)
                eSub
            ls <- runDb' pool $ select $ from $ \(a `InnerJoin` u) -> do
                on $ a Database.Gerippe.^. AliasFkUser ==. u Database.Gerippe.^. UserId
                where_ $ u Database.Gerippe.^. UserName ==. val uiUserName
                  &&. a Database.Gerippe.^. AliasName   ==. val uiAliasName
                pure (u, a)
            (Entity _ User {..}, Entity uiKeyAlias uiAlias) <- case ls of
                [entry] -> pure entry
                _ -> toServerError $ "user not found: " <> Text.unpack uiUserName
            let uiIsSiteAdmin = userIsSiteAdmin
            uiClearances <- runDb' pool (getWhere ClearanceFkAlias uiKeyAlias) >>= \case
                [Entity _ Clearance{..}] -> pure clearanceRank
                _                        -> toServerError $ "mkContext: clearance: expect unique entry"
                -- for clearances $ \(Entity _ Clearance{..}, Entity _ Db.Podcast{..}) -> (podcastIdentifier, clearanceRank)
            pure $ UserInfo { .. }
    in
        authHandler :. EmptyContext
rubenmoor commented 2 years ago

I found the cause of my error:

When using evalSnap, the function throwError :: MonadSnap m => ServantErr -> m a from servant-snap doesn't work. Instead of getting the HTTP error that I specify, I get the IO exception above (throwIO $ ErrorCall "no value").

I don't know whether this problems originiates in evalSnap and thus snap-core or within throwError and thus servant-snap.

I also don't know whether this is tolerated behavior. Maybe evalSnap isn't meant to be used with throwError. Maybe I can safely switch to runSnap.

I am currently working with snap-core-1.0.4.1 and snap-server-1.1.1.2 and can't easily swap out either of them.

rubenmoor commented 2 years ago

Given that the documentation of servant-snap states

throwError :: MonadSnap m => ServantErr -> m a

Terminate request handling with a ServantErr via finishWith

... and evalSnap has a very specific use, not a general one, I conclude this is by no means a bug. I guess I would prefer a variant of evalSnap that evaluates to IO (Maybe a) or something instead of throwing IO exceptions.