grapesy-1.0.0: src/Network/GRPC/Client/Session.hs
{-# LANGUAGE OverloadedStrings #-}
module Network.GRPC.Client.Session (
ClientSession(..)
, ClientInbound
, ClientOutbound
, Headers(..)
-- * Exceptions
, CallSetupFailure(..)
, InvalidTrailers(..)
) where
import Control.Exception
import Data.Proxy
import Data.Void
import Network.HTTP.Types qualified as HTTP
import Network.GRPC.Client.Connection (Connection, ConnParams(..))
import Network.GRPC.Client.Connection qualified as Connection
import Network.GRPC.Common
import Network.GRPC.Common.Compression qualified as Compr
import Network.GRPC.Common.Headers
import Network.GRPC.Spec
import Network.GRPC.Spec.Serialization
import Network.GRPC.Util.Session
{-------------------------------------------------------------------------------
Definition
-------------------------------------------------------------------------------}
data ClientSession rpc = ClientSession {
clientConnection :: Connection
}
{-------------------------------------------------------------------------------
Instances
-------------------------------------------------------------------------------}
data ClientInbound rpc
data ClientOutbound rpc
instance IsRPC rpc => DataFlow (ClientInbound rpc) where
data Headers (ClientInbound rpc) = InboundHeaders {
inbHeaders :: ResponseHeaders' HandledSynthesized
, inbCompression :: Compression
}
deriving (Show)
type Message (ClientInbound rpc) = (InboundMeta, Output rpc)
type Trailers (ClientInbound rpc) = ProperTrailers'
type NoMessages (ClientInbound rpc) = TrailersOnly' HandledSynthesized
instance IsRPC rpc => DataFlow (ClientOutbound rpc) where
data Headers (ClientOutbound rpc) = OutboundHeaders {
outHeaders :: RequestHeaders
, outCompression :: Compression
}
deriving (Show)
type Message (ClientOutbound rpc) = (OutboundMeta, Input rpc)
type Trailers (ClientOutbound rpc) = NoMetadata
-- gRPC does not support a Trailers-Only case for requests
-- (indeed, does not support request trailers at all).
type NoMessages (ClientOutbound rpc) = Void
instance SupportsClientRpc rpc => IsSession (ClientSession rpc) where
type Inbound (ClientSession rpc) = ClientInbound rpc
type Outbound (ClientSession rpc) = ClientOutbound rpc
buildOutboundTrailers _ = \NoMetadata -> []
parseInboundTrailers _ = \trailers ->
if null trailers then
-- Although we parse the trailers in a lenient fashion (like all
-- headers), only throwing errors for headers that we really need, if we
-- get no trailers at /all/, then most likely something has gone wrong;
-- for example, perhaps an intermediate cache has dropped the gRPC
-- trailers entirely. We therefore check for this case separately and
-- throw a different error.
--
-- We /must/ throw a GrpcException (rather than some kind of custom one)
-- because the spec mandates that we synthesize a status and status
-- message when the peer omits them.
throwIO $ GrpcException {
grpcError = GrpcUnknown
, grpcErrorMessage = Just "Call closed without trailers"
, grpcErrorDetails = Nothing
, grpcErrorMetadata = []
}
else
return $ parseProperTrailers' (Proxy @rpc) trailers
parseMsg _ = parseOutput (Proxy @rpc) . inbCompression
buildMsg _ = buildInput (Proxy @rpc) . outCompression
instance SupportsClientRpc rpc => InitiateSession (ClientSession rpc) where
parseResponse (ClientSession conn) (ResponseInfo status headers body) =
case classifyServerResponse (Proxy @rpc) status headers body of
Left parsed -> do
trailersOnly <- throwSynthesized throwIO parsed
-- We classify the response as Trailers-Only if the grpc-status header
-- is present, or when the HTTP status is anything other than 200 OK
-- (which we treat, as per the spec, as an implicit grpc-status).
-- The "closed without trailers" case is therefore not relevant.
case verifyAllIf connVerifyHeaders trailersOnly of
Left err -> throwIO $ CallSetupInvalidResponseHeaders err
Right _hdrs -> return $ FlowStartNoMessages trailersOnly
Right parsed -> do
responseHeaders <- throwSynthesized throwIO parsed
case verifyAllIf connVerifyHeaders responseHeaders of
Left err -> throwIO $ CallSetupInvalidResponseHeaders err
Right hdrs -> do
Connection.updateConnectionMeta conn responseHeaders
cIn <- getCompression $ requiredResponseCompression hdrs
return $ FlowStartRegular $ InboundHeaders {
inbHeaders = responseHeaders
, inbCompression = cIn
}
where
ConnParams{
connCompression
, connVerifyHeaders
} = Connection.connParams conn
getCompression :: Maybe CompressionId -> IO Compression
getCompression Nothing = return noCompression
getCompression (Just cid) =
case Compr.getSupported connCompression cid of
Just compr -> return compr
Nothing -> throwIO $ CallSetupUnsupportedCompression cid
buildRequestInfo _ start = RequestInfo {
requestMethod = rawMethod resourceHeaders
, requestPath = rawPath resourceHeaders
, requestHeaders = buildRequestHeaders (Proxy @rpc) $
case start of
FlowStartRegular headers -> outHeaders headers
FlowStartNoMessages impossible -> absurd impossible
}
where
resourceHeaders :: RawResourceHeaders
resourceHeaders = buildResourceHeaders $ ResourceHeaders {
resourceMethod = Post
, resourcePath = rpcPath (Proxy @rpc)
}
instance NoTrailers (ClientSession rpc) where
noTrailers _ = NoMetadata
{-------------------------------------------------------------------------------
Exceptions
-------------------------------------------------------------------------------}
data CallSetupFailure =
-- | Server chose an unsupported compression algorithm
CallSetupUnsupportedCompression CompressionId
-- | We failed to parse the response headers
| CallSetupInvalidResponseHeaders (InvalidHeaders HandledSynthesized)
deriving stock (Show)
deriving anyclass (Exception)
-- | We failed to parse the response trailers
data InvalidTrailers =
-- | Some of the trailers could not be parsed
InvalidTrailers {
invalidTrailers :: [HTTP.Header]
, invalidTrailersError :: String
}
deriving stock (Show)
deriving anyclass (Exception)