{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Servant.Common.Req where import Control.Applicative import Control.Exception import Control.Monad import Control.Monad.Catch (MonadThrow) import Control.Monad.IO.Class import Control.Monad.Trans.Either import Data.ByteString.Lazy hiding (pack, filter, map, null, elem) import Data.IORef import Data.String import Data.String.Conversions import Data.Proxy import Data.Text (Text) import Data.Text.Encoding import Network.HTTP.Client hiding (Proxy) import Network.HTTP.Client.TLS import Network.HTTP.Media import Network.HTTP.Types import Network.URI import Servant.API.ContentTypes import Servant.Common.BaseUrl import Servant.Common.Text import System.IO.Unsafe import qualified Network.HTTP.Client as Client data ServantError = FailureResponse { responseStatus :: Status , responseContentType :: MediaType , responseBody :: ByteString } | DecodeFailure { decodeError :: String , responseContentType :: MediaType , responseBody :: ByteString } | UnsupportedContentType { responseContentType :: MediaType , responseBody :: ByteString } | ConnectionError { connectionError :: HttpException } | InvalidContentTypeHeader { responseContentTypeHeader :: ByteString , responseBody :: ByteString } deriving (Show) data Req = Req { reqPath :: String , qs :: QueryText , reqBody :: Maybe (ByteString, MediaType) , reqAccept :: [MediaType] , headers :: [(String, Text)] } defReq :: Req defReq = Req "" [] Nothing [] [] appendToPath :: String -> Req -> Req appendToPath p req = req { reqPath = reqPath req ++ "/" ++ p } appendToMatrixParams :: String -> Maybe String -> Req -> Req appendToMatrixParams pname pvalue req = req { reqPath = reqPath req ++ ";" ++ pname ++ maybe "" ("=" ++) pvalue } appendToQueryString :: Text -- ^ param name -> Maybe Text -- ^ param value -> Req -> Req appendToQueryString pname pvalue req = req { qs = qs req ++ [(pname, pvalue)] } addHeader :: ToText a => String -> a -> Req -> Req addHeader name val req = req { headers = headers req ++ [(name, toText val)] } setRQBody :: ByteString -> MediaType -> Req -> Req setRQBody b t req = req { reqBody = Just (b, t) } reqToRequest :: (Functor m, MonadThrow m) => Req -> BaseUrl -> m Request reqToRequest req (BaseUrl reqScheme reqHost reqPort) = fmap (setheaders . setAccept . setrqb . setQS ) $ parseUrl url where url = show $ nullURI { uriScheme = case reqScheme of Http -> "http:" Https -> "https:" , uriAuthority = Just $ URIAuth { uriUserInfo = "" , uriRegName = reqHost , uriPort = ":" ++ show reqPort } , uriPath = reqPath req } setrqb r = case reqBody req of Nothing -> r Just (b,t) -> r { requestBody = RequestBodyLBS b , requestHeaders = requestHeaders r ++ [(hContentType, cs . show $ t)] } setQS = setQueryString $ queryTextToQuery (qs req) setheaders r = r { requestHeaders = requestHeaders r <> fmap toProperHeader (headers req) } setAccept r = r { requestHeaders = filter ((/= "Accept") . fst) (requestHeaders r) <> [("Accept", renderHeader $ reqAccept req) | not . null . reqAccept $ req] } toProperHeader (name, val) = (fromString name, encodeUtf8 val) -- * performing requests {-# NOINLINE __manager #-} __manager :: IORef Manager __manager = unsafePerformIO (newManager tlsManagerSettings >>= newIORef) __withGlobalManager :: (Manager -> IO a) -> IO a __withGlobalManager action = readIORef __manager >>= action displayHttpRequest :: Method -> String displayHttpRequest httpmethod = "HTTP " ++ cs httpmethod ++ " request" performRequest :: Method -> Req -> (Int -> Bool) -> BaseUrl -> EitherT ServantError IO (Int, ByteString, MediaType, Response ByteString) performRequest reqMethod req isWantedStatus reqHost = do partialRequest <- liftIO $ reqToRequest req reqHost let request = partialRequest { Client.method = reqMethod , checkStatus = \ _status _headers _cookies -> Nothing } eResponse <- liftIO $ __withGlobalManager $ \ manager -> catchHttpException $ Client.httpLbs request manager case eResponse of Left err -> left $ ConnectionError err Right response -> do let status = Client.responseStatus response body = Client.responseBody response status_code = statusCode status ct <- case lookup "Content-Type" $ Client.responseHeaders response of Nothing -> pure $ "application"//"octet-stream" Just t -> case parseAccept t of Nothing -> left $ InvalidContentTypeHeader (cs t) body Just t' -> pure t' unless (isWantedStatus status_code) $ left $ FailureResponse status ct body return (status_code, body, ct, response) performRequestCT :: MimeUnrender ct result => Proxy ct -> Method -> Req -> [Int] -> BaseUrl -> EitherT ServantError IO result performRequestCT ct reqMethod req wantedStatus reqHost = do let acceptCT = contentType ct (_status, respBody, respCT, _response) <- performRequest reqMethod (req { reqAccept = [acceptCT] }) (`elem` wantedStatus) reqHost unless (matches respCT (acceptCT)) $ left $ UnsupportedContentType respCT respBody either (left . (\s -> DecodeFailure s respCT respBody)) return (mimeUnrender ct respBody) performRequestNoBody :: Method -> Req -> [Int] -> BaseUrl -> EitherT ServantError IO () performRequestNoBody reqMethod req wantedStatus reqHost = do _ <- performRequest reqMethod req (`elem` wantedStatus) reqHost return () catchHttpException :: IO a -> IO (Either HttpException a) catchHttpException action = catch (Right <$> action) (pure . Left)