packages feed

spire-http-0.1.0.0: src/Spire/Http/RequestId.hs

-- | Request ID middleware.
--
-- Generates a unique request ID for each request (or preserves an
-- existing one from the @X-Request-Id@ header). The ID is:
--
-- 1. Stored in request 'Extensions' (accessible to handlers)
-- 2. Copied to the response @X-Request-Id@ header
--
-- @
-- svc |> requestIdLayer
-- @
module Spire.Http.RequestId
  ( -- * Request ID type
    RequestId (..)
    -- * Layer
  , requestIdLayer
    -- * Header name
  , requestIdHeader
  ) where

import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BS
import Data.IORef
import Data.List (lookup)
import Network.HTTP.Types (HeaderName)

import Spire (Middleware, middleware)
import Spire.Service (Service (..))
import Http.Core
  ( Request (..), Response (..)
  , Extensions, insertExtension
  )


-- | A request identifier, either propagated from the client or generated.
newtype RequestId = RequestId { unRequestId :: ByteString }
  deriving (Eq, Ord, Show)


-- | The header name used for request IDs.
requestIdHeader :: HeaderName
requestIdHeader = "X-Request-Id"


-- | Simple counter-based ID generator (sufficient for single-process).
-- In production you'd use UUID v4 or similar.
newtype IdGen = IdGen (IORef Int)

newIdGen :: IO IdGen
newIdGen = IdGen <$> newIORef 0

nextId :: IdGen -> IO ByteString
nextId (IdGen ref) = do
  n <- atomicModifyIORef' ref (\n -> (n + 1, n))
  pure $ BS.pack $ "req-" ++ show n


-- | Middleware that assigns and propagates request IDs.
requestIdLayer :: IO (Middleware IO (Request ByteString) (Response ByteString))
requestIdLayer = do
  gen <- newIdGen
  pure $ middleware $ \(Service inner) -> Service $ \req -> do
    -- Use existing header or generate new
    let existing = lookup requestIdHeader (requestHeaders req)
    rid <- case existing of
      Just val -> pure val
      Nothing  -> nextId gen

    -- Store in extensions for handler access
    let reqId = RequestId rid
    insertExtension reqId (requestExtensions req)

    -- Call inner service
    resp <- inner req

    -- Add to response headers
    pure resp
      { responseHeaders = (requestIdHeader, rid) : responseHeaders resp }