grpc-spec-1.0.0: src/Network/GRPC/Spec/Serialization/Headers/Common.hs
{-# LANGUAGE OverloadedStrings #-}
module Network.GRPC.Spec.Serialization.Headers.Common (
-- * Content type
buildContentType
, parseContentType
-- * Message type
, buildMessageType
, parseMessageType
-- * Message encoding
, buildMessageEncoding
, buildMessageAcceptEncoding
, parseMessageEncoding
, parseMessageAcceptEncoding
-- * Utilities
, trim
) where
import Control.Monad
import Control.Monad.Except
import Data.ByteString qualified as BS.Strict
import Data.ByteString qualified as Strict (ByteString)
import Data.ByteString.Char8 qualified as BS.Strict.C8
import Data.Foldable (toList)
import Data.List (intersperse)
import Data.List.NonEmpty (NonEmpty(..))
import Data.Proxy
import Data.Word
import Network.HTTP.Types qualified as HTTP
import Network.GRPC.Spec
import Network.GRPC.Spec.Util.ByteString
{-------------------------------------------------------------------------------
> Content-Type →
> "content-type"
> "application/grpc"
> [("+proto" / "+json" / {custom})]
-------------------------------------------------------------------------------}
buildContentType ::
Maybe Strict.ByteString -- ^ Content-type, if known
-> HTTP.Header
buildContentType mContentType = (
"content-type"
, case mContentType of
Nothing -> "application/grpc"
Just ct -> ct
)
-- | Parse @content-type@ header
--
-- The gRPC spec mandates different behaviour here for requests and responses:
-- when parsing a request (i.e., on the server), the spec requires that the
-- server responds with @415 Unsupported Media Type@. When parsing a response,
-- however (i.e., on the client), the spec mandates that we synthesize a
-- gRPC exception. We therefore take a function as parameter to construct the
-- actual error.
parseContentType :: forall m rpc.
(MonadError (InvalidHeaders GrpcException) m, IsRPC rpc)
=> Proxy rpc
-> (String -> InvalidHeaders GrpcException)
-> HTTP.Header
-> m ContentType
parseContentType proxy invalid (_name, value) = do
if value == rpcContentType proxy then
return ContentTypeDefault
else do
-- Headers must be ASCII, justifying the use of BS.Strict.C8.
-- See <https://www.rfc-editor.org/rfc/rfc7230#section-3.2.4>.
-- The gRPC spec does not allow for quoted strings.
withoutPrefix <-
case BS.Strict.C8.stripPrefix "application/grpc" value of
Nothing -> err "Missing \"application/grpc\" prefix."
Just remainder -> return remainder
-- The gRPC spec does not allow for any parameters.
when (';' `BS.Strict.C8.elem` withoutPrefix) $
err "Unexpected parameter."
-- Check format
--
-- The only @format@ we should allow is @serializationFormat proxy@.
-- However, some non-conforming proxies use formats such as
-- @application/grpc+octet-stream@. We therefore ignore @format@ here.
if BS.Strict.C8.null withoutPrefix then
-- Accept "application/grpc"
return $ ContentTypeOverride value
else
case BS.Strict.C8.stripPrefix "+" withoutPrefix of
Just _format ->
-- Accept "application/grpc+<format>"
return $ ContentTypeOverride value
Nothing ->
err "Invalid subtype."
where
err :: String -> m a
err reason = throwError . invalid . concat $ [
reason
, " Expected \"application/grpc\" or \""
, BS.Strict.C8.unpack $
rpcContentType proxy
, "\", with \""
, "application/grpc+{other_format}"
, "\" also accepted."
]
{-------------------------------------------------------------------------------
> Message-Type → "grpc-message-type" {type name for message schema}
-------------------------------------------------------------------------------}
buildMessageType ::
IsRPC rpc
=> Proxy rpc
-> MessageType
-> Maybe HTTP.Header
buildMessageType proxy messageType =
mkHeader <$> chooseMessageType proxy messageType
where
mkHeader :: Strict.ByteString -> HTTP.Header
mkHeader = ("grpc-message-type",)
-- | Parse message type
--
-- We do not need the @grpc-message-type@ header in order to know the message
-- type, because the /path/ determines the service and method, and that in turn
-- determines the message type. Therefore, if the value is not what we expect,
-- we merely record this fact ('MessageTypeOverride') but don't otherwise do
-- anything differently.
parseMessageType :: forall rpc.
IsRPC rpc
=> Proxy rpc
-> HTTP.Header
-> MessageType
parseMessageType proxy (_name, given) =
case rpcMessageType proxy of
Nothing ->
-- We expected no message type at all, but did get one
MessageTypeOverride given
Just expected ->
if expected == given
then MessageTypeDefault
else MessageTypeOverride given
{-------------------------------------------------------------------------------
> Message-Encoding → "grpc-encoding" Content-Coding
> Content-Coding → "identity" / "gzip" / "deflate" / "snappy" / {custom}
-------------------------------------------------------------------------------}
buildMessageEncoding :: CompressionId -> HTTP.Header
buildMessageEncoding compr = (
"grpc-encoding"
, serializeCompressionId compr
)
parseMessageEncoding ::
MonadError (InvalidHeaders GrpcException) m
=> HTTP.Header
-> m CompressionId
parseMessageEncoding (_name, value) =
return $ deserializeCompressionId value
{-------------------------------------------------------------------------------
> Message-Accept-Encoding →
> "grpc-accept-encoding" Content-Coding *("," Content-Coding)
-------------------------------------------------------------------------------}
buildMessageAcceptEncoding :: NonEmpty CompressionId -> HTTP.Header
buildMessageAcceptEncoding compr = (
"grpc-accept-encoding"
, mconcat . intersperse "," . map serializeCompressionId $ toList compr
)
parseMessageAcceptEncoding :: forall m.
MonadError (InvalidHeaders GrpcException) m
=> HTTP.Header
-> m (NonEmpty CompressionId)
parseMessageAcceptEncoding hdr@(_name, value) =
atLeastOne
. map (deserializeCompressionId . strip)
. BS.Strict.splitWith (== ascii ',')
$ value
where
atLeastOne :: forall a. [a] -> m (NonEmpty a)
atLeastOne (x : xs) = return (x :| xs)
atLeastOne [] = throwError $ invalidHeader Nothing hdr $
"Expected at least one compresion ID"
{-------------------------------------------------------------------------------
Utilities
-------------------------------------------------------------------------------}
-- | Trim leading or trailing whitespace
--
-- We only allow for space and tab, based on
-- <https://www.rfc-editor.org/rfc/rfc9110.html#name-whitespace>.
trim :: Strict.ByteString -> Strict.ByteString
trim = ltrim . rtrim
where
ltrim, rtrim :: Strict.ByteString -> Strict.ByteString
ltrim = BS.Strict.dropWhile isSpace
rtrim = BS.Strict.dropWhileEnd isSpace
isSpace :: Word8 -> Bool
isSpace x = x == 32 || x == 9