@@ -1,5 +1,6 @@
11{-# LANGUAGE OverloadedStrings, RankNTypes #-}
22{-# LANGUAGE CPP #-}
3+{-# LANGUAGE DeriveDataTypeable #-}
34
45-- | An HTTP\/2-aware variant of the 'Network.Wai.Application' type. Compared
56-- to the original, this exposes the new functionality of server push and
@@ -28,6 +29,7 @@ module Network.Wai.HTTP2
2829 HTTP2Application
2930 -- * Responder
3031 , Responder
32+ , RespondFunc
3133 , Body
3234 , BodyOf
3335 , Chunk(..)
@@ -50,11 +52,12 @@ module Network.Wai.HTTP2
5052 , streamSimple
5153 ) where
5254
53−#if __GLASGOW_HASKELL__ < 710
54−import Data.Functor ((<$))
55−#endif
5655import Blaze.ByteString.Builder (Builder)
56+import Control.Exception (Exception, SomeException, throwIO)
5757import Data.ByteString (ByteString)
58+import Data.IORef (newIORef, readIORef, writeIORef)
59+import Data.Monoid (mempty)
60+import Data.Typeable (Typeable)
5861import qualified Network.HTTP.Types as H
5962
6063import Network.Wai (Application)
@@ -97,6 +100,14 @@ type Body a = BodyOf Chunk a
97100-- 'Network.Wai.StreamingBody' is identical to @BodyOf Builder ()@.
98101type BodyOf c a = (c -> IO ()) -> IO () -> IO a
99102
103+-- | Create trailers based on the result of a stream body.
104+type TrailerFunc a = Either SomeException a -> Trailers -- TODO IO?
105+
106+-- | Given to 'Responders'; provide a status, headers, a stream body, and a way
107+-- to produce trailers, and we'll give you a token proving you called the
108+-- 'RespondFunc'.
109+type RespondFunc s a = H.Status -> H.ResponseHeaders -> TrailerFunc a -> Body a -> IO s
110+
100111-- | The result of an 'HTTP2Application'; or, alternately, an application
101112-- that's independent of the request. This is a continuation-passing style
102113-- function that first provides a response by calling the given respond
@@ -105,8 +116,7 @@ type BodyOf c a = (c -> IO ()) -> IO () -> IO a
105116-- The respond function is similar to the one in 'Network.Wai.Application', but
106117-- it only takes a streaming body, the status and headers are curried, and it
107118-- passes on any result value from the stream body.
108−type Responder = (forall a. H.Status -> H.ResponseHeaders -> Body a -> IO a)
109− -> IO Trailers
119+type Responder = forall s. (forall a. RespondFunc s a) -> IO s
110120
111121-- | A function given to an 'HTTP2Application' to initiate a server-pushed
112122-- stream. Its argument is the same as the result of an 'HTTP2Application', so
@@ -140,8 +150,7 @@ streamFilePart path part write _ = write $ FileChunk path part
140150respondFilePart :: H.Status -> H.ResponseHeaders -> FilePath -> FilePart -> Responder
141151respondFilePart s h path part respond = do
142152 let (s', h') = adjustForFilePart s h part
143− respond s' h' $ streamFilePart path part
144− return []
153+ respond s' h' mempty $ streamFilePart path part
145154
146155-- | Serve the requested range of the specified file (based on the Range
147156-- header), using the given 'H.Status' and 'H.ResponseHeaders' as a base. If
@@ -150,11 +159,11 @@ respondFilePart s h path part respond = do
150159-- the status will be replaced with 206 and the Content-Range header will be
151160-- added. The Content-Length header will always be added.
152161respondFile :: H.Status -> H.ResponseHeaders -> FilePath -> H.RequestHeaders -> Responder
153−respondFile s h path req respond = do
162+respondFile s h path reqHdrs respond = do
154163 fileSize <- tryGetFileSize path
155164 case fileSize of
156165 Left _ -> respondNotFound h respond
157− Right size -> respondFileExists s h path size req respond
166+ Right size -> respondFileExists s h path size reqHdrs respond
158167
159168-- As 'respondFile', but with prior knowledge of the file's existence and size.
160169respondFileExists :: H.Status -> H.ResponseHeaders -> FilePath -> Integer -> H.RequestHeaders -> Responder
@@ -163,9 +172,8 @@ respondFileExists s h path size reqHdrs =
163172
164173-- | Respond with a minimal 404 page with the given headers.
165174respondNotFound :: H.ResponseHeaders -> Responder
166−respondNotFound h respond = do
167− respond H.notFound404 h' $ streamBuilder "File not found."
168− return []
175+respondNotFound h respond = respond H.notFound404 h' mempty $
176+ streamBuilder "File not found."
169177 where
170178 contentType = (H.hContentType, "text/plain; charset=utf-8")
171179 h' = contentType:filter ((/=H.hContentType) . fst) h
@@ -190,8 +198,8 @@ streamSimple body write flush = body (write . BuilderChunk) flush
190198-- 'ResponseFile'.
191199responder :: Request -> Response -> Responder
192200responder req response respond = case response of
193− (ResponseBuilder s h b) -> [] <$ respond s h (streamBuilder b)
194− (ResponseStream s h body) -> [] <$ respond s h (streamSimple body)
201+ (ResponseBuilder s h b) -> respond s h mempty (streamBuilder b)
202+ (ResponseStream s h body) -> respond s h mempty (streamSimple body)
195203 (ResponseRaw _ fallback) -> responder req fallback respond
196204 (ResponseFile s h path mpart) -> go respond
197205 where
@@ -202,8 +210,25 @@ responder req response respond = case response of
202210 (respondFilePart s h path)
203211 mpart
204212
213+-- | An 'Network.Wai.Application' we tried to promote neither called its
214+-- respond action nor raised; this is only possible if it imported the
215+-- 'ResponseReceived' and used it to lie about having called the action.
216+data RespondNeverCalled = RespondNeverCalled deriving (Show, Typeable)
217+
218+instance Exception RespondNeverCalled
219+
205220-- | Promote a normal WAI 'Application' to an 'HTTP2Application' by ignoring
206221-- the HTTP/2-specific features.
207222promoteApplication :: Application -> HTTP2Application
208−promoteApplication app req _ respond = [] <$ app req respond'
209− where respond' r = ResponseReceived <$ responder req r respond
223+promoteApplication app req _ respond = do
224+ -- In HTTP2Applications, the Responder is required to ferry a value of
225+ -- arbitrary type from the RespondFunc back to the caller of the
226+ -- application, but in Application the type is fixed to ResponseReceived.
227+ -- To add this extra power to an Application, we have to squirrel it away
228+ -- in an IORef as a hack.
229+ ref <- newIORef Nothing
230+ let respond' r = do
231+ writeIORef ref . Just =<< responder req r respond
232+ return ResponseReceived
233+ ResponseReceived <- app req respond'
234+ readIORef ref >>= maybe (throwIO RespondNeverCalled) return