thrift-http-0.1.0.1: Thrift/Server/HTTP.hs
{-
Copyright (c) Meta Platforms, Inc. and affiliates.
All rights reserved.
This source code is licensed under the BSD-style license found in the
LICENSE file in the root directory of this source tree.
-}
-- | Support for creating a Thrift-over-HTTP server
{-# LANGUAGE TypeApplications #-}
module Thrift.Server.HTTP (
ServerOptions(..),
Server(..),
defaultOptions,
withBackgroundServer,
withBackgroundServer',
thriftApplication,
) where
import Data.ByteString (ByteString)
import Control.Concurrent
import Control.Concurrent.Async
import Control.Exception
import qualified Data.ByteString.Lazy as LBS
import Data.Proxy
import Data.String
import Data.Streaming.Network (bindPortTCP, bindRandomPortTCP)
import Network.HTTP.Types
import Network.Socket (close)
import Network.Wai.Handler.Warp
import Network.Wai
import Thrift.Processor hiding (Header)
import qualified Thrift.Processor as Thrift
import Thrift.Protocol
import Thrift.Protocol.Binary
import Thrift.Protocol.Compact
import Thrift.Protocol.JSON
-- TODO:
-- - one-way requests are currently treated as 2-way. Can we do any
-- better?
-- | Options for creating a Thrift-over-HTTP service.
data ServerOptions = ServerOptions
{ desiredPort :: Maybe Int
-- ^ If 'Nothing', creates the server on a random free port,
-- passing the actual port number as 'serverPort'.
, numWorkerThreads :: Maybe Int
-- ^ Currently ignored, provided for compatibility with CppServer
, warpSettings :: Settings
}
-- | Default options for creating a Thrift-over-HTTP service.
defaultOptions :: ServerOptions
defaultOptions = ServerOptions
{ desiredPort = Nothing
, numWorkerThreads = Nothing
, warpSettings = setHost (fromString "!6") defaultSettings
-- IPv6 only by default
}
-- | A running HTTP server.
data Server = Server
{ serverPort :: Int
-- The actual port number, which might be useful if
-- 'desiredPort' was 'Nothing'.
, serverAsync :: Async ()
-- The 'Async' running the HTTP server
}
-- | Create an HTTP server for a Thrift service from the given 'ServerOptions'.
-- This is a simple wrapper around Warp's 'runSettings' that optionally creates
-- the server on a random port, and also wait for the server to start before
-- invoking the given action. Shuts down the server when the action returns.
withBackgroundServer
:: forall s a . (Processor s)
=> (forall r . s r -> IO r) -- ^ handler to use
-> ServerOptions
-> (Server -> IO a) -- ^ action to run while the server is up
-> IO a
withBackgroundServer handler = withBackgroundServer' handler (\_ _ -> [])
-- | Create an HTTP server for a Thrift service from the given 'ServerOptions'.
-- This is a simple wrapper around Warp's 'runSettings' that optionally creates
-- the server on a random port, and also wait for the server to start before
-- invoking the given action. Shuts down the server when the action returns.
withBackgroundServer'
:: forall s a . (Processor s)
=> (forall r . s r -> IO r) -- ^ handler to use
-> (forall r . s r -> Either SomeException r -> Thrift.Header)
-> ServerOptions
-> (Server -> IO a) -- ^ action to run while the server is up
-> IO a
withBackgroundServer' handler postProcess ServerOptions{..} action = do
ready <- newEmptyMVar
let
host = getHost warpSettings
application = thriftApplication handler postProcess
settings =
maybe id setPort desiredPort $
setBeforeMainLoop (putMVar ready ())
warpSettings
go port sock =
withAsync (runSettingsSocket settings sock application) $ \a -> do
takeMVar ready
action (Server port a)
case desiredPort of
Nothing ->
bracket (bindRandomPortTCP host) (close . snd) $ \(port,sock) ->
go port sock
Just port ->
bracket (bindPortTCP port host) close (go port)
-- | Make a WAI 'Application' for a Thrift service. Use this with a
-- transport layer such as Warp to make a complete server, or call
-- 'withBackgroundServer' to do it all.
thriftApplication
:: forall s . (Processor s)
=> (forall r . s r -> IO r) -- ^ handler to use
-> (forall r . s r -> Either SomeException r -> Thrift.Header)
-> Application
thriftApplication handler postProcess req respond = do
body <- strictRequestBody req
withProto (requestHeaders req) $ \proto contentType -> do
(res, _maybeEx, _headers) <-
process proto 0 handler postProcess (LBS.toStrict body)
respond $ responseLBS
status200
[(hContentType, contentType)]
(LBS.fromStrict res)
where
withProto
:: [Header]
-> (forall p . Protocol p => Proxy p -> ByteString -> IO b)
-> IO b
withProto hdrs f =
case [ t | (header,t) <- hdrs, header == hContentType ] of
t@"application/x-thrift-binary" : _ -> f (Proxy @Binary) t
t@"application/x-thrift-compact" : _ -> f (Proxy @Compact) t
t@"application/x-thrift-json" : _ -> f (Proxy @JSON) t
_ -> f (Proxy @Binary) "application/x-thrift-binary"