Skip to content

Commit b1a6d88

Browse files
committed
Revise the Router type to allow proper sharing.
We've previously used functions in the Router type to provide information for subrouters. But this accesses the Requests too early, and breaks sharing of the router structure in general, causing the Router or large parts of the Router to be recomputed on every request. We now do not use functions anymore, and properly compute all static parts of the router first, and gain access to the request only in Delayed. This also turns the code used within Delayed into a proper monad now called DelayedIO, making some of the code using it a bit nicer.
1 parent d4c6f67 commit b1a6d88

File tree

10 files changed

+293
-270
lines changed

10 files changed

+293
-270
lines changed

servant-server/CHANGELOG.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
0.7
22
---
33

4-
* The `Router` type has been changed. There are now more situations where
5-
servers will make use of static lookups to efficiently route the request
6-
to the correct endpoint. Functions `layout` and `layoutWithContext` have
7-
been added to visualize the router layout for debugging purposes. Test
4+
* The `Router` type has been changed. Static router tables should now
5+
be properly shared between requests, drastically increasing the
6+
number of situations where servers will be able to route requests
7+
efficiently. Functions `layout` and `layoutWithContext` have been
8+
added to visualize the router layout for debugging purposes. Test
89
cases for expected router layouts have been added.
910
* Export `throwError` from module `Servant`
1011
* Add `Handler` type synonym

servant-server/src/Servant/Server.hs

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -132,21 +132,13 @@ serve p = serveWithContext p EmptyContext
132132

133133
serveWithContext :: (HasServer layout context)
134134
=> Proxy layout -> Context context -> Server layout -> Application
135-
serveWithContext p context server = toApplication (runRouter (route p context d))
136-
where
137-
d = Delayed r r r r (\ _ _ _ -> Route server)
138-
r = return (Route ())
135+
serveWithContext p context server =
136+
toApplication (runRouter (route p context (emptyDelayed (Route server))))
139137

140138
-- | The function 'layout' produces a textual description of the internal
141139
-- router layout for debugging purposes. Note that the router layout is
142140
-- determined just by the API, not by the handlers.
143141
--
144-
-- This function makes certain assumptions about the well-behavedness of
145-
-- the 'HasServer' instances of the combinators which should be ok for the
146-
-- core servant constructions, but might not be satisfied for some other
147-
-- combinators provided elsewhere. It is possible that the function may
148-
-- crash for these.
149-
--
150142
-- Example:
151143
--
152144
-- For the following API
@@ -168,7 +160,7 @@ serveWithContext p context server = toApplication (runRouter (route p context d)
168160
-- > │ └─ e/
169161
-- > │ └─•
170162
-- > ├─ b/
171-
-- > │ └─ <dyn>/
163+
-- > │ └─ <capture>/
172164
-- > │ ├─•
173165
-- > │ ┆
174166
-- > │ └─•
@@ -185,7 +177,7 @@ serveWithContext p context server = toApplication (runRouter (route p context d)
185177
--
186178
-- [@─•@] Leaves reflect endpoints.
187179
--
188-
-- [@\<dyn\>/@] This is a delayed capture of a path component.
180+
-- [@\<capture\>/@] This is a delayed capture of a path component.
189181
--
190182
-- [@\<raw\>@] This is a part of the API we do not know anything about.
191183
--
@@ -200,10 +192,8 @@ layout p = layoutWithContext p EmptyContext
200192
-- | Variant of 'layout' that takes an additional 'Context'.
201193
layoutWithContext :: (HasServer layout context)
202194
=> Proxy layout -> Context context -> Text
203-
layoutWithContext p context = routerLayout (route p context d)
204-
where
205-
d = Delayed r r r r (\ _ _ _ -> FailFatal err501)
206-
r = return (Route ())
195+
layoutWithContext p context =
196+
routerLayout (route p context (emptyDelayed (FailFatal err501)))
207197

208198
-- Documentation
209199

servant-server/src/Servant/Server/Experimental/Auth.hs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
module Servant.Server.Experimental.Auth where
1414

15+
import Control.Monad.Trans (liftIO)
1516
import Control.Monad.Trans.Except (runExceptT)
1617
import Data.Proxy (Proxy (Proxy))
1718
import Data.Typeable (Typeable)
@@ -24,10 +25,11 @@ import Servant.Server.Internal (HasContextEntry,
2425
HasServer, ServerT,
2526
getContextEntry,
2627
route)
27-
import Servant.Server.Internal.Router (Router' (WithRequest))
28-
import Servant.Server.Internal.RoutingApplication (RouteResult (FailFatal, Route),
29-
addAuthCheck)
30-
import Servant.Server.Internal.ServantErr (ServantErr, Handler)
28+
import Servant.Server.Internal.RoutingApplication (addAuthCheck,
29+
delayedFailFatal,
30+
DelayedIO,
31+
withRequest)
32+
import Servant.Server.Internal.ServantErr (Handler)
3133

3234
-- * General Auth
3335

@@ -57,8 +59,10 @@ instance ( HasServer api context
5759
type ServerT (AuthProtect tag :> api) m =
5860
AuthServerData (AuthProtect tag) -> ServerT api m
5961

60-
route Proxy context subserver = WithRequest $ \ request ->
61-
route (Proxy :: Proxy api) context (subserver `addAuthCheck` authCheck request)
62+
route Proxy context subserver =
63+
route (Proxy :: Proxy api) context (subserver `addAuthCheck` withRequest authCheck)
6264
where
65+
authHandler :: Request -> Handler (AuthServerData (AuthProtect tag))
6366
authHandler = unAuthHandler (getContextEntry context)
64-
authCheck = fmap (either FailFatal Route) . runExceptT . authHandler
67+
authCheck :: Request -> DelayedIO (AuthServerData (AuthProtect tag))
68+
authCheck = (>>= either delayedFailFatal return) . liftIO . runExceptT . authHandler

servant-server/src/Servant/Server/Internal.hs

Lines changed: 58 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ module Servant.Server.Internal
2222
, module Servant.Server.Internal.ServantErr
2323
) where
2424

25+
import Control.Monad.Trans (liftIO)
2526
import qualified Data.ByteString as B
2627
import qualified Data.ByteString.Char8 as BC8
2728
import qualified Data.ByteString.Lazy as BL
@@ -70,7 +71,11 @@ import Servant.Server.Internal.ServantErr
7071
class HasServer layout context where
7172
type ServerT layout (m :: * -> *) :: *
7273

73-
route :: Proxy layout -> Context context -> Delayed (Server layout) -> Router
74+
route ::
75+
Proxy layout
76+
-> Context context
77+
-> Delayed env (Server layout)
78+
-> Router env
7479

7580
type Server layout = ServerT layout Handler
7681

@@ -92,7 +97,7 @@ instance (HasServer a context, HasServer b context) => HasServer (a :<|> b) cont
9297
type ServerT (a :<|> b) m = ServerT a m :<|> ServerT b m
9398

9499
route Proxy context server = choice (route pa context ((\ (a :<|> _) -> a) <$> server))
95-
(route pb context ((\ (_ :<|> b) -> b) <$> server))
100+
(route pb context ((\ (_ :<|> b) -> b) <$> server))
96101
where pa = Proxy :: Proxy a
97102
pb = Proxy :: Proxy b
98103

@@ -120,12 +125,12 @@ instance (KnownSymbol capture, FromHttpApiData a, HasServer sublayout context)
120125
a -> ServerT sublayout m
121126

122127
route Proxy context d =
123-
DynamicRouter $ \ first ->
128+
CaptureRouter $
124129
route (Proxy :: Proxy sublayout)
125130
context
126-
(addCapture d $ case parseUrlPieceMaybe first :: Maybe a of
127-
Nothing -> return $ Fail err400
128-
Just v -> return $ Route v
131+
(addCapture d $ \ txt -> case parseUrlPieceMaybe txt :: Maybe a of
132+
Nothing -> delayedFail err400
133+
Just v -> return v
129134
)
130135

131136
allowedMethodHead :: Method -> Request -> Bool
@@ -144,41 +149,41 @@ processMethodRouter handleA status method headers request = case handleA of
144149
bdy = if allowedMethodHead method request then "" else body
145150
hdrs = (hContentType, cs contentT) : (fromMaybe [] headers)
146151

147-
methodCheck :: Method -> Request -> IO (RouteResult ())
152+
methodCheck :: Method -> Request -> DelayedIO ()
148153
methodCheck method request
149-
| allowedMethod method request = return $ Route ()
150-
| otherwise = return $ Fail err405
154+
| allowedMethod method request = return ()
155+
| otherwise = delayedFail err405
151156

152-
acceptCheck :: (AllMime list) => Proxy list -> B.ByteString -> IO (RouteResult ())
157+
acceptCheck :: (AllMime list) => Proxy list -> B.ByteString -> DelayedIO ()
153158
acceptCheck proxy accH
154-
| canHandleAcceptH proxy (AcceptHeader accH) = return $ Route ()
155-
| otherwise = return $ FailFatal err406
159+
| canHandleAcceptH proxy (AcceptHeader accH) = return ()
160+
| otherwise = delayedFailFatal err406
156161

157162
methodRouter :: (AllCTRender ctypes a)
158163
=> Method -> Proxy ctypes -> Status
159-
-> Delayed (Handler a)
160-
-> Router
164+
-> Delayed env (Handler a)
165+
-> Router env
161166
methodRouter method proxy status action = leafRouter route'
162167
where
163-
route' request respond =
168+
route' env request respond =
164169
let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
165170
in runAction (action `addMethodCheck` methodCheck method request
166171
`addAcceptCheck` acceptCheck proxy accH
167-
) respond $ \ output -> do
172+
) env request respond $ \ output -> do
168173
let handleA = handleAcceptH proxy (AcceptHeader accH) output
169174
processMethodRouter handleA status method Nothing request
170175

171176
methodRouterHeaders :: (GetHeaders (Headers h v), AllCTRender ctypes v)
172177
=> Method -> Proxy ctypes -> Status
173-
-> Delayed (Handler (Headers h v))
174-
-> Router
178+
-> Delayed env (Handler (Headers h v))
179+
-> Router env
175180
methodRouterHeaders method proxy status action = leafRouter route'
176181
where
177-
route' request respond =
182+
route' env request respond =
178183
let accH = fromMaybe ct_wildcard $ lookup hAccept $ requestHeaders request
179184
in runAction (action `addMethodCheck` methodCheck method request
180185
`addAcceptCheck` acceptCheck proxy accH
181-
) respond $ \ output -> do
186+
) env request respond $ \ output -> do
182187
let headers = getHeaders output
183188
handleA = handleAcceptH proxy (AcceptHeader accH) (getResponse output)
184189
processMethodRouter handleA status method (Just headers) request
@@ -230,8 +235,8 @@ instance (KnownSymbol sym, FromHttpApiData a, HasServer sublayout context)
230235
type ServerT (Header sym a :> sublayout) m =
231236
Maybe a -> ServerT sublayout m
232237

233-
route Proxy context subserver = WithRequest $ \ request ->
234-
let mheader = parseHeaderMaybe =<< lookup str (requestHeaders request)
238+
route Proxy context subserver =
239+
let mheader req = parseHeaderMaybe =<< lookup str (requestHeaders req)
235240
in route (Proxy :: Proxy sublayout) context (passToServer subserver mheader)
236241
where str = fromString $ symbolVal (Proxy :: Proxy sym)
237242

@@ -262,10 +267,10 @@ instance (KnownSymbol sym, FromHttpApiData a, HasServer sublayout context)
262267
type ServerT (QueryParam sym a :> sublayout) m =
263268
Maybe a -> ServerT sublayout m
264269

265-
route Proxy context subserver = WithRequest $ \ request ->
266-
let querytext = parseQueryText $ rawQueryString request
267-
param =
268-
case lookup paramname querytext of
270+
route Proxy context subserver =
271+
let querytext r = parseQueryText $ rawQueryString r
272+
param r =
273+
case lookup paramname (querytext r) of
269274
Nothing -> Nothing -- param absent from the query string
270275
Just Nothing -> Nothing -- param present with no value -> Nothing
271276
Just (Just v) -> parseQueryParamMaybe v -- if present, we try to convert to
@@ -298,13 +303,13 @@ instance (KnownSymbol sym, FromHttpApiData a, HasServer sublayout context)
298303
type ServerT (QueryParams sym a :> sublayout) m =
299304
[a] -> ServerT sublayout m
300305

301-
route Proxy context subserver = WithRequest $ \ request ->
302-
let querytext = parseQueryText $ rawQueryString request
306+
route Proxy context subserver =
307+
let querytext r = parseQueryText $ rawQueryString r
303308
-- if sym is "foo", we look for query string parameters
304309
-- named "foo" or "foo[]" and call parseQueryParam on the
305310
-- corresponding values
306-
parameters = filter looksLikeParam querytext
307-
values = mapMaybe (convert . snd) parameters
311+
parameters r = filter looksLikeParam (querytext r)
312+
values r = mapMaybe (convert . snd) (parameters r)
308313
in route (Proxy :: Proxy sublayout) context (passToServer subserver values)
309314
where paramname = cs $ symbolVal (Proxy :: Proxy sym)
310315
looksLikeParam (name, _) = name == paramname || name == (paramname <> "[]")
@@ -329,9 +334,9 @@ instance (KnownSymbol sym, HasServer sublayout context)
329334
type ServerT (QueryFlag sym :> sublayout) m =
330335
Bool -> ServerT sublayout m
331336

332-
route Proxy context subserver = WithRequest $ \ request ->
333-
let querytext = parseQueryText $ rawQueryString request
334-
param = case lookup paramname querytext of
337+
route Proxy context subserver =
338+
let querytext r = parseQueryText $ rawQueryString r
339+
param r = case lookup paramname (querytext r) of
335340
Just Nothing -> True -- param is there, with no value
336341
Just (Just v) -> examine v -- param with a value
337342
Nothing -> False -- param not in the query string
@@ -352,8 +357,8 @@ instance HasServer Raw context where
352357

353358
type ServerT Raw m = Application
354359

355-
route Proxy _ rawApplication = RawRouter $ \ request respond -> do
356-
r <- runDelayed rawApplication
360+
route Proxy _ rawApplication = RawRouter $ \ env request respond -> do
361+
r <- runDelayed rawApplication env request
357362
case r of
358363
Route app -> app request (respond . Route)
359364
Fail a -> respond $ Fail a
@@ -386,22 +391,22 @@ instance ( AllCTUnrender list a, HasServer sublayout context
386391
type ServerT (ReqBody list a :> sublayout) m =
387392
a -> ServerT sublayout m
388393

389-
route Proxy context subserver = WithRequest $ \ request ->
390-
route (Proxy :: Proxy sublayout) context (addBodyCheck subserver (bodyCheck request))
394+
route Proxy context subserver =
395+
route (Proxy :: Proxy sublayout) context (addBodyCheck subserver bodyCheck)
391396
where
392-
bodyCheck request = do
397+
bodyCheck = withRequest $ \ request -> do
393398
-- See HTTP RFC 2616, section 7.2.1
394399
-- http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
395400
-- See also "W3C Internet Media Type registration, consistency of use"
396401
-- http://www.w3.org/2001/tag/2002/0129-mime
397402
let contentTypeH = fromMaybe "application/octet-stream"
398403
$ lookup hContentType $ requestHeaders request
399404
mrqbody <- handleCTypeH (Proxy :: Proxy list) (cs contentTypeH)
400-
<$> lazyRequestBody request
405+
<$> liftIO (lazyRequestBody request)
401406
case mrqbody of
402-
Nothing -> return $ FailFatal err415
403-
Just (Left e) -> return $ FailFatal err400 { errBody = cs e }
404-
Just (Right v) -> return $ Route v
407+
Nothing -> delayedFailFatal err415
408+
Just (Left e) -> delayedFailFatal err400 { errBody = cs e }
409+
Just (Right v) -> return v
405410

406411
-- | Make sure the incoming request starts with @"/path"@, strip it and
407412
-- pass the rest of the request path to @sublayout@.
@@ -418,28 +423,28 @@ instance (KnownSymbol path, HasServer sublayout context) => HasServer (path :> s
418423
instance HasServer api context => HasServer (RemoteHost :> api) context where
419424
type ServerT (RemoteHost :> api) m = SockAddr -> ServerT api m
420425

421-
route Proxy context subserver = WithRequest $ \req ->
422-
route (Proxy :: Proxy api) context (passToServer subserver $ remoteHost req)
426+
route Proxy context subserver =
427+
route (Proxy :: Proxy api) context (passToServer subserver remoteHost)
423428

424429
instance HasServer api context => HasServer (IsSecure :> api) context where
425430
type ServerT (IsSecure :> api) m = IsSecure -> ServerT api m
426431

427-
route Proxy context subserver = WithRequest $ \req ->
428-
route (Proxy :: Proxy api) context (passToServer subserver $ secure req)
432+
route Proxy context subserver =
433+
route (Proxy :: Proxy api) context (passToServer subserver secure)
429434

430435
where secure req = if isSecure req then Secure else NotSecure
431436

432437
instance HasServer api context => HasServer (Vault :> api) context where
433438
type ServerT (Vault :> api) m = Vault -> ServerT api m
434439

435-
route Proxy context subserver = WithRequest $ \req ->
436-
route (Proxy :: Proxy api) context (passToServer subserver $ vault req)
440+
route Proxy context subserver =
441+
route (Proxy :: Proxy api) context (passToServer subserver vault)
437442

438443
instance HasServer api context => HasServer (HttpVersion :> api) context where
439444
type ServerT (HttpVersion :> api) m = HttpVersion -> ServerT api m
440445

441-
route Proxy context subserver = WithRequest $ \req ->
442-
route (Proxy :: Proxy api) context (passToServer subserver $ httpVersion req)
446+
route Proxy context subserver =
447+
route (Proxy :: Proxy api) context (passToServer subserver httpVersion)
443448

444449
-- | Basic Authentication
445450
instance ( KnownSymbol realm
@@ -450,12 +455,12 @@ instance ( KnownSymbol realm
450455

451456
type ServerT (BasicAuth realm usr :> api) m = usr -> ServerT api m
452457

453-
route Proxy context subserver = WithRequest $ \ request ->
454-
route (Proxy :: Proxy api) context (subserver `addAuthCheck` authCheck request)
458+
route Proxy context subserver =
459+
route (Proxy :: Proxy api) context (subserver `addAuthCheck` authCheck)
455460
where
456461
realm = BC8.pack $ symbolVal (Proxy :: Proxy realm)
457462
basicAuthContext = getContextEntry context
458-
authCheck req = runBasicAuth req realm basicAuthContext
463+
authCheck = withRequest $ \ req -> runBasicAuth req realm basicAuthContext
459464

460465
-- * helpers
461466

servant-server/src/Servant/Server/Internal/BasicAuth.hs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
module Servant.Server.Internal.BasicAuth where
77

88
import Control.Monad (guard)
9+
import Control.Monad.Trans (liftIO)
910
import qualified Data.ByteString as BS
1011
import Data.ByteString.Base64 (decodeLenient)
1112
import Data.Monoid ((<>))
@@ -57,13 +58,13 @@ decodeBAHdr req = do
5758

5859
-- | Run and check basic authentication, returning the appropriate http error per
5960
-- the spec.
60-
runBasicAuth :: Request -> BS.ByteString -> BasicAuthCheck usr -> IO (RouteResult usr)
61+
runBasicAuth :: Request -> BS.ByteString -> BasicAuthCheck usr -> DelayedIO usr
6162
runBasicAuth req realm (BasicAuthCheck ba) =
6263
case decodeBAHdr req of
6364
Nothing -> plzAuthenticate
64-
Just e -> ba e >>= \res -> case res of
65+
Just e -> liftIO (ba e) >>= \res -> case res of
6566
BadPassword -> plzAuthenticate
6667
NoSuchUser -> plzAuthenticate
67-
Unauthorized -> return $ FailFatal err403
68-
Authorized usr -> return $ Route usr
69-
where plzAuthenticate = return $ FailFatal err401 { errHeaders = [mkBAChallengerHdr realm] }
68+
Unauthorized -> delayedFailFatal err403
69+
Authorized usr -> return usr
70+
where plzAuthenticate = delayedFailFatal err401 { errHeaders = [mkBAChallengerHdr realm] }

0 commit comments

Comments
 (0)