grpc-spec-1.0.0: test-grpc-spec/Test/Prop/Serialization.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE OverloadedLabels #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Test.Prop.Serialization (tests) where
import Control.Lens ((.~))
import Control.Monad
import Control.Monad.Except (Except, runExcept)
import Control.Monad.State
import Data.Bifunctor
import Data.ByteString qualified as BS.Strict
import Data.ByteString qualified as Strict (ByteString)
import Data.ByteString.Base64 qualified as BS.Strict.Base64
import Data.ByteString.Char8 qualified as BS.Strict.Char8
import Data.CaseInsensitive qualified as CI
import Data.Char (isSpace)
import Data.Function
import Data.List (nubBy, uncons, intersperse)
import Data.Maybe (mapMaybe)
import Data.ProtoLens.Labels ()
import Data.ProtoLens.Message
import Data.Proxy
import Data.Text.Lazy qualified as Text.Lazy
import Data.TreeDiff (ToExpr, ediff, ansiWlEditExpr)
import Data.Void (Void)
import Network.HTTP.Types qualified as HTTP
import Prettyprinter (Doc)
import Prettyprinter qualified as PP
import Prettyprinter.Render.Terminal (AnsiStyle)
import Prettyprinter.Render.Terminal qualified as PP.Ansi
import Test.Tasty hiding (Timeout)
import Test.Tasty.QuickCheck
import Network.GRPC.Spec
import Network.GRPC.Spec.Serialization
import Test.Util.Awkward
import Test.Util.Orphans ()
tests :: TestTree
tests = testGroup "Test.Prop.Serialization" [
testGroup "Base64" [
testProperty "roundtrip" $
roundtrip buildBinaryValue parseBinaryValue
, testProperty "emitUnpadded" $ \(Awkward value) ->
binaryNotPadded value
, testProperty "acceptPadded" $
roundtrip buildBinaryPadded parseBinaryValue
, testProperty "acceptCommas" $ \cs ->
roundtripWith
(showIntermediate .*. labelHasMultipleChunks)
(buildBinaryChunked cs)
parseBinaryValue
]
, testGroup "Headers" [
testProperty "CustomMetadata" $
roundtrip buildCustomMetadata parseCustomMetadata
, testProperty "Timeout" $
roundtrip buildTimeout parseTimeout
, testProperty "Pushback" $
roundtrip @Void buildPushback parsePushback
, testProperty "RequestHeaders" $
roundtrip (buildRequestHeaders rpc)
(parseRequestHeaders rpc)
, testProperty "ResponseHeaders" $
roundtrip (buildResponseHeaders rpc)
(parseResponseHeaders rpc)
, testProperty "ProperTrailers" $
roundtrip buildProperTrailers
(parseProperTrailers rpc)
, testProperty "TrailersOnly" $
roundtrip (buildTrailersOnly' rpc)
(parseTrailersOnly rpc)
]
, testGroup "Duplicates" [
testProperty "RequestHeaders" $ \dups ->
roundtripWith
(showIntermediate .*. labelDups)
(introduceDups dups . buildRequestHeaders rpc)
( parseRequestHeaders rpc)
, testProperty "ResponseHeaders" $ \dups ->
roundtripWith
(showIntermediate .*. labelDups)
(introduceDups dups . buildResponseHeaders rpc)
( parseResponseHeaders rpc)
, testProperty "ProperTrailers" $ \dups ->
roundtripWith
(showIntermediate .*. labelDups)
(introduceDups dups . buildProperTrailers)
( parseProperTrailers rpc)
, testProperty "TrailersOnly" $ \dups ->
roundtripWith
(showIntermediate .*. labelDups)
(introduceDups dups . buildTrailersOnly' rpc)
( parseTrailersOnly rpc)
]
, testGroup "Padding" [
testProperty "RequestHeaders" $ \padding ->
roundtripWith
(showIntermediate .*. labelPadding)
(introducePadding padding . buildRequestHeaders rpc)
( parseRequestHeaders rpc)
, testProperty "ResponseHeaders" $ \padding ->
roundtripWith
(showIntermediate .*. labelPadding)
(introducePadding padding . buildResponseHeaders rpc)
( parseResponseHeaders rpc)
, testProperty "ProperTrailers" $ \padding ->
roundtripWith
(showIntermediate .*. labelPadding)
(introducePadding padding . buildProperTrailers)
( parseProperTrailers rpc)
, testProperty "TrailersOnly" $ \padding ->
roundtripWith
(showIntermediate .*. labelPadding)
(introducePadding padding . buildTrailersOnly' rpc)
( parseTrailersOnly rpc)
]
]
where
rpc = Proxy @(RawRpc "serv" "meth")
{-------------------------------------------------------------------------------
Binary headers
-------------------------------------------------------------------------------}
-- | The gRPC spec mandates we should not /create/ padded values
binaryNotPadded :: Strict.ByteString -> Bool
binaryNotPadded = BS.Strict.Char8.all (/= '=') . buildBinaryValue
-- | The gRPC spec mandates we must /accept/ padded values
--
-- We cannot call 'buildBinaryValue' here, because it creates padded values
-- (as tested with 'binaryNotPadded').
buildBinaryPadded :: Strict.ByteString -> Strict.ByteString
buildBinaryPadded = BS.Strict.Base64.encode
-- | Build binary value by encoding chunks separately and joining the results
--
-- See 'parseBinaryValue' for detailed discussion.
--
-- We depend on the presence of commas here; see discussion in 'introduceDups',
-- with the corresponding statistics relevant to this case collected by
-- 'labelHasMultipleChunks'.
buildBinaryChunked :: [Bool] -> Strict.ByteString -> Strict.ByteString
buildBinaryChunked ds value =
mconcat . intersperse "," $
map buildBinaryValue chunks
where
chunks :: [Strict.ByteString]
(chunks, _) = splitHeaderValueBS ds value
labelHasMultipleChunks :: (a, Strict.ByteString) -> Property -> Property
labelHasMultipleChunks (_, bs) =
tabulate "has multiple chunks" [
show $ ',' `BS.Strict.Char8.elem` bs
]
{-------------------------------------------------------------------------------
Duplicates in Custom-Metadata
The spec mandates:
> Custom-Metadata header order is not guaranteed to be preserved except for
> values with duplicate header names. Duplicate header names may have their
> values joined with "," as the delimiter and be considered semantically
> equivalent.
We need to be careful with whitespace around the delimeter here; the spec
mandates
> ASCII-Value should not have leading or trailing whitespace. If it contains
> leading or trailing whitespace, it may be stripped.
Since we use an internal representation that does not allow for duplicates, we
test this by introducing duplicates in the /serialized/ form.
-------------------------------------------------------------------------------}
-- | Introduce duplicte headers
--
-- We introduce duplicates by splitting existing custom-metadata at existing
-- limiters (so that the roundtrip still passes), /if/ the corresponding 'Bool'
-- value is 'True' (so that we can shrink towards not introducing duplicates).
--
-- NOTE: Our generator for strict bytestrings generates commas with relatively
-- high probability, which ensures that we have enough source material here to
-- generate duplicate headers. We should keep an eye on the statistics however
-- to ensure that this continues to be the case (see 'labelDups').
introduceDups :: [Bool] -> [HTTP.Header] -> [HTTP.Header]
introduceDups = \dups -> concat . flip evalState dups . mapM go
where
go :: HTTP.Header -> State [Bool] [HTTP.Header]
go hdr@(name, value)
-- Don't split reserved headers (only metadata)
| Nothing <- safeHeaderName (CI.foldedCase name)
= return [hdr]
-- We can split ASCII or binary headers
| otherwise
= state $ \dups -> first (map (name,)) $ splitHeaderValueBS dups value
splitHeaderValueBS ::
[Bool]
-> Strict.ByteString
-> ([Strict.ByteString], [Bool])
splitHeaderValueBS ds =
first (map BS.Strict.Char8.pack)
. splitHeaderValue ds
. BS.Strict.Char8.unpack
-- | Split a header value
--
-- We split the header value at @","@ boundaries, /provided/ that the comma is
-- not preceded or followed by whitespace (otherwise that whitespace would be
-- lost, since individual headers are trimmed).
--
-- Examples:
--
-- > splitHeaderValue [] "abc,def,ghi" == (["abc,def,ghi"] , [])
-- > splitHeaderValue [True] "abc,def,ghi" == (["abc","def,ghi"] , [])
-- > splitHeaderValue [False] "abc,def,ghi" == (["abc,def,ghi"] , [])
-- > splitHeaderValue [False,True] "abc,def,ghi" == (["abc,def","ghi"] , [])
-- > splitHeaderValue [True,True] "abc,def,ghi" == (["abc","def","ghi"] , [])
-- > splitHeaderValue [True,False,True] "abc,def" == (["abc","def"] , [False,True])
-- > splitHeaderValue [True] "abc, def,ghi" == (["abc, def","ghi"] , [])
-- > splitHeaderValue [True] "abc ,def,ghi" == (["abc ,def","ghi"] , [])
splitHeaderValue ::
[Bool] -- ^ Allowed splits (useful to shrink towards splitting less)
-> String -- ^ String to split
-> ([String], [Bool])
splitHeaderValue = go []
where
go ::
[Char] -- Accumulated chunk, in reverse order
-> [Bool] -- Allowed splits left
-> String -- String left to process
-> ([String], [Bool])
go acc [] xs = finalize acc [] xs
go acc ds [] = finalize acc ds []
go acc (d:ds) (x:xs)
| canSplit, d = first (reverse acc :) $ go [] ds xs
| canSplit, not d = go (x:acc) ds xs
| otherwise = go (x:acc) (d:ds) xs
where
prevIsSpace, nextIsSpace, canSplit :: Bool
prevIsSpace = maybe False (isSpace . fst) $ uncons acc
nextIsSpace = maybe False (isSpace . fst) $ uncons xs
canSplit = x == ',' && not prevIsSpace && not nextIsSpace
finalize :: [Char] -> [Bool] -> String -> ([String], [Bool])
finalize acc ds xs = ([reverse acc ++ xs], ds)
labelDups :: (a, [HTTP.Header]) -> Property -> Property
labelDups (_a, headers) =
tabulate "has duplicate headers" [
show $ length headers /= length (nubBy ((==) `on` fst) headers)
]
{-------------------------------------------------------------------------------
Roundtrip tests
-------------------------------------------------------------------------------}
roundtrip :: forall e a b.
(Eq a, ToExpr a, Show b, Show e)
=> (a -> b) -- ^ There
-> (b -> Except e a) -- ^ and back again
-> Awkward a -> Property
roundtrip = roundtripWith showIntermediate
roundtripWith :: forall e a b.
(Eq a, ToExpr a, Show e)
=> ((a, b) -> Property -> Property) -- ^ Statistics, metadata, ..
-> (a -> b) -- ^ There
-> (b -> Except e a) -- ^ and back again
-> Awkward a -> Property
roundtripWith modProp there back (Awkward a) =
modProp (a, b) $
let ma' = runExcept (back b)
in case ma' of
Left err ->
counterexample (show err) False
Right a'
| a == a' ->
property True
| otherwise ->
counterexample (renderDoc $ ansiWlEditExpr $ ediff a a') False
where
b :: b
b = there a
showIntermediate :: Show b => (a, b) -> Property -> Property
showIntermediate (_, b) = counterexample (show b)
(.*.) ::
((a, b) -> Property -> Property)
-> ((a, b) -> Property -> Property)
-> ((a, b) -> Property -> Property)
(.*.) f g (a, b) = f (a, b) . g (a, b)
{-------------------------------------------------------------------------------
Padding
-------------------------------------------------------------------------------}
newtype Padding = Padding {
getPadding :: Strict.ByteString
}
deriving Show
instance Arbitrary Padding where
arbitrary = do
n <- choose (0, 5)
Padding . BS.Strict.Char8.pack <$> replicateM n (elements " \t")
shrink =
map (Padding . BS.Strict.pack)
. shrinkList (const []) -- only remove elements from the list
. BS.Strict.unpack
. getPadding
newtype PaddingPerHeader = PaddingPerHeader {
-- | Left and right padding for each header
getPaddingPerHeader :: [(Padding, Padding)]
}
deriving stock (Show)
instance Arbitrary PaddingPerHeader where
arbitrary = sized $ \sz -> do
n <- choose (0, sz)
PaddingPerHeader <$> replicateM n genPadding
where
-- Most of the time, no padding
genPadding :: Gen (Padding, Padding)
genPadding = frequency [
(7, (,) <$> pure (Padding "") <*> pure (Padding ""))
, (1, (,) <$> arbitrary <*> pure (Padding ""))
, (1, (,) <$> pure (Padding "") <*> arbitrary)
, (1, (,) <$> arbitrary <*> arbitrary)
]
shrink =
map PaddingPerHeader
. shrinkList shrink
. getPaddingPerHeader
introducePadding :: PaddingPerHeader -> [HTTP.Header] -> [HTTP.Header]
introducePadding = \(PaddingPerHeader padding) ->
flip evalState padding . mapM go
where
go :: HTTP.Header -> State [(Padding, Padding)] HTTP.Header
go (name, value) = state $ \case
[] ->
((name, value), [])
(l, r) : padding' ->
((name, getPadding l <> value <> getPadding r), padding')
labelPadding :: (a, [HTTP.Header]) -> Property -> Property
labelPadding (_a, headers) =
tabulate "has padding" [
show $ any (hasPadding . snd) headers
]
where
hasPadding :: Strict.ByteString -> Bool
hasPadding bs = or [
not . BS.Strict.null $ BS.Strict.Char8.takeWhile isSpace bs
, not . BS.Strict.null $ BS.Strict.Char8.takeWhileEnd isSpace bs
]
{-------------------------------------------------------------------------------
Arbitrary instances
We do not yet provide shrinkers for each definition; they should be defined if
and when a test breaks.
-------------------------------------------------------------------------------}
instance Arbitrary (Awkward CustomMetadata) where
arbitrary = Awkward <$> do
name <- genName
awkward `suchThatMap` safeCustomMetadata name
where
genName :: Gen HeaderName
genName = oneof [
getAwkward <$> arbitrary
, fmap (<> "-bin") $ getAwkward <$> arbitrary
] `suchThatMap` safeHeaderName
-- For now we shrink only the value
shrink (Awkward (CustomMetadata name value)) =
mapMaybe (fmap Awkward . safeCustomMetadata name) $ shrink value
instance Arbitrary (Awkward CustomMetadataMap) where
arbitrary = Awkward <$>
customMetadataMapFromList <$> awkward
shrink =
map (Awkward . customMetadataMapFromList . map getAwkward)
. shrink
. (map Awkward . customMetadataMapToList . getAwkward)
instance Arbitrary (Awkward RequestHeaders) where
arbitrary = Awkward <$> do
requestTimeout <- awkward
requestCompression <- awkward
requestAcceptCompression <- awkward
requestContentType <- Just <$> awkward -- cannot be missing
requestMessageType <- Just <$> awkward -- cannot be missing
requestUserAgent <- awkward
requestIncludeTE <- arbitrary
requestTraceContext <- awkward
requestPreviousRpcAttempts <- awkward
requestMetadata <- awkward
return $ RequestHeaders{
requestTimeout
, requestCompression
, requestAcceptCompression
, requestContentType
, requestMessageType
, requestUserAgent
, requestIncludeTE
, requestTraceContext
, requestPreviousRpcAttempts
, requestMetadata
, requestUnrecognized = ()
}
shrink h@(Awkward h') = concat [
shrinkAwkward (\x -> h'{requestTimeout = x}) requestTimeout h
, shrinkAwkward (\x -> h'{requestCompression = x}) requestCompression h
, shrinkAwkward (\x -> h'{requestAcceptCompression = x}) requestAcceptCompression h
, shrinkAwkward (\x -> h'{requestUserAgent = x}) requestUserAgent h
, shrinkRegular (\x -> h'{requestIncludeTE = x}) requestIncludeTE h
, shrinkAwkward (\x -> h'{requestTraceContext = x}) requestTraceContext h
, shrinkAwkward (\x -> h'{requestPreviousRpcAttempts = x}) requestPreviousRpcAttempts h
, shrinkAwkward (\x -> h'{requestMetadata = x}) requestMetadata h
]
instance Arbitrary (Awkward ResponseHeaders) where
arbitrary = Awkward <$> do
responseCompression <- awkward
responseAcceptCompression <- awkward
responseContentType <- Just <$> awkward
responseMetadata <- awkward
return ResponseHeaders {
responseCompression
, responseAcceptCompression
, responseContentType
, responseMetadata
, responseUnrecognized = ()
}
shrink h@(Awkward h') = concat [
shrinkAwkward (\x -> h'{responseCompression = x}) responseCompression h
, shrinkAwkward (\x -> h'{responseAcceptCompression = x}) responseAcceptCompression h
, shrinkAwkward (\x -> h'{responseMetadata = x}) responseMetadata h
]
instance Arbitrary (Awkward ProperTrailers) where
arbitrary = Awkward <$> do
properTrailersGrpcStatus <- awkward
properTrailersGrpcMessage <- awkward
properTrailersStatusDetails <- awkward
properTrailersPushback <- awkward
properTrailersOrcaLoadReport <- awkward
properTrailersMetadata <- awkward
return $ ProperTrailers{
properTrailersGrpcStatus
, properTrailersGrpcMessage
, properTrailersStatusDetails
, properTrailersPushback
, properTrailersOrcaLoadReport
, properTrailersMetadata
, properTrailersUnrecognized = ()
}
shrink h@(Awkward h') = concat [
shrinkAwkward (\x -> h'{properTrailersGrpcStatus = x}) properTrailersGrpcStatus h
, shrinkAwkward (\x -> h'{properTrailersGrpcMessage = x}) properTrailersGrpcMessage h
, shrinkAwkward (\x -> h'{properTrailersMetadata = x}) properTrailersMetadata h
, shrinkAwkward (\x -> h'{properTrailersPushback = x}) properTrailersPushback h
, shrinkAwkward (\x -> h'{properTrailersOrcaLoadReport = x}) properTrailersOrcaLoadReport h
]
instance Arbitrary (Awkward TrailersOnly) where
arbitrary = Awkward <$> do
trailersOnlyContentType <- Just <$> awkward
trailersOnlyProper <- awkward
return $ TrailersOnly {
trailersOnlyContentType
, trailersOnlyProper
}
shrink h@(Awkward h') = concat [
shrinkAwkward (\x -> h'{trailersOnlyContentType = x}) trailersOnlyContentType h
, shrinkAwkward (\x -> h'{trailersOnlyProper = x}) trailersOnlyProper h
]
instance Arbitrary (Awkward Timeout) where
arbitrary = fmap Awkward $
Timeout <$> awkward <*> awkward
instance Arbitrary (Awkward TimeoutUnit) where
arbitrary = Awkward <$> elements [
Hour
, Minute
, Second
, Millisecond
, Microsecond
, Nanosecond
]
instance Arbitrary (Awkward TimeoutValue) where
arbitrary = fmap Awkward $
TimeoutValue <$> arbitrary `suchThat` isValidTimeoutValue
shrink (Awkward (TimeoutValue x)) =
map (Awkward . TimeoutValue) $ shrink x
instance Arbitrary (Awkward CompressionId) where
arbitrary = Awkward <$> oneof [
pure Identity
, pure GZip
, pure Deflate
, pure Snappy
, Custom <$> awkward `suchThatMap` validCompressionId
]
shrink (Awkward cid) = Awkward <$>
case cid of
Identity -> []
GZip -> [Identity]
Deflate -> [Identity]
Snappy -> [Identity]
Custom x -> concat [
[Identity]
, mapMaybe (fmap Custom . validCompressionId) $ shrink x
]
instance Arbitrary (Awkward ContentType) where
arbitrary = Awkward <$>
oneof [
pure $ ContentTypeDefault
, (\format -> ContentTypeOverride $ defaultRpcContentType format)
<$> awkward `suchThatMap` validFormat
]
shrink (Awkward ct) = Awkward <$>
case ct of
ContentTypeDefault -> []
ContentTypeOverride bs -> concat [
[ContentTypeDefault]
, [ ContentTypeOverride $ defaultRpcContentType format'
| Just format <- [BS.Strict.Char8.stripPrefix ("application/grpc+") bs]
, format' <- mapMaybe (validFormat . getAwkward) $ shrink (Awkward format)
]
]
instance Arbitrary (Awkward MessageType) where
arbitrary = Awkward <$>
oneof [
pure $ MessageTypeDefault
, MessageTypeOverride <$> awkward `suchThat` validMessageType
]
shrink (Awkward mt) = Awkward <$>
case mt of
MessageTypeDefault -> []
MessageTypeOverride x -> concat [
[MessageTypeDefault]
, [ MessageTypeOverride x'
| x' <- shrink x
, validMessageType x'
]
]
instance Arbitrary (Awkward TraceContext) where
arbitrary = Awkward <$> do
traceContextTraceId <- awkward
traceContextSpanId <- awkward
traceContextOptions <- awkward
return TraceContext {
traceContextTraceId
, traceContextSpanId
, traceContextOptions
}
instance Arbitrary (Awkward TraceId) where
arbitrary = Awkward <$> do
tid <- replicateM 16 arbitrary -- length is fixed
return $ TraceId $ BS.Strict.pack tid
instance Arbitrary (Awkward SpanId) where
arbitrary = Awkward <$> do
tid <- replicateM 8 arbitrary -- length is fixed
return $ SpanId $ BS.Strict.pack tid
instance Arbitrary (Awkward TraceOptions) where
arbitrary = Awkward <$> do
traceOptionsSampled <- arbitrary
return TraceOptions {
traceOptionsSampled
}
instance Arbitrary (Awkward GrpcStatus) where
arbitrary = Awkward <$>
elements [
GrpcOk
, GrpcError GrpcCancelled
, GrpcError GrpcUnknown
, GrpcError GrpcInvalidArgument
, GrpcError GrpcDeadlineExceeded
, GrpcError GrpcNotFound
, GrpcError GrpcAlreadyExists
, GrpcError GrpcPermissionDenied
, GrpcError GrpcResourceExhausted
, GrpcError GrpcFailedPrecondition
, GrpcError GrpcAborted
, GrpcError GrpcOutOfRange
, GrpcError GrpcUnimplemented
, GrpcError GrpcInternal
, GrpcError GrpcUnavailable
, GrpcError GrpcDataLoss
, GrpcError GrpcUnauthenticated
]
instance Arbitrary (Awkward Pushback) where
arbitrary = Awkward <$>
oneof [
RetryAfter <$> arbitrary
, pure DoNotRetry
]
instance Arbitrary (Awkward OrcaLoadReport) where
arbitrary = Awkward <$> do
-- @rps@ is a deprecated field, we omit it from the test
cpuUtilization <- awkward
memUtilization <- awkward
requestCost <- awkward
utilization <- awkward
rpsFractional <- awkward
eps <- awkward
namedMetrics <- awkward
applicationUtilization <- awkward
return $
defMessage
& #cpuUtilization .~ cpuUtilization
& #memUtilization .~ memUtilization
& #requestCost .~ requestCost
& #utilization .~ utilization
& #rpsFractional .~ rpsFractional
& #eps .~ eps
& #namedMetrics .~ namedMetrics
& #applicationUtilization .~ applicationUtilization
{-------------------------------------------------------------------------------
Generating valid values
-------------------------------------------------------------------------------}
validCompressionId :: String -> Maybe String
validCompressionId cid =
case filter (not . forbiddenChar) cid of
"" -> Nothing
cid' -> Just cid'
where
forbiddenChar :: Char -> Bool
forbiddenChar c = or [
isSpace c
, c `elem` [',']
]
validFormat :: Strict.ByteString -> Maybe Strict.ByteString
validFormat format =
case BS.Strict.Char8.filter (not . forbiddenChar) format of
"" -> Nothing
"grpc" -> Nothing -- Would be parsed as @ContentTypeDefault@
cid' -> Just cid'
where
forbiddenChar :: Char -> Bool
forbiddenChar c = or [
isSpace c
, c `elem` [';']
]
validMessageType :: Strict.ByteString -> Bool
validMessageType "Void" = False -- Would be parsed as @MessageTypeDefault@
validMessageType _ = True
{-------------------------------------------------------------------------------
Auxiliary
-------------------------------------------------------------------------------}
renderDoc :: Doc AnsiStyle -> String
renderDoc =
Text.Lazy.unpack
. PP.Ansi.renderLazy
. resetTerminal
. PP.layoutPretty PP.defaultLayoutOptions
where
-- Reset the vivid red from tasty
resetTerminal :: PP.SimpleDocStream AnsiStyle -> PP.SimpleDocStream AnsiStyle
resetTerminal = PP.SAnnPush $ PP.Ansi.color PP.Ansi.Black
buildTrailersOnly' :: IsRPC rpc => Proxy rpc -> TrailersOnly -> [HTTP.Header]
buildTrailersOnly' rpc = buildTrailersOnly (Just . chooseContentType rpc)
type instance RequestMetadata (RawRpc "serv" "meth") = NoMetadata
type instance ResponseInitialMetadata (RawRpc "serv" "meth") = NoMetadata
type instance ResponseTrailingMetadata (RawRpc "serv" "meth") = NoMetadata