on master help sign in
Merge pull request #670 from phadej/pull-649
22b4d13Oleg Grenrus
servant-server/CHANGELOG.md+4
@@ -3,6 +3,10 @@
3
3
4* Add `err422` Unprocessable Entity
4* Add `err422` Unprocessable Entity
5 ([#646](https://github.com/haskell-servant/servant/pull/646))
5 ([#646](https://github.com/haskell-servant/servant/pull/646))
6+* Changed `HasServer` instances for `QueryParam` and `QueryParam` to throw 400
7+ when parsing fails
8+ ([#649](6e77453b67dc164e5381fb867e5e6475302619a3))
9+* Added `paramD` block to `Delayed`
6
10
7* `Handler` is now an abstract datatype. Migration hint: change `throwE` to `throwError`.
11* `Handler` is now an abstract datatype. Migration hint: change `throwE` to `throwError`.
8 ([#641](https://github.com/haskell-servant/servant/issues/641))
12 ([#641](https://github.com/haskell-servant/servant/issues/641))
servant-server/src/Servant/Server/Internal.hs+52−29
@@ -29,8 +29,10 @@ import qualified Data.ByteString as B
29import qualified Data.ByteString.Char8 as BC8
29import qualified Data.ByteString.Char8 as BC8
30import qualified Data.ByteString.Lazy as BL
30import qualified Data.ByteString.Lazy as BL
31import Data.Maybe (fromMaybe, mapMaybe)
31import Data.Maybe (fromMaybe, mapMaybe)
32+import Data.Either (partitionEithers)
32import Data.String (fromString)
33import Data.String (fromString)
33import Data.String.Conversions (cs, (<>))
34import Data.String.Conversions (cs, (<>))
35+import qualified Data.Text as T
34import Data.Typeable
36import Data.Typeable
35import GHC.TypeLits (KnownNat, KnownSymbol, natVal,
37import GHC.TypeLits (KnownNat, KnownSymbol, natVal,
36 symbolVal)
38 symbolVal)
@@ -45,7 +47,7 @@ import Network.Wai (Application, Request, Response,
45import Prelude ()
47import Prelude ()
46import Prelude.Compat
48import Prelude.Compat
47import Web.HttpApiData (FromHttpApiData, parseHeaderMaybe,
49import Web.HttpApiData (FromHttpApiData, parseHeaderMaybe,
48− parseQueryParamMaybe,
50+ parseQueryParam,
49 parseUrlPieceMaybe,
51 parseUrlPieceMaybe,
50 parseUrlPieces)
52 parseUrlPieces)
51import Servant.API ((:<|>) (..), (:>), BasicAuth, Capture,
53import Servant.API ((:<|>) (..), (:>), BasicAuth, Capture,
@@ -311,14 +313,22 @@ instance (KnownSymbol sym, FromHttpApiData a, HasServer api context)
311 Maybe a -> ServerT api m
313 Maybe a -> ServerT api m
312
314
313 route Proxy context subserver =
315 route Proxy context subserver =
314− let querytext r = parseQueryText $ rawQueryString r
316+ let querytext req = parseQueryText $ rawQueryString req
315− param r =
317+ parseParam req =
316− case lookup paramname (querytext r) of
318+ case lookup paramname (querytext req) of
317− Nothing -> Nothing -- param absent from the query string
319+ Nothing -> return Nothing -- param absent from the query string
318− Just Nothing -> Nothing -- param present with no value -> Nothing
320+ Just Nothing -> return Nothing -- param present with no value -> Nothing
319− Just (Just v) -> parseQueryParamMaybe v -- if present, we try to convert to
321+ Just (Just v) ->
320− -- the right type
322+ case parseQueryParam v of
323+ Left e -> delayedFailFatal err400
324+ { errBody = cs $ "Error parsing query parameter " <> paramname <> " failed: " <> e
325+ }
326+
327+ Right param -> return $ Just param
328+ delayed = addParameterCheck subserver . withRequest $ \req ->
329+ parseParam req
330+
321− in route (Proxy :: Proxy api) context (passToServer subserver param)
331+ in route (Proxy :: Proxy api) context delayed
322 where paramname = cs $ symbolVal (Proxy :: Proxy sym)
332 where paramname = cs $ symbolVal (Proxy :: Proxy sym)
323
333
324-- | If you use @'QueryParams' "authors" Text@ in one of the endpoints for your API,
334-- | If you use @'QueryParams' "authors" Text@ in one of the endpoints for your API,
@@ -346,18 +356,25 @@ instance (KnownSymbol sym, FromHttpApiData a, HasServer api context)
346 type ServerT (QueryParams sym a :> api) m =
356 type ServerT (QueryParams sym a :> api) m =
347 [a] -> ServerT api m
357 [a] -> ServerT api m
348
358
349− route Proxy context subserver =
359+ route Proxy context subserver = route (Proxy :: Proxy api) context $
350− let querytext r = parseQueryText $ rawQueryString r
360+ subserver `addParameterCheck` withRequest paramsCheck
351− -- if sym is "foo", we look for query string parameters
361+ where
352− -- named "foo" or "foo[]" and call parseQueryParam on the
362+ paramname = cs $ symbolVal (Proxy :: Proxy sym)
353− -- corresponding values
363+ paramsCheck req =
354− parameters r = filter looksLikeParam (querytext r)
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]
355− values r = mapMaybe (convert . snd) (parameters r)
371+ params = mapMaybe snd
356− in route (Proxy :: Proxy api) context (passToServer subserver values)
372+ . filter (looksLikeParam . fst)
357− where paramname = cs $ symbolVal (Proxy :: Proxy sym)
373+ . parseQueryText
374+ . rawQueryString
375+ $ req
376+
358− looksLikeParam (name, _) = name == paramname || name == (paramname <> "[]")
377+ looksLikeParam name = name == paramname || name == (paramname <> "[]")
359− convert Nothing = Nothing
360− convert (Just v) = parseQueryParamMaybe v
361
378
362-- | If you use @'QueryFlag' "published"@ in one of the endpoints for your API,
379-- | If you use @'QueryFlag' "published"@ in one of the endpoints for your API,
363-- this automatically requires your server-side handler to be a function
380-- this automatically requires your server-side handler to be a function
@@ -439,22 +456,28 @@ instance ( AllCTUnrender list a, HasServer api context
439 type ServerT (ReqBody list a :> api) m =
456 type ServerT (ReqBody list a :> api) m =
440 a -> ServerT api m
457 a -> ServerT api m
441
458
442− route Proxy context subserver =
459+ route Proxy context subserver
460+ = route (Proxy :: Proxy api) context $
443− route (Proxy :: Proxy api) context (addBodyCheck subserver bodyCheck)
461+ addBodyCheck subserver ctCheck bodyCheck
444 where
462 where
463+ -- Content-Type check, we only lookup we can try to parse the request body
445− bodyCheck = withRequest $ \ request -> do
464+ ctCheck = withRequest $ \ request -> do
446 -- See HTTP RFC 2616, section 7.2.1
465 -- See HTTP RFC 2616, section 7.2.1
447 -- http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
466 -- http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
448 -- See also "W3C Internet Media Type registration, consistency of use"
467 -- See also "W3C Internet Media Type registration, consistency of use"
449 -- http://www.w3.org/2001/tag/2002/0129-mime
468 -- http://www.w3.org/2001/tag/2002/0129-mime
450 let contentTypeH = fromMaybe "application/octet-stream"
469 let contentTypeH = fromMaybe "application/octet-stream"
451 $ lookup hContentType $ requestHeaders request
470 $ lookup hContentType $ requestHeaders request
452− mrqbody <- handleCTypeH (Proxy :: Proxy list) (cs contentTypeH)
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
453− <$> liftIO (lazyRequestBody request)
477+ mrqbody <- f <$> liftIO (lazyRequestBody request)
454 case mrqbody of
478 case mrqbody of
455− Nothing -> delayedFailFatal err415
456− Just (Left e) -> delayedFailFatal err400 { errBody = cs e }
479+ Left e -> delayedFailFatal err400 { errBody = cs e }
457− Just (Right v) -> return v
480+ Right v -> return v
458
481
459-- | Make sure the incoming request starts with @"/path"@, strip it and
482-- | Make sure the incoming request starts with @"/path"@, strip it and
460-- pass the rest of the request path to @api@.
483-- pass the rest of the request path to @api@.
servant-server/src/Servant/Server/Internal/RoutingApplication.hs+60−31
@@ -132,14 +132,14 @@ toApplication ra request respond = ra request routingRespond
132-- 405 (bad method)
132-- 405 (bad method)
133-- 401 (unauthorized)
133-- 401 (unauthorized)
134-- 415 (unsupported media type)
134-- 415 (unsupported media type)
135−-- 400 (bad request)
136-- 406 (not acceptable)
135-- 406 (not acceptable)
136+-- 400 (bad request)
137-- @
137-- @
138--
138--
139-- Therefore, while routing, we delay most checks so that they
139-- Therefore, while routing, we delay most checks so that they
140-- will ultimately occur in the right order.
140-- will ultimately occur in the right order.
141--
141--
142−-- A 'Delayed' contains three delayed blocks of tests, and
142+-- A 'Delayed' contains many delayed blocks of tests, and
143-- the actual handler:
143-- the actual handler:
144--
144--
145-- 1. Delayed captures. These can actually cause 404, and
145-- 1. Delayed captures. These can actually cause 404, and
@@ -152,23 +152,36 @@ toApplication ra request respond = ra request routingRespond
152-- it does not provide an input for the handler. Method checks
152-- it does not provide an input for the handler. Method checks
153-- are comparatively cheap.
153-- are comparatively cheap.
154--
154--
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+--
155−-- 3. Body and accept header checks. The request body check can
163+-- 6. Body check. The request body check can cause 400.
156−-- cause both 400 and 415. This provides an input to the handler.
157−-- The accept header check can be performed as the final
158−-- computation in this block. It can cause a 406.
159--
164--
160data Delayed env c where
165data Delayed env c where
161 Delayed :: { capturesD :: env -> DelayedIO captures
166 Delayed :: { capturesD :: env -> DelayedIO captures
162 , methodD :: DelayedIO ()
167 , methodD :: DelayedIO ()
163 , authD :: DelayedIO auth
168 , authD :: DelayedIO auth
169+ , acceptD :: DelayedIO ()
170+ , contentD :: DelayedIO contentType
171+ , paramsD :: DelayedIO params
164− , bodyD :: DelayedIO body
172+ , bodyD :: contentType -> DelayedIO body
165− , serverD :: captures -> auth -> body -> Request -> RouteResult c
173+ , serverD :: captures
174+ -> params
175+ -> auth
176+ -> body
177+ -> Request
178+ -> RouteResult c
166 } -> Delayed env c
179 } -> Delayed env c
167
180
168instance Functor (Delayed env) where
181instance Functor (Delayed env) where
169 fmap f Delayed{..} =
182 fmap f Delayed{..} =
170 Delayed
183 Delayed
171− { serverD = \ c a b req -> f <$> serverD c a b req
184+ { serverD = \ c p a b req -> f <$> serverD c p a b req
172 , ..
185 , ..
173 } -- Note [Existential Record Update]
186 } -- Note [Existential Record Update]
174
187
@@ -200,7 +213,7 @@ runDelayedIO m req = transResourceT runRouteResultT $ runReaderT (runDelayedIO'
200-- | A 'Delayed' without any stored checks.
213-- | A 'Delayed' without any stored checks.
201emptyDelayed :: RouteResult a -> Delayed env a
214emptyDelayed :: RouteResult a -> Delayed env a
202emptyDelayed result =
215emptyDelayed result =
203− Delayed (const r) r r r (\ _ _ _ _ -> result)
216+ Delayed (const r) r r r r r (const r) (\ _ _ _ _ _ -> result)
204 where
217 where
205 r = return ()
218 r = return ()
206
219
@@ -225,10 +238,21 @@ addCapture :: Delayed env (a -> b)
225addCapture Delayed{..} new =
238addCapture Delayed{..} new =
226 Delayed
239 Delayed
227 { capturesD = \ (txt, env) -> (,) <$> capturesD env <*> new txt
240 { capturesD = \ (txt, env) -> (,) <$> capturesD env <*> new txt
228− , serverD = \ (x, v) a b req -> ($ v) <$> serverD x a b req
241+ , serverD = \ (x, v) p a b req -> ($ v) <$> serverD x p a b req
229 , ..
242 , ..
230 } -- Note [Existential Record Update]
243 } -- Note [Existential Record Update]
231
244
245+-- | Add a parameter check to the end of the params block
246+addParameterCheck :: Delayed env (a -> b)
247+ -> DelayedIO a
248+ -> Delayed env b
249+addParameterCheck Delayed {..} new =
250+ Delayed
251+ { paramsD = (,) <$> paramsD <*> new
252+ , serverD = \c (p, pNew) a b req -> ($ pNew) <$> serverD c p a b req
253+ , ..
254+ }
255+
232-- | Add a method check to the end of the method block.
256-- | Add a method check to the end of the method block.
233addMethodCheck :: Delayed env a
257addMethodCheck :: Delayed env a
234 -> DelayedIO ()
258 -> DelayedIO ()
@@ -246,24 +270,29 @@ addAuthCheck :: Delayed env (a -> b)
246addAuthCheck Delayed{..} new =
270addAuthCheck Delayed{..} new =
247 Delayed
271 Delayed
248 { authD = (,) <$> authD <*> new
272 { authD = (,) <$> authD <*> new
249− , serverD = \ c (y, v) b req -> ($ v) <$> serverD c y b req
273+ , serverD = \ c p (y, v) b req -> ($ v) <$> serverD c p y b req
250 , ..
274 , ..
251 } -- Note [Existential Record Update]
275 } -- Note [Existential Record Update]
252
276
253−-- | 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.
254addBodyCheck :: Delayed env (a -> b)
281addBodyCheck :: Delayed env (a -> b)
255− -> DelayedIO a
282+ -> DelayedIO c -- ^ content type check
283+ -> (c -> DelayedIO a) -- ^ body check
256 -> Delayed env b
284 -> Delayed env b
257−addBodyCheck Delayed{..} new =
285+addBodyCheck Delayed{..} newContentD newBodyD =
258 Delayed
286 Delayed
287+ { contentD = (,) <$> contentD <*> newContentD
259− { bodyD = (,) <$> bodyD <*> new
288+ , bodyD = \(content, c) -> (,) <$> bodyD content <*> newBodyD c
260− , serverD = \ c a (z, v) req -> ($ v) <$> serverD c a z req
289+ , serverD = \ c p a (z, v) req -> ($ v) <$> serverD c p a z req
261 , ..
290 , ..
262 } -- Note [Existential Record Update]
291 } -- Note [Existential Record Update]
263
292
264
293
265−-- | Add an accept header check to the beginning of the body
294+-- | Add an accept header check before handling parameters.
266−-- block. There is a tradeoff here. In principle, we'd like
295+-- In principle, we'd like
267-- to take a bad body (400) response take precedence over a
296-- to take a bad body (400) response take precedence over a
268-- failed accept check (406). BUT to allow streaming the body,
297-- failed accept check (406). BUT to allow streaming the body,
269-- we cannot run the body check and then still backtrack.
298-- we cannot run the body check and then still backtrack.
@@ -277,7 +306,7 @@ addAcceptCheck :: Delayed env a
277 -> Delayed env a
306 -> Delayed env a
278addAcceptCheck Delayed{..} new =
307addAcceptCheck Delayed{..} new =
279 Delayed
308 Delayed
280− { bodyD = new *> bodyD
309+ { acceptD = acceptD *> new
281 , ..
310 , ..
282 } -- Note [Existential Record Update]
311 } -- Note [Existential Record Update]
283
312
@@ -287,7 +316,7 @@ addAcceptCheck Delayed{..} new =
287passToServer :: Delayed env (a -> b) -> (Request -> a) -> Delayed env b
316passToServer :: Delayed env (a -> b) -> (Request -> a) -> Delayed env b
288passToServer Delayed{..} x =
317passToServer Delayed{..} x =
289 Delayed
318 Delayed
290− { serverD = \ c a b req -> ($ x req) <$> serverD c a b req
319+ { serverD = \ c p a b req -> ($ x req) <$> serverD c p a b req
291 , ..
320 , ..
292 } -- Note [Existential Record Update]
321 } -- Note [Existential Record Update]
293
322
@@ -301,16 +330,16 @@ runDelayed :: Delayed env a
301 -> env
330 -> env
302 -> Request
331 -> Request
303 -> ResourceT IO (RouteResult a)
332 -> ResourceT IO (RouteResult a)
304−runDelayed Delayed{..} env req =
333+runDelayed Delayed{..} env = runDelayedIO $ do
305− runDelayedIO
334+ r <- ask
306− (do c <- capturesD env
335+ c <- capturesD env
307− methodD
336+ methodD
308− a <- authD
337+ a <- authD
338+ acceptD
339+ content <- contentD
340+ p <- paramsD -- Has to be before body parsing, but after content-type checks
309− b <- bodyD
341+ b <- bodyD content
310− r <- ask
311− liftRouteResult (serverD c a b r)
342+ liftRouteResult (serverD c p a b r)
312− )
313− req
314
343
315-- | Runs a delayed server and the resulting action.
344-- | Runs a delayed server and the resulting action.
316-- Takes a continuation that lets us send a response.
345-- Takes a continuation that lets us send a response.
servant-server/test/Servant/Server/ErrorSpec.hs+23−9
@@ -6,9 +6,11 @@
6{-# OPTIONS_GHC -fno-warn-orphans #-}
6{-# OPTIONS_GHC -fno-warn-orphans #-}
7module Servant.Server.ErrorSpec (spec) where
7module Servant.Server.ErrorSpec (spec) where
8
8
9+import Control.Monad (when)
9import Data.Aeson (encode)
10import Data.Aeson (encode)
10import qualified Data.ByteString.Char8 as BC
11import qualified Data.ByteString.Char8 as BC
11import qualified Data.ByteString.Lazy.Char8 as BCL
12import qualified Data.ByteString.Lazy.Char8 as BCL
13+import Data.Monoid ((<>))
12import Data.Proxy
14import Data.Proxy
13import Network.HTTP.Types (hAccept, hAuthorization,
15import Network.HTTP.Types (hAccept, hAuthorization,
14 hContentType, methodGet,
16 hContentType, methodGet,
@@ -44,13 +46,14 @@ type ErrorOrderApi = "home"
44 :> BasicAuth "error-realm" ()
46 :> BasicAuth "error-realm" ()
45 :> ReqBody '[JSON] Int
47 :> ReqBody '[JSON] Int
46 :> Capture "t" Int
48 :> Capture "t" Int
49+ :> QueryParam "param" Int
47 :> Post '[JSON] Int
50 :> Post '[JSON] Int
48
51
49errorOrderApi :: Proxy ErrorOrderApi
52errorOrderApi :: Proxy ErrorOrderApi
50errorOrderApi = Proxy
53errorOrderApi = Proxy
51
54
52errorOrderServer :: Server ErrorOrderApi
55errorOrderServer :: Server ErrorOrderApi
53−errorOrderServer = \_ _ _ -> throwError err402
56+errorOrderServer = \_ _ _ _ -> throwError err402
54
57
55-- On error priorities:
58-- On error priorities:
56--
59--
@@ -85,7 +88,8 @@ errorOrderSpec =
85 goodContentType = (hContentType, "application/json")
88 goodContentType = (hContentType, "application/json")
86 goodAccept = (hAccept, "application/json")
89 goodAccept = (hAccept, "application/json")
87 goodMethod = methodPost
90 goodMethod = methodPost
88− goodUrl = "home/2"
91+ goodUrl = "home/2?param=55"
92+ badParams = goodUrl <> "?param=foo"
89 goodBody = encode (5 :: Int)
93 goodBody = encode (5 :: Int)
90 -- username:password = servant:server
94 -- username:password = servant:server
91 goodAuth = (hAuthorization, "Basic c2VydmFudDpzZXJ2ZXI=")
95 goodAuth = (hAuthorization, "Basic c2VydmFudDpzZXJ2ZXI=")
@@ -95,24 +99,35 @@ errorOrderSpec =
95 `shouldRespondWith` 404
99 `shouldRespondWith` 404
96
100
97 it "has 405 as its second highest priority error" $ do
101 it "has 405 as its second highest priority error" $ do
98− request badMethod goodUrl [badAuth, badContentType, badAccept] badBody
102+ request badMethod badParams [badAuth, badContentType, badAccept] badBody
99 `shouldRespondWith` 405
103 `shouldRespondWith` 405
100
104
101 it "has 401 as its third highest priority error (auth)" $ do
105 it "has 401 as its third highest priority error (auth)" $ do
102− request goodMethod goodUrl [badAuth, badContentType, badAccept] badBody
106+ request goodMethod badParams [badAuth, badContentType, badAccept] badBody
103 `shouldRespondWith` 401
107 `shouldRespondWith` 401
104
108
105 it "has 406 as its fourth highest priority error" $ do
109 it "has 406 as its fourth highest priority error" $ do
106− request goodMethod goodUrl [goodAuth, badContentType, badAccept] badBody
110+ request goodMethod badParams [goodAuth, badContentType, badAccept] badBody
107 `shouldRespondWith` 406
111 `shouldRespondWith` 406
108
112
109 it "has 415 as its fifth highest priority error" $ do
113 it "has 415 as its fifth highest priority error" $ do
110− request goodMethod goodUrl [goodAuth, badContentType, goodAccept] badBody
114+ request goodMethod badParams [goodAuth, badContentType, goodAccept] badBody
111 `shouldRespondWith` 415
115 `shouldRespondWith` 415
112
116
113 it "has 400 as its sixth highest priority error" $ do
117 it "has 400 as its sixth highest priority error" $ do
118+ badParamsRes <- request goodMethod badParams [goodAuth, goodContentType, goodAccept] goodBody
114− request goodMethod goodUrl [goodAuth, goodContentType, goodAccept] badBody
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
115− `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
116
131
117 it "has handler-level errors as last priority" $ do
132 it "has handler-level errors as last priority" $ do
118 request goodMethod goodUrl [goodAuth, goodContentType, goodAccept] goodBody
133 request goodMethod goodUrl [goodAuth, goodContentType, goodAccept] goodBody
@@ -221,7 +236,6 @@ errorRetrySpec =
221 then Nothing
236 then Nothing
222 else Just "body not correct\n"
237 else Just "body not correct\n"
223
238
224−
225-- }}}
239-- }}}
226------------------------------------------------------------------------------
240------------------------------------------------------------------------------
227-- * Error Choice {{{
241-- * Error Choice {{{
servant-server/test/Servant/Server/Internal/RoutingApplicationSpec.hs+11−7
@@ -19,6 +19,7 @@ import Data.Proxy
19import GHC.TypeLits (Symbol, KnownSymbol, symbolVal)
19import GHC.TypeLits (Symbol, KnownSymbol, symbolVal)
20import Servant
20import Servant
21import Servant.Server.Internal.RoutingApplication
21import Servant.Server.Internal.RoutingApplication
22+import Network.Wai (defaultRequest)
22import Test.Hspec
23import Test.Hspec
23import Test.Hspec.Wai (request, shouldRespondWith, with)
24import Test.Hspec.Wai (request, shouldRespondWith, with)
24
25
@@ -56,20 +57,23 @@ freeTestResource = modifyIORef delayedTestRef $ \r -> case r of
56
57
57delayed :: DelayedIO () -> RouteResult (Handler ()) -> Delayed () (Handler ())
58delayed :: DelayedIO () -> RouteResult (Handler ()) -> Delayed () (Handler ())
58delayed body srv = Delayed
59delayed body srv = Delayed
59− { capturesD = \_ -> return ()
60+ { capturesD = \() -> return ()
60 , methodD = return ()
61 , methodD = return ()
61 , authD = return ()
62 , authD = return ()
63+ , acceptD = return ()
64+ , contentD = return ()
65+ , paramsD = return ()
62− , bodyD = do
66+ , bodyD = \() -> do
63− liftIO (writeTestResource"hia" >> putStrLn "garbage created")
67+ liftIO (writeTestResource "hia" >> putStrLn "garbage created")
64 _ <- register (freeTestResource >> putStrLn "garbage collected")
68 _ <- register (freeTestResource >> putStrLn "garbage collected")
65 body
69 body
66− , serverD = \() () _body _req -> srv
70+ , serverD = \() () () _body _req -> srv
67 }
71 }
68
72
69simpleRun :: Delayed () (Handler ())
73simpleRun :: Delayed () (Handler ())
70 -> IO ()
74 -> IO ()
71simpleRun d = fmap (either ignoreE id) . try $
75simpleRun d = fmap (either ignoreE id) . try $
72− runAction d () undefined (\_ -> return ()) (\_ -> FailFatal err500)
76+ runAction d () defaultRequest (\_ -> return ()) (\_ -> FailFatal err500)
73
77
74 where ignoreE :: SomeException -> ()
78 where ignoreE :: SomeException -> ()
75 ignoreE = const ()
79 ignoreE = const ()
@@ -84,10 +88,10 @@ data Res (sym :: Symbol)
84instance (KnownSymbol sym, HasServer api ctx) => HasServer (Res sym :> api) ctx where
88instance (KnownSymbol sym, HasServer api ctx) => HasServer (Res sym :> api) ctx where
85 type ServerT (Res sym :> api) m = IORef (TestResource String) -> ServerT api m
89 type ServerT (Res sym :> api) m = IORef (TestResource String) -> ServerT api m
86 route Proxy ctx server = route (Proxy :: Proxy api) ctx $
90 route Proxy ctx server = route (Proxy :: Proxy api) ctx $
87− server `addBodyCheck` check
91+ addBodyCheck server (return ()) check
88 where
92 where
89 sym = symbolVal (Proxy :: Proxy sym)
93 sym = symbolVal (Proxy :: Proxy sym)
90− check = do
94+ check () = do
91 liftIO $ writeTestResource sym
95 liftIO $ writeTestResource sym
92 _ <- register freeTestResource
96 _ <- register freeTestResource
93 return delayedTestRef
97 return delayedTestRef
servant-server/test/Servant/ServerSpec.hs+56−1
@@ -275,12 +275,14 @@ captureAllSpec = do
275type QueryParamApi = QueryParam "name" String :> Get '[JSON] Person
275type QueryParamApi = QueryParam "name" String :> Get '[JSON] Person
276 :<|> "a" :> QueryParams "names" String :> Get '[JSON] Person
276 :<|> "a" :> QueryParams "names" String :> Get '[JSON] Person
277 :<|> "b" :> QueryFlag "capitalize" :> Get '[JSON] Person
277 :<|> "b" :> QueryFlag "capitalize" :> Get '[JSON] Person
278+ :<|> "param" :> QueryParam "age" Integer :> Get '[JSON] Person
279+ :<|> "multiparam" :> QueryParams "ages" Integer :> Get '[JSON] Person
278
280
279queryParamApi :: Proxy QueryParamApi
281queryParamApi :: Proxy QueryParamApi
280queryParamApi = Proxy
282queryParamApi = Proxy
281
283
282qpServer :: Server QueryParamApi
284qpServer :: Server QueryParamApi
283−qpServer = queryParamServer :<|> qpNames :<|> qpCapitalize
285+qpServer = queryParamServer :<|> qpNames :<|> qpCapitalize :<|> qpAge :<|> qpAges
284
286
285 where qpNames (_:name2:_) = return alice { name = name2 }
287 where qpNames (_:name2:_) = return alice { name = name2 }
286 qpNames _ = return alice
288 qpNames _ = return alice
@@ -288,6 +290,11 @@ qpServer = queryParamServer :<|> qpNames :<|> qpCapitalize
288 qpCapitalize False = return alice
290 qpCapitalize False = return alice
289 qpCapitalize True = return alice { name = map toUpper (name alice) }
291 qpCapitalize True = return alice { name = map toUpper (name alice) }
290
292
293+ qpAge Nothing = return alice
294+ qpAge (Just age') = return alice{ age = age'}
295+
296+ qpAges ages = return alice{ age = sum ages}
297+
291 queryParamServer (Just name_) = return alice{name = name_}
298 queryParamServer (Just name_) = return alice{name = name_}
292 queryParamServer Nothing = return alice
299 queryParamServer Nothing = return alice
293
300
@@ -319,6 +326,54 @@ queryParamSpec = do
319 name = "john"
326 name = "john"
320 }
327 }
321
328
329+ it "parses a query parameter" $
330+ (flip runSession) (serve queryParamApi qpServer) $ do
331+ let params = "?age=55"
332+ response <- Network.Wai.Test.request defaultRequest{
333+ rawQueryString = params,
334+ queryString = parseQuery params,
335+ pathInfo = ["param"]
336+ }
337+ liftIO $
338+ decode' (simpleBody response) `shouldBe` Just alice{
339+ age = 55
340+ }
341+
342+ it "generates an error on query parameter parse failure" $
343+ (flip runSession) (serve queryParamApi qpServer) $ do
344+ let params = "?age=foo"
345+ response <- Network.Wai.Test.request defaultRequest{
346+ rawQueryString = params,
347+ queryString = parseQuery params,
348+ pathInfo = ["param"]
349+ }
350+ liftIO $ statusCode (simpleStatus response) `shouldBe` 400
351+ return ()
352+
353+ it "parses multiple query parameters" $
354+ (flip runSession) (serve queryParamApi qpServer) $ do
355+ let params = "?ages=10&ages=22"
356+ response <- Network.Wai.Test.request defaultRequest{
357+ rawQueryString = params,
358+ queryString = parseQuery params,
359+ pathInfo = ["multiparam"]
360+ }
361+ liftIO $
362+ decode' (simpleBody response) `shouldBe` Just alice{
363+ age = 32
364+ }
365+
366+ it "generates an error on parse failures of multiple parameters" $
367+ (flip runSession) (serve queryParamApi qpServer) $ do
368+ let params = "?ages=2&ages=foo"
369+ response <- Network.Wai.Test.request defaultRequest{
370+ rawQueryString = params,
371+ queryString = parseQuery params,
372+ pathInfo = ["multiparam"]
373+ }
374+ liftIO $ statusCode (simpleStatus response) `shouldBe` 400
375+ return ()
376+
322
377
323 it "allows retrieving value-less GET parameters" $
378 it "allows retrieving value-less GET parameters" $
324 (flip runSession) (serve queryParamApi qpServer) $ do
379 (flip runSession) (serve queryParamApi qpServer) $ do
servant/src/Servant/API/ContentTypes.hs+13−8
@@ -220,14 +220,20 @@ class Accept ctype => MimeUnrender ctype a where
220 {-# MINIMAL mimeUnrender | mimeUnrenderWithType #-}
220 {-# MINIMAL mimeUnrender | mimeUnrenderWithType #-}
221
221
222class AllCTUnrender (list :: [*]) a where
222class AllCTUnrender (list :: [*]) a where
223+ canHandleCTypeH
224+ :: Proxy list
225+ -> ByteString -- Content-Type header
226+ -> Maybe (ByteString -> Either String a)
227+
223 handleCTypeH :: Proxy list
228 handleCTypeH :: Proxy list
224 -> ByteString -- Content-Type header
229 -> ByteString -- Content-Type header
225 -> ByteString -- Request body
230 -> ByteString -- Request body
226 -> Maybe (Either String a)
231 -> Maybe (Either String a)
232+ handleCTypeH p ctypeH body = ($ body) `fmap` canHandleCTypeH p ctypeH
227
233
228instance ( AllMimeUnrender ctyps a ) => AllCTUnrender ctyps a where
234instance ( AllMimeUnrender ctyps a ) => AllCTUnrender ctyps a where
229− handleCTypeH _ ctypeH body = M.mapContentMedia lkup (cs ctypeH)
235+ canHandleCTypeH p ctypeH =
230− where lkup = allMimeUnrender (Proxy :: Proxy ctyps) body
236+ M.mapContentMedia (allMimeUnrender p) (cs ctypeH)
231
237
232--------------------------------------------------------------------------
238--------------------------------------------------------------------------
233-- * Utils (Internal)
239-- * Utils (Internal)
@@ -292,20 +298,19 @@ instance OVERLAPPING_
292--------------------------------------------------------------------------
298--------------------------------------------------------------------------
293class (AllMime list) => AllMimeUnrender (list :: [*]) a where
299class (AllMime list) => AllMimeUnrender (list :: [*]) a where
294 allMimeUnrender :: Proxy list
300 allMimeUnrender :: Proxy list
295− -> ByteString
296− -> [(M.MediaType, Either String a)]
301+ -> [(M.MediaType, ByteString -> Either String a)]
297
302
298instance AllMimeUnrender '[] a where
303instance AllMimeUnrender '[] a where
299− allMimeUnrender _ _ = []
304+ allMimeUnrender _ = []
300
305
301instance ( MimeUnrender ctyp a
306instance ( MimeUnrender ctyp a
302 , AllMimeUnrender ctyps a
307 , AllMimeUnrender ctyps a
303 ) => AllMimeUnrender (ctyp ': ctyps) a where
308 ) => AllMimeUnrender (ctyp ': ctyps) a where
304− allMimeUnrender _ bs =
309+ allMimeUnrender _ =
305 (map mk $ NE.toList $ contentTypes pctyp)
310 (map mk $ NE.toList $ contentTypes pctyp)
306− ++ allMimeUnrender pctyps bs
311+ ++ allMimeUnrender pctyps
307 where
312 where
308− mk ct = (ct, mimeUnrenderWithType pctyp ct bs)
313+ mk ct = (ct, \bs -> mimeUnrenderWithType pctyp ct bs)
309 pctyp = Proxy :: Proxy ctyp
314 pctyp = Proxy :: Proxy ctyp
310 pctyps = Proxy :: Proxy ctyps
315 pctyps = Proxy :: Proxy ctyps
311
316