packages feed

warp-3.4.15: test/EarlyHintsSpec.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}

module EarlyHintsSpec (spec) where

import Test.Hspec

#define HAS_EARLY_HINTS_SUPPORT (MIN_VERSION_http_semantics(0,4,1) && MIN_VERSION_http2(5,4,2))

#if HAS_EARLY_HINTS_SUPPORT
import Control.Exception (bracket)
import Data.ByteString (ByteString)
import Data.IORef
import Network.HPACK (TokenHeaderTable, getFieldValue)
import Network.HPACK.Token (toToken)
import Network.HTTP.Types (Status, methodGet, ok200, status200, status404)
import qualified Network.HTTP2.Client as C
import Network.Socket
import Network.Wai
import Network.Wai.Handler.Warp (Port, testWithApplication)

spec :: Spec
spec = describe "HTTP/2 Early Hints" $
    it "delivers a WAI app's 103 Early Hints to the client before the final response (h2c)" $
        testWithApplication (pure app) $ \port -> do
            hintsRef <- newIORef []
            earlyHintsClient port hintsRef `shouldReturn` Just ok200
            hints <- readIORef hintsRef
            map (getFieldValue (toToken "link") . snd) hints
                `shouldBe` (Just <$> earlyResponses)

-- | The @Link@ header values delivered as Early Hints, in order.
earlyResponses :: [ByteString]
earlyResponses =
    [ "</style.css>; rel=preload; as=style"
    , "</app.js>; rel=preload; as=script"
    ]

-- | A WAI app that emits two Early Hints sections, then the final response.
app :: Application
app req respond
    | pathInfo req == ["early"] = do
        mapM_ (\link -> requestSendEarlyHints req [("link", link)]) earlyResponses
        respond $ responseLBS status200 [("content-type", "text/plain")] "Hello"
    | otherwise = respond $ responseLBS status404 [] ""

-- | Drive Warp over h2c with the HTTP/2 client, recording each 103 Early Hints
--   section via the client's informational handler, and return the final status.
earlyHintsClient :: Port -> IORef [TokenHeaderTable] -> IO (Maybe Status)
earlyHintsClient port hintsRef = withTCP "127.0.0.1" port $ \sock ->
    bracket (C.allocSimpleConfig sock 4096) C.freeSimpleConfig $ \conf ->
        C.run cliconf (conf{C.confOnInformational = onInformational}) $ \sendRequest _aux ->
            sendRequest (C.requestNoBody methodGet "/early" []) (return . C.responseStatus)
  where
    cliconf = C.defaultClientConfig{C.authority = "127.0.0.1"}
    onInformational _streamId tbl = modifyIORef' hintsRef (++ [tbl])

-- | Connect to a TCP server, run an action, and close the socket afterwards.
withTCP :: HostName -> Port -> (Socket -> IO a) -> IO a
withTCP host port = bracket open close
  where
    open = do
        addr : _ <- getAddrInfo (Just defaultHints{addrSocketType = Stream}) (Just host) (Just (show port))
        sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
        connect sock (addrAddress addr)
        return sock
#else
spec :: Spec
spec = describe "HTTP/2 Early Hints" $
    it "delivers a WAI app's 103 Early Hints to the client before the final response (h2c)" $
        pendingWith "requires http2 >= 5.4.2 and http-semantics >= 0.4.1"
#endif