on master help sign in
Resolve todos
commit58e931f48a9dba161c146e40cf6fd57f908306a2 authorOleg Grenrus <oleg.grenrus@iki.fi> dateMon 16 Jan 2017, 14:17 (10 years ago) parenta61551b
5 files changed+108−74
servant-server/src/Servant/Server/Internal.hs+38−33 servant-server/src/Servant/Server/Internal/RoutingApplication.hs+34−23 servant-server/test/Servant/Server/ErrorSpec.hs+14−4 servant-server/test/Servant/Server/Internal/RoutingApplicationSpec.hs+9−6 servant/src/Servant/API/ContentTypes.hs+13−8
servant-server/src/Servant/Server/Internal.hs+38−33
@@ -28,10 +28,11 @@ import Control.Monad.Trans.Resource (runResourceT)
2828import qualified Data.ByteString as B
2929import qualified Data.ByteString.Char8 as BC8
3030import qualified Data.ByteString.Lazy as BL
31+import Data.Maybe (fromMaybe, mapMaybe)
3132import Data.Either (partitionEithers)
32−import Data.Maybe (fromMaybe)
3333import Data.String (fromString)
3434import Data.String.Conversions (cs, (<>))
35+import qualified Data.Text as T
3536import Data.Typeable
3637import GHC.TypeLits (KnownNat, KnownSymbol, natVal,
3738 symbolVal)
@@ -319,10 +320,9 @@ instance (KnownSymbol sym, FromHttpApiData a, HasServer api context)
319320 Just Nothing -> return Nothing -- param present with no value -> Nothing
320321 Just (Just v) ->
321322 case parseQueryParam v of
322− -- TODO: This should set an error description (including
323− -- paramname)
324− Left _e -> delayedFailFatal err400 -- parsing the request
325− -- paramter failed
323+ Left e -> delayedFailFatal err400
324+ { errBody = cs $ "Error parsing query parameter " <> paramname <> " failed: " <> e
325+ }
326326
327327 Right param -> return $ Just param
328328 delayed = addParameterCheck subserver . withRequest $ \req ->
@@ -356,26 +356,25 @@ instance (KnownSymbol sym, FromHttpApiData a, HasServer api context)
356356 type ServerT (QueryParams sym a :> api) m =
357357 [a] -> ServerT api m
358358
359− route Proxy context subserver =
360− let querytext r = parseQueryText $ rawQueryString r
361− -- if sym is "foo", we look for query string parameters
362− -- named "foo" or "foo[]" and call parseQueryParam on the
363− -- corresponding values
364− parameters r = filter looksLikeParam (querytext r)
365− parseParam (paramName, paramTxt) =
366− case parseQueryParam (fromMaybe "" paramTxt) of
367− Left _e -> Left paramName -- On error, remember name of parameter
368− Right paramVal -> Right paramVal
369− parseParams req =
370− case partitionEithers $ parseParam <$> parameters req of
371− ([], params) -> return params -- No errors
372− -- TODO: This should set an error description
373− (_errors, _) -> delayedFailFatal err400
374− delayed = addParameterCheck subserver . withRequest $ \req ->
375− parseParams req
376− in route (Proxy :: Proxy api) context delayed
377− where paramname = cs $ symbolVal (Proxy :: Proxy sym)
378− looksLikeParam (name, _) = name == paramname || name == (paramname <> "[]")
359+ route Proxy context subserver = route (Proxy :: Proxy api) context $
360+ subserver `addParameterCheck` withRequest paramsCheck
361+ where
362+ paramname = cs $ symbolVal (Proxy :: Proxy sym)
363+ paramsCheck req =
364+ case partitionEithers $ fmap parseQueryParam params of
365+ ([], parsed) -> return parsed
366+ (errs, _) -> delayedFailFatal err400
367+ { errBody = cs $ "Error parsing query parameter(s) " <> paramname <> " failed: " <> T.intercalate ", " errs
368+ }
369+ where
370+ params :: [T.Text]
371+ params = mapMaybe snd
372+ . filter (looksLikeParam . fst)
373+ . parseQueryText
374+ . rawQueryString
375+ $ req
376+
377+ looksLikeParam name = name == paramname || name == (paramname <> "[]")
379378
380379-- | If you use @'QueryFlag' "published"@ in one of the endpoints for your API,
381380-- this automatically requires your server-side handler to be a function
@@ -457,22 +456,28 @@ instance ( AllCTUnrender list a, HasServer api context
457456 type ServerT (ReqBody list a :> api) m =
458457 a -> ServerT api m
459458
460− route Proxy context subserver =
461− route (Proxy :: Proxy api) context (addBodyCheck subserver bodyCheck)
459+ route Proxy context subserver
460+ = route (Proxy :: Proxy api) context $
461+ addBodyCheck subserver ctCheck bodyCheck
462462 where
463− bodyCheck = withRequest $ \ request -> do
463+ -- Content-Type check, we only lookup we can try to parse the request body
464+ ctCheck = withRequest $ \ request -> do
464465 -- See HTTP RFC 2616, section 7.2.1
465466 -- http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
466467 -- See also "W3C Internet Media Type registration, consistency of use"
467468 -- http://www.w3.org/2001/tag/2002/0129-mime
468469 let contentTypeH = fromMaybe "application/octet-stream"
469470 $ lookup hContentType $ requestHeaders request
470− mrqbody <- handleCTypeH (Proxy :: Proxy list) (cs contentTypeH)
471− <$> liftIO (lazyRequestBody request)
471+ case canHandleCTypeH (Proxy :: Proxy list) (cs contentTypeH) :: Maybe (BL.ByteString -> Either String a) of
472+ Nothing -> delayedFailFatal err415
473+ Just f -> return f
474+
475+ -- Body check, we get a body parsing functions as the first argument.
476+ bodyCheck f = withRequest $ \ request -> do
477+ mrqbody <- f <$> liftIO (lazyRequestBody request)
472478 case mrqbody of
473− Nothing -> delayedFailFatal err415
474− Just (Left e) -> delayedFailFatal err400 { errBody = cs e }
475− Just (Right v) -> return v
479+ Left e -> delayedFailFatal err400 { errBody = cs e }
480+ Right v -> return v
476481
477482-- | Make sure the incoming request starts with @"/path"@, strip it and
478483-- pass the rest of the request path to @api@.
servant-server/src/Servant/Server/Internal/RoutingApplication.hs+34−23
@@ -132,14 +132,14 @@ toApplication ra request respond = ra request routingRespond
132132-- 405 (bad method)
133133-- 401 (unauthorized)
134134-- 415 (unsupported media type)
135−-- 400 (bad request)
136135-- 406 (not acceptable)
136+-- 400 (bad request)
137137-- @
138138--
139139-- Therefore, while routing, we delay most checks so that they
140140-- will ultimately occur in the right order.
141141--
142−-- A 'Delayed' contains four delayed blocks of tests, and
142+-- A 'Delayed' contains many delayed blocks of tests, and
143143-- the actual handler:
144144--
145145-- 1. Delayed captures. These can actually cause 404, and
@@ -148,24 +148,28 @@ toApplication ra request respond = ra request routingRespond
148148-- check order from the error reporting, see above). Delayed
149149-- captures can provide inputs to the actual handler.
150150--
151−-- 2. Query parameter checks. They require parsing and can cause 400 if the
152−-- parsing fails. Query parameter checks provide inputs to the handler
153−--
154−-- 3. Method check(s). This can cause a 405. On success,
151+-- 2. Method check(s). This can cause a 405. On success,
155152-- it does not provide an input for the handler. Method checks
156153-- are comparatively cheap.
157154--
158−-- 4. Body and accept header checks. The request body check can
159−-- cause both 400 and 415. This provides an input to the handler.
160−-- The accept header check can be performed as the final
161−-- computation in this block. It can cause a 406.
155+-- 3. Authentication checks. This can cause 401.
156+--
157+-- 4. Accept and content type header checks. These checks
158+-- can cause 415 and 406 errors.
159+--
160+-- 5. Query parameter checks. They require parsing and can cause 400 if the
161+-- parsing fails. Query parameter checks provide inputs to the handler
162+--
163+-- 6. Body check. The request body check can cause 400.
162164--
163165data Delayed env c where
164166 Delayed :: { capturesD :: env -> DelayedIO captures
165− , paramsD :: DelayedIO params
166167 , methodD :: DelayedIO ()
167168 , authD :: DelayedIO auth
168− , bodyD :: DelayedIO body
169+ , acceptD :: DelayedIO ()
170+ , contentD :: DelayedIO contentType
171+ , paramsD :: DelayedIO params
172+ , bodyD :: contentType -> DelayedIO body
169173 , serverD :: captures
170174 -> params
171175 -> auth
@@ -209,7 +213,7 @@ runDelayedIO m req = transResourceT runRouteResultT $ runReaderT (runDelayedIO'
209213-- | A 'Delayed' without any stored checks.
210214emptyDelayed :: RouteResult a -> Delayed env a
211215emptyDelayed result =
212− Delayed (const r) r r r r (\ _ _ _ _ _ -> result)
216+ Delayed (const r) r r r r r (const r) (\ _ _ _ _ _ -> result)
213217 where
214218 r = return ()
215219
@@ -270,20 +274,25 @@ addAuthCheck Delayed{..} new =
270274 , ..
271275 } -- Note [Existential Record Update]
272276
273−-- | Add a body check to the end of the body block.
277+-- | Add a content type and body checks around parameter checks.
278+--
279+-- We'll report failed content type check (415), before trying to parse
280+-- query parameters (400). Which, in turn, happens before request body parsing.
274281addBodyCheck :: Delayed env (a -> b)
275− -> DelayedIO a
282+ -> DelayedIO c -- ^ content type check
283+ -> (c -> DelayedIO a) -- ^ body check
276284 -> Delayed env b
277−addBodyCheck Delayed{..} new =
285+addBodyCheck Delayed{..} newContentD newBodyD =
278286 Delayed
279− { bodyD = (,) <$> bodyD <*> new
287+ { contentD = (,) <$> contentD <*> newContentD
288+ , bodyD = \(content, c) -> (,) <$> bodyD content <*> newBodyD c
280289 , serverD = \ c p a (z, v) req -> ($ v) <$> serverD c p a z req
281290 , ..
282291 } -- Note [Existential Record Update]
283292
284293
285−-- | Add an accept header check to the beginning of the body
286−-- block. There is a tradeoff here. In principle, we'd like
294+-- | Add an accept header check before handling parameters.
295+-- In principle, we'd like
287296-- to take a bad body (400) response take precedence over a
288297-- failed accept check (406). BUT to allow streaming the body,
289298-- we cannot run the body check and then still backtrack.
@@ -297,7 +306,7 @@ addAcceptCheck :: Delayed env a
297306 -> Delayed env a
298307addAcceptCheck Delayed{..} new =
299308 Delayed
300− { bodyD = new *> bodyD
309+ { acceptD = acceptD *> new
301310 , ..
302311 } -- Note [Existential Record Update]
303312
@@ -322,12 +331,14 @@ runDelayed :: Delayed env a
322331 -> Request
323332 -> ResourceT IO (RouteResult a)
324333runDelayed Delayed{..} env = runDelayedIO $ do
334+ r <- ask
325335 c <- capturesD env
326336 methodD
327337 a <- authD
328− b <- bodyD
329− r <- ask
330− p <- paramsD -- Has to be after body to respect the relative error order
338+ acceptD
339+ content <- contentD
340+ p <- paramsD -- Has to be before body parsing, but after content-type checks
341+ b <- bodyD content
331342 liftRouteResult (serverD c p a b r)
332343
333344-- | Runs a delayed server and the resulting action.
servant-server/test/Servant/Server/ErrorSpec.hs+14−4
@@ -6,6 +6,7 @@
66{-# OPTIONS_GHC -fno-warn-orphans #-}
77module Servant.Server.ErrorSpec (spec) where
88
9+import Control.Monad (when)
910import Data.Aeson (encode)
1011import qualified Data.ByteString.Char8 as BC
1112import qualified Data.ByteString.Lazy.Char8 as BCL
@@ -114,10 +115,19 @@ errorOrderSpec =
114115 `shouldRespondWith` 415
115116
116117 it "has 400 as its sixth highest priority error" $ do
117− request goodMethod badParams [goodAuth, goodContentType, goodAccept] goodBody
118− `shouldRespondWith` 400
119− request goodMethod goodUrl [goodAuth, goodContentType, goodAccept] badBody
120− `shouldRespondWith` 400
118+ badParamsRes <- request goodMethod badParams [goodAuth, goodContentType, goodAccept] goodBody
119+ badBodyRes <- request goodMethod goodUrl [goodAuth, goodContentType, goodAccept] badBody
120+
121+ -- Both bad body and bad params result in 400
122+ return badParamsRes `shouldRespondWith` 400
123+ return badBodyRes `shouldRespondWith` 400
124+
125+ -- Param check should occur before body checks
126+ both <- request goodMethod badParams [goodAuth, goodContentType, goodAccept ] badBody
127+ when (both /= badParamsRes) $ liftIO $
128+ expectationFailure $ "badParams + badBody /= badParams: " ++ show both ++ ", " ++ show badParamsRes
129+ when (both == badBodyRes) $ liftIO $
130+ expectationFailure $ "badParams + badBody == badBody: " ++ show both
121131
122132 it "has handler-level errors as last priority" $ do
123133 request goodMethod goodUrl [goodAuth, goodContentType, goodAccept] goodBody
servant-server/test/Servant/Server/Internal/RoutingApplicationSpec.hs+9−6
@@ -19,6 +19,7 @@ import Data.Proxy
1919import GHC.TypeLits (Symbol, KnownSymbol, symbolVal)
2020import Servant
2121import Servant.Server.Internal.RoutingApplication
22+import Network.Wai (defaultRequest)
2223import Test.Hspec
2324import Test.Hspec.Wai (request, shouldRespondWith, with)
2425
@@ -56,12 +57,14 @@ freeTestResource = modifyIORef delayedTestRef $ \r -> case r of
5657
5758delayed :: DelayedIO () -> RouteResult (Handler ()) -> Delayed () (Handler ())
5859delayed body srv = Delayed
59− { capturesD = \_ -> return ()
60+ { capturesD = \() -> return ()
6061 , methodD = return ()
6162 , authD = return ()
63+ , acceptD = return ()
64+ , contentD = return ()
6265 , paramsD = return ()
63− , bodyD = do
64− liftIO (writeTestResource"hia" >> putStrLn "garbage created")
66+ , bodyD = \() -> do
67+ liftIO (writeTestResource "hia" >> putStrLn "garbage created")
6568 _ <- register (freeTestResource >> putStrLn "garbage collected")
6669 body
6770 , serverD = \() () () _body _req -> srv
@@ -70,7 +73,7 @@ delayed body srv = Delayed
7073simpleRun :: Delayed () (Handler ())
7174 -> IO ()
7275simpleRun d = fmap (either ignoreE id) . try $
73− runAction d () undefined (\_ -> return ()) (\_ -> FailFatal err500)
76+ runAction d () defaultRequest (\_ -> return ()) (\_ -> FailFatal err500)
7477
7578 where ignoreE :: SomeException -> ()
7679 ignoreE = const ()
@@ -85,10 +88,10 @@ data Res (sym :: Symbol)
8588instance (KnownSymbol sym, HasServer api ctx) => HasServer (Res sym :> api) ctx where
8689 type ServerT (Res sym :> api) m = IORef (TestResource String) -> ServerT api m
8790 route Proxy ctx server = route (Proxy :: Proxy api) ctx $
88− server `addBodyCheck` check
91+ addBodyCheck server (return ()) check
8992 where
9093 sym = symbolVal (Proxy :: Proxy sym)
91− check = do
94+ check () = do
9295 liftIO $ writeTestResource sym
9396 _ <- register freeTestResource
9497 return delayedTestRef
servant/src/Servant/API/ContentTypes.hs+13−8
@@ -220,14 +220,20 @@ class Accept ctype => MimeUnrender ctype a where
220220 {-# MINIMAL mimeUnrender | mimeUnrenderWithType #-}
221221
222222class AllCTUnrender (list :: [*]) a where
223+ canHandleCTypeH
224+ :: Proxy list
225+ -> ByteString -- Content-Type header
226+ -> Maybe (ByteString -> Either String a)
227+
223228 handleCTypeH :: Proxy list
224229 -> ByteString -- Content-Type header
225230 -> ByteString -- Request body
226231 -> Maybe (Either String a)
232+ handleCTypeH p ctypeH body = ($ body) `fmap` canHandleCTypeH p ctypeH
227233
228234instance ( AllMimeUnrender ctyps a ) => AllCTUnrender ctyps a where
229− handleCTypeH _ ctypeH body = M.mapContentMedia lkup (cs ctypeH)
230− where lkup = allMimeUnrender (Proxy :: Proxy ctyps) body
235+ canHandleCTypeH p ctypeH =
236+ M.mapContentMedia (allMimeUnrender p) (cs ctypeH)
231237
232238--------------------------------------------------------------------------
233239-- * Utils (Internal)
@@ -292,20 +298,19 @@ instance OVERLAPPING_
292298--------------------------------------------------------------------------
293299class (AllMime list) => AllMimeUnrender (list :: [*]) a where
294300 allMimeUnrender :: Proxy list
295− -> ByteString
296− -> [(M.MediaType, Either String a)]
301+ -> [(M.MediaType, ByteString -> Either String a)]
297302
298303instance AllMimeUnrender '[] a where
299− allMimeUnrender _ _ = []
304+ allMimeUnrender _ = []
300305
301306instance ( MimeUnrender ctyp a
302307 , AllMimeUnrender ctyps a
303308 ) => AllMimeUnrender (ctyp ': ctyps) a where
304− allMimeUnrender _ bs =
309+ allMimeUnrender _ =
305310 (map mk $ NE.toList $ contentTypes pctyp)
306− ++ allMimeUnrender pctyps bs
311+ ++ allMimeUnrender pctyps
307312 where
308− mk ct = (ct, mimeUnrenderWithType pctyp ct bs)
313+ mk ct = (ct, \bs -> mimeUnrenderWithType pctyp ct bs)
309314 pctyp = Proxy :: Proxy ctyp
310315 pctyps = Proxy :: Proxy ctyps
311316