git.iko.soy/typeable/servant
on master help sign in
throw 400 on query parameter parse failure
3a0cbddPhilipp Balzarek
servant-server/src/Servant/Server/Internal.hs+32−14
@@ -28,7 +28,8 @@ import Control.Monad.Trans.Resource (runResourceT)
28import qualified Data.ByteString as B
28import 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
31+import Data.Either (partitionEithers)
31−import Data.Maybe (fromMaybe, mapMaybe)
32+import Data.Maybe (fromMaybe)
32import Data.String (fromString)
33import Data.String (fromString)
33import Data.String.Conversions (cs, (<>))
34import Data.String.Conversions (cs, (<>))
34import Data.Typeable
35import Data.Typeable
@@ -45,7 +46,7 @@ import Network.Wai (Application, Request, Response,
45import Prelude ()
46import Prelude ()
46import Prelude.Compat
47import Prelude.Compat
47import Web.HttpApiData (FromHttpApiData, parseHeaderMaybe,
48import Web.HttpApiData (FromHttpApiData, parseHeaderMaybe,
48− parseQueryParamMaybe,
49+ parseQueryParam,
49 parseUrlPieceMaybe,
50 parseUrlPieceMaybe,
50 parseUrlPieces)
51 parseUrlPieces)
51import Servant.API ((:<|>) (..), (:>), BasicAuth, Capture,
52import Servant.API ((:<|>) (..), (:>), BasicAuth, Capture,
@@ -311,14 +312,23 @@ instance (KnownSymbol sym, FromHttpApiData a, HasServer api context)
311 Maybe a -> ServerT api m
312 Maybe a -> ServerT api m
312
313
313 route Proxy context subserver =
314 route Proxy context subserver =
314− let querytext r = parseQueryText $ rawQueryString r
315+ let querytext req = parseQueryText $ rawQueryString req
315− param r =
316+ parseParam req =
316− case lookup paramname (querytext r) of
317+ case lookup paramname (querytext req) of
317− Nothing -> Nothing -- param absent from the query string
318+ Nothing -> return Nothing -- param absent from the query string
318− Just Nothing -> Nothing -- param present with no value -> Nothing
319+ Just Nothing -> return Nothing -- param present with no value -> Nothing
319− Just (Just v) -> parseQueryParamMaybe v -- if present, we try to convert to
320+ Just (Just v) ->
320− -- the right type
321+ 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
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,
@@ -352,12 +362,20 @@ instance (KnownSymbol sym, FromHttpApiData a, HasServer api context)
352 -- named "foo" or "foo[]" and call parseQueryParam on the
362 -- named "foo" or "foo[]" and call parseQueryParam on the
353 -- corresponding values
363 -- corresponding values
354 parameters r = filter looksLikeParam (querytext r)
364 parameters r = filter looksLikeParam (querytext r)
355− values r = mapMaybe (convert . snd) (parameters 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
356− in route (Proxy :: Proxy api) context (passToServer subserver values)
376+ in route (Proxy :: Proxy api) context delayed
357 where paramname = cs $ symbolVal (Proxy :: Proxy sym)
377 where paramname = cs $ symbolVal (Proxy :: Proxy sym)
358 looksLikeParam (name, _) = name == paramname || name == (paramname <> "[]")
378 looksLikeParam (name, _) = name == paramname || name == (paramname <> "[]")
359− convert Nothing = Nothing
360− convert (Just v) = parseQueryParamMaybe v
361
379
362-- | If you use @'QueryFlag' "published"@ in one of the endpoints for your API,
380-- | 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
381-- this automatically requires your server-side handler to be a function
servant-server/src/Servant/Server/Internal/RoutingApplication.hs+38−20
@@ -139,7 +139,7 @@ toApplication ra request respond = ra request routingRespond
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 four 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
@@ -148,27 +148,36 @@ toApplication ra request respond = ra request routingRespond
148-- check order from the error reporting, see above). Delayed
148-- check order from the error reporting, see above). Delayed
149-- captures can provide inputs to the actual handler.
149-- captures can provide inputs to the actual handler.
150--
150--
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+--
151−-- 2. Method check(s). This can cause a 405. On success,
154+-- 3. Method check(s). This can cause a 405. On success,
152-- it does not provide an input for the handler. Method checks
155-- it does not provide an input for the handler. Method checks
153-- are comparatively cheap.
156-- are comparatively cheap.
154--
157--
155−-- 3. Body and accept header checks. The request body check can
158+-- 4. Body and accept header checks. The request body check can
156-- cause both 400 and 415. This provides an input to the handler.
159-- cause both 400 and 415. This provides an input to the handler.
157-- The accept header check can be performed as the final
160-- The accept header check can be performed as the final
158-- computation in this block. It can cause a 406.
161-- computation in this block. It can cause a 406.
159--
162--
160data Delayed env c where
163data Delayed env c where
161 Delayed :: { capturesD :: env -> DelayedIO captures
164 Delayed :: { capturesD :: env -> DelayedIO captures
165+ , paramsD :: DelayedIO params
162 , methodD :: DelayedIO ()
166 , methodD :: DelayedIO ()
163 , authD :: DelayedIO auth
167 , authD :: DelayedIO auth
164 , bodyD :: DelayedIO body
168 , bodyD :: DelayedIO body
165− , serverD :: captures -> auth -> body -> Request -> RouteResult c
169+ , serverD :: captures
170+ -> params
171+ -> auth
172+ -> body
173+ -> Request
174+ -> RouteResult c
166 } -> Delayed env c
175 } -> Delayed env c
167
176
168instance Functor (Delayed env) where
177instance Functor (Delayed env) where
169 fmap f Delayed{..} =
178 fmap f Delayed{..} =
170 Delayed
179 Delayed
171− { serverD = \ c a b req -> f <$> serverD c a b req
180+ { serverD = \ c p a b req -> f <$> serverD c p a b req
172 , ..
181 , ..
173 } -- Note [Existential Record Update]
182 } -- Note [Existential Record Update]
174
183
@@ -200,7 +209,7 @@ runDelayedIO m req = transResourceT runRouteResultT $ runReaderT (runDelayedIO'
200-- | A 'Delayed' without any stored checks.
209-- | A 'Delayed' without any stored checks.
201emptyDelayed :: RouteResult a -> Delayed env a
210emptyDelayed :: RouteResult a -> Delayed env a
202emptyDelayed result =
211emptyDelayed result =
203− Delayed (const r) r r r (\ _ _ _ _ -> result)
212+ Delayed (const r) r r r r (\ _ _ _ _ _ -> result)
204 where
213 where
205 r = return ()
214 r = return ()
206
215
@@ -225,10 +234,21 @@ addCapture :: Delayed env (a -> b)
225addCapture Delayed{..} new =
234addCapture Delayed{..} new =
226 Delayed
235 Delayed
227 { capturesD = \ (txt, env) -> (,) <$> capturesD env <*> new txt
236 { capturesD = \ (txt, env) -> (,) <$> capturesD env <*> new txt
228− , serverD = \ (x, v) a b req -> ($ v) <$> serverD x a b req
237+ , serverD = \ (x, v) p a b req -> ($ v) <$> serverD x p a b req
229 , ..
238 , ..
230 } -- Note [Existential Record Update]
239 } -- Note [Existential Record Update]
231
240
241+-- | Add a parameter check to the end of the params block
242+addParameterCheck :: Delayed env (a -> b)
243+ -> DelayedIO a
244+ -> Delayed env b
245+addParameterCheck Delayed {..} new =
246+ Delayed
247+ { paramsD = (,) <$> paramsD <*> new
248+ , serverD = \c (p, pNew) a b req -> ($ pNew) <$> serverD c p a b req
249+ , ..
250+ }
251+
232-- | Add a method check to the end of the method block.
252-- | Add a method check to the end of the method block.
233addMethodCheck :: Delayed env a
253addMethodCheck :: Delayed env a
234 -> DelayedIO ()
254 -> DelayedIO ()
@@ -246,7 +266,7 @@ addAuthCheck :: Delayed env (a -> b)
246addAuthCheck Delayed{..} new =
266addAuthCheck Delayed{..} new =
247 Delayed
267 Delayed
248 { authD = (,) <$> authD <*> new
268 { authD = (,) <$> authD <*> new
249− , serverD = \ c (y, v) b req -> ($ v) <$> serverD c y b req
269+ , serverD = \ c p (y, v) b req -> ($ v) <$> serverD c p y b req
250 , ..
270 , ..
251 } -- Note [Existential Record Update]
271 } -- Note [Existential Record Update]
252
272
@@ -257,7 +277,7 @@ addBodyCheck :: Delayed env (a -> b)
257addBodyCheck Delayed{..} new =
277addBodyCheck Delayed{..} new =
258 Delayed
278 Delayed
259 { bodyD = (,) <$> bodyD <*> new
279 { bodyD = (,) <$> bodyD <*> new
260− , serverD = \ c a (z, v) req -> ($ v) <$> serverD c a z req
280+ , serverD = \ c p a (z, v) req -> ($ v) <$> serverD c p a z req
261 , ..
281 , ..
262 } -- Note [Existential Record Update]
282 } -- Note [Existential Record Update]
263
283
@@ -287,7 +307,7 @@ addAcceptCheck Delayed{..} new =
287passToServer :: Delayed env (a -> b) -> (Request -> a) -> Delayed env b
307passToServer :: Delayed env (a -> b) -> (Request -> a) -> Delayed env b
288passToServer Delayed{..} x =
308passToServer Delayed{..} x =
289 Delayed
309 Delayed
290− { serverD = \ c a b req -> ($ x req) <$> serverD c a b req
310+ { serverD = \ c p a b req -> ($ x req) <$> serverD c p a b req
291 , ..
311 , ..
292 } -- Note [Existential Record Update]
312 } -- Note [Existential Record Update]
293
313
@@ -301,16 +321,14 @@ runDelayed :: Delayed env a
301 -> env
321 -> env
302 -> Request
322 -> Request
303 -> ResourceT IO (RouteResult a)
323 -> ResourceT IO (RouteResult a)
304−runDelayed Delayed{..} env req =
324+runDelayed Delayed{..} env = runDelayedIO $ do
305− runDelayedIO
306− (do c <- capturesD env
325+ c <- capturesD env
307− methodD
326+ methodD
308− a <- authD
327+ a <- authD
309− b <- bodyD
328+ b <- bodyD
310− r <- ask
329+ r <- ask
330+ p <- paramsD -- Has to be after body to respect the relative error order
311− liftRouteResult (serverD c a b r)
331+ liftRouteResult (serverD c p a b r)
312− )
313− req
314
332
315-- | Runs a delayed server and the resulting action.
333-- | Runs a delayed server and the resulting action.
316-- Takes a continuation that lets us send a response.
334-- Takes a continuation that lets us send a response.
servant-server/test/Servant/Server/ErrorSpec.hs+11−7
@@ -9,6 +9,7 @@ module Servant.Server.ErrorSpec (spec) where
9import Data.Aeson (encode)
9import Data.Aeson (encode)
10import qualified Data.ByteString.Char8 as BC
10import qualified Data.ByteString.Char8 as BC
11import qualified Data.ByteString.Lazy.Char8 as BCL
11import qualified Data.ByteString.Lazy.Char8 as BCL
12+import Data.Monoid ((<>))
12import Data.Proxy
13import Data.Proxy
13import Network.HTTP.Types (hAccept, hAuthorization,
14import Network.HTTP.Types (hAccept, hAuthorization,
14 hContentType, methodGet,
15 hContentType, methodGet,
@@ -44,13 +45,14 @@ type ErrorOrderApi = "home"
44 :> BasicAuth "error-realm" ()
45 :> BasicAuth "error-realm" ()
45 :> ReqBody '[JSON] Int
46 :> ReqBody '[JSON] Int
46 :> Capture "t" Int
47 :> Capture "t" Int
48+ :> QueryParam "param" Int
47 :> Post '[JSON] Int
49 :> Post '[JSON] Int
48
50
49errorOrderApi :: Proxy ErrorOrderApi
51errorOrderApi :: Proxy ErrorOrderApi
50errorOrderApi = Proxy
52errorOrderApi = Proxy
51
53
52errorOrderServer :: Server ErrorOrderApi
54errorOrderServer :: Server ErrorOrderApi
53−errorOrderServer = \_ _ _ -> throwError err402
55+errorOrderServer = \_ _ _ _ -> throwError err402
54
56
55-- On error priorities:
57-- On error priorities:
56--
58--
@@ -85,7 +87,8 @@ errorOrderSpec =
85 goodContentType = (hContentType, "application/json")
87 goodContentType = (hContentType, "application/json")
86 goodAccept = (hAccept, "application/json")
88 goodAccept = (hAccept, "application/json")
87 goodMethod = methodPost
89 goodMethod = methodPost
88− goodUrl = "home/2"
90+ goodUrl = "home/2?param=55"
91+ badParams = goodUrl <> "?param=foo"
89 goodBody = encode (5 :: Int)
92 goodBody = encode (5 :: Int)
90 -- username:password = servant:server
93 -- username:password = servant:server
91 goodAuth = (hAuthorization, "Basic c2VydmFudDpzZXJ2ZXI=")
94 goodAuth = (hAuthorization, "Basic c2VydmFudDpzZXJ2ZXI=")
@@ -95,22 +98,24 @@ errorOrderSpec =
95 `shouldRespondWith` 404
98 `shouldRespondWith` 404
96
99
97 it "has 405 as its second highest priority error" $ do
100 it "has 405 as its second highest priority error" $ do
98− request badMethod goodUrl [badAuth, badContentType, badAccept] badBody
101+ request badMethod badParams [badAuth, badContentType, badAccept] badBody
99 `shouldRespondWith` 405
102 `shouldRespondWith` 405
100
103
101 it "has 401 as its third highest priority error (auth)" $ do
104 it "has 401 as its third highest priority error (auth)" $ do
102− request goodMethod goodUrl [badAuth, badContentType, badAccept] badBody
105+ request goodMethod badParams [badAuth, badContentType, badAccept] badBody
103 `shouldRespondWith` 401
106 `shouldRespondWith` 401
104
107
105 it "has 406 as its fourth highest priority error" $ do
108 it "has 406 as its fourth highest priority error" $ do
106− request goodMethod goodUrl [goodAuth, badContentType, badAccept] badBody
109+ request goodMethod badParams [goodAuth, badContentType, badAccept] badBody
107 `shouldRespondWith` 406
110 `shouldRespondWith` 406
108
111
109 it "has 415 as its fifth highest priority error" $ do
112 it "has 415 as its fifth highest priority error" $ do
110− request goodMethod goodUrl [goodAuth, badContentType, goodAccept] badBody
113+ request goodMethod badParams [goodAuth, badContentType, goodAccept] badBody
111 `shouldRespondWith` 415
114 `shouldRespondWith` 415
112
115
113 it "has 400 as its sixth highest priority error" $ do
116 it "has 400 as its sixth highest priority error" $ do
117+ request goodMethod badParams [goodAuth, goodContentType, goodAccept] goodBody
118+ `shouldRespondWith` 400
114 request goodMethod goodUrl [goodAuth, goodContentType, goodAccept] badBody
119 request goodMethod goodUrl [goodAuth, goodContentType, goodAccept] badBody
115 `shouldRespondWith` 400
120 `shouldRespondWith` 400
116
121
@@ -221,7 +226,6 @@ errorRetrySpec =
221 then Nothing
226 then Nothing
222 else Just "body not correct\n"
227 else Just "body not correct\n"
223
228
224−
225-- }}}
229-- }}}
226------------------------------------------------------------------------------
230------------------------------------------------------------------------------
227-- * Error Choice {{{
231-- * Error Choice {{{
servant-server/test/Servant/Server/Internal/RoutingApplicationSpec.hs+2−1
@@ -59,11 +59,12 @@ delayed body srv = Delayed
59 { capturesD = \_ -> return ()
59 { capturesD = \_ -> return ()
60 , methodD = return ()
60 , methodD = return ()
61 , authD = return ()
61 , authD = return ()
62+ , paramsD = return ()
62 , bodyD = do
63 , bodyD = do
63 liftIO (writeTestResource"hia" >> putStrLn "garbage created")
64 liftIO (writeTestResource"hia" >> putStrLn "garbage created")
64 _ <- register (freeTestResource >> putStrLn "garbage collected")
65 _ <- register (freeTestResource >> putStrLn "garbage collected")
65 body
66 body
66− , serverD = \() () _body _req -> srv
67+ , serverD = \() () () _body _req -> srv
67 }
68 }
68
69
69simpleRun :: Delayed () (Handler ())
70simpleRun :: Delayed () (Handler ())
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