packages feed

grapesy-1.2.0: src/Network/GRPC/Util/ServerStream.hs

module Network.GRPC.Util.ServerStream (
    -- * Server API
    serverOutputStream,
    serverInputStream,
) where

import Network.HTTP.Semantics (OutBodyIface)
import Network.HTTP.Semantics qualified as HTTP
import Network.HTTP.Semantics.Server qualified as Server

import Network.GRPC.Util.HeaderTable (fromHeaderTable)
import Network.GRPC.Util.Imports
import Network.GRPC.Util.Stream

{-------------------------------------------------------------------------------
  Server API
-------------------------------------------------------------------------------}

serverInputStream :: Server.Request -> IO InputStream
serverInputStream req = do
    return InputStream {
        _getChunk =
           wrapClientDisconnected $
             Server.getRequestBodyChunk' req
      , _getTrailers =
           wrapClientDisconnected $
             maybe [] fromHeaderTable <$> Server.getRequestTrailers req
      }

-- | Create output stream
--
-- == Note on the use of Trailers-Only in non-error cases
--
-- If the stream is closed without writing anything, the situation is similar to
-- the gRPC @Trailers-Only@ case, except that we have already sent the initial
-- set of headers. In this case, http2 will (reasonably enough) create an empty
-- DATA frame, and then another HEADERS frame for the trailers. This is conform
-- the gRPC specification, which mandates:
--
-- > Most responses are expected to have both headers and trailers but
-- > Trailers-Only is permitted for calls that produce an immediate error.
--
-- If we compare this to the official Python example @RouteGuide@ server,
-- however, we see that the @Trailers-Only@ case is sometimes also used in
-- non-error cases. An example is @RouteGuide.listFeatures@: when there /are/ no
-- features in the specified rectangle, the server will send no messages back to
-- the client. The example Python server will use the gRPC Trailers-Only case
-- here (and so we must be able to deal with that in our client implementation).
--
-- We do provide this functionality, but only through a specific API (see
-- 'sendTrailersOnly'); when that API is used, we do not make use of this
-- 'OutputStream' abstraction (indeed, we do not stream at all). In streaming
-- cases (the default) we do not make use of @Trailers-Only@.
serverOutputStream :: HasCallStack => OutBodyIface -> IO OutputStream
serverOutputStream iface = do
    -- Make sure that http2 does not wait for the first message before sending
    -- the response headers. This is important: the client might want the
    -- initial response metadata before the first message.
    --
    -- This does require some justification; if any of the reasons below is
    -- no longer true, we might need to reconsider:
    --
    -- o The extra cost of this flush is that we might need an additional TCP
    --   packet; no big deal.
    -- o We only create the 'OutputStream' once the user actually initiates the
    --   response, at which point the headers are fixed.
    -- o We do not use an 'OutputStream' at all when we are in the Trailers-Only
    --   case (see discussion above).

    let outputStream = OutputStream {
            _writeChunk = \c ->
               wrapClientDisconnected $
                 HTTP.outBodyPush iface c
          , _writeChunkFinal = \c ->
               wrapClientDisconnected $
                 HTTP.outBodyPushFinal iface c
          , _flush =
               wrapClientDisconnected $
                 HTTP.outBodyFlush iface
          }

    flush outputStream
    return outputStream