Skip to content

Commit 22b4d13

Browse files
authored
Merge pull request #670 from phadej/pull-649
Throw error on param parse failure.
2 parents 8c32913 + 58e931f commit 22b4d13

File tree

7 files changed

+219
-85
lines changed

7 files changed

+219
-85
lines changed

servant-server/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
* Add `err422` Unprocessable Entity
55
([#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`
610

711
* `Handler` is now an abstract datatype. Migration hint: change `throwE` to `throwError`.
812
([#641](https://github.com/haskell-servant/servant/issues/641))

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

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ import qualified Data.ByteString as B
2929
import qualified Data.ByteString.Char8 as BC8
3030
import qualified Data.ByteString.Lazy as BL
3131
import Data.Maybe (fromMaybe, mapMaybe)
32+
import Data.Either (partitionEithers)
3233
import Data.String (fromString)
3334
import Data.String.Conversions (cs, (<>))
35+
import qualified Data.Text as T
3436
import Data.Typeable
3537
import GHC.TypeLits (KnownNat, KnownSymbol, natVal,
3638
symbolVal)
@@ -45,7 +47,7 @@ import Network.Wai (Application, Request, Response,
4547
import Prelude ()
4648
import Prelude.Compat
4749
import Web.HttpApiData (FromHttpApiData, parseHeaderMaybe,
48-
parseQueryParamMaybe,
50+
parseQueryParam,
4951
parseUrlPieceMaybe,
5052
parseUrlPieces)
5153
import Servant.API ((:<|>) (..), (:>), BasicAuth, Capture,
@@ -311,14 +313,22 @@ instance (KnownSymbol sym, FromHttpApiData a, HasServer api context)
311313
Maybe a -> ServerT api m
312314

313315
route Proxy context subserver =
314-
let querytext r = parseQueryText $ rawQueryString r
315-
param r =
316-
case lookup paramname (querytext r) of
317-
Nothing -> Nothing -- param absent from the query string
318-
Just Nothing -> Nothing -- param present with no value -> Nothing
319-
Just (Just v) -> parseQueryParamMaybe v -- if present, we try to convert to
320-
-- the right type
321-
in route (Proxy :: Proxy api) context (passToServer subserver param)
316+
let querytext req = parseQueryText $ rawQueryString req
317+
parseParam req =
318+
case lookup paramname (querytext req) of
319+
Nothing -> return Nothing -- param absent from the query string
320+
Just Nothing -> return Nothing -- param present with no value -> Nothing
321+
Just (Just v) ->
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+
331+
in route (Proxy :: Proxy api) context delayed
322332
where paramname = cs $ symbolVal (Proxy :: Proxy sym)
323333

324334
-- | 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)
346356
type ServerT (QueryParams sym a :> api) m =
347357
[a] -> ServerT api m
348358

349-
route Proxy context subserver =
350-
let querytext r = parseQueryText $ rawQueryString r
351-
-- if sym is "foo", we look for query string parameters
352-
-- named "foo" or "foo[]" and call parseQueryParam on the
353-
-- corresponding values
354-
parameters r = filter looksLikeParam (querytext r)
355-
values r = mapMaybe (convert . snd) (parameters r)
356-
in route (Proxy :: Proxy api) context (passToServer subserver values)
357-
where paramname = cs $ symbolVal (Proxy :: Proxy sym)
358-
looksLikeParam (name, _) = name == paramname || name == (paramname <> "[]")
359-
convert Nothing = Nothing
360-
convert (Just v) = parseQueryParamMaybe v
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 <> "[]")
361378

362379
-- | If you use @'QueryFlag' "published"@ in one of the endpoints for your API,
363380
-- this automatically requires your server-side handler to be a function
@@ -439,22 +456,28 @@ instance ( AllCTUnrender list a, HasServer api context
439456
type ServerT (ReqBody list a :> api) m =
440457
a -> ServerT api m
441458

442-
route Proxy context subserver =
443-
route (Proxy :: Proxy api) context (addBodyCheck subserver bodyCheck)
459+
route Proxy context subserver
460+
= route (Proxy :: Proxy api) context $
461+
addBodyCheck subserver ctCheck bodyCheck
444462
where
445-
bodyCheck = withRequest $ \ request -> do
463+
-- Content-Type check, we only lookup we can try to parse the request body
464+
ctCheck = withRequest $ \ request -> do
446465
-- See HTTP RFC 2616, section 7.2.1
447466
-- http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
448467
-- See also "W3C Internet Media Type registration, consistency of use"
449468
-- http://www.w3.org/2001/tag/2002/0129-mime
450469
let contentTypeH = fromMaybe "application/octet-stream"
451470
$ lookup hContentType $ requestHeaders request
452-
mrqbody <- handleCTypeH (Proxy :: Proxy list) (cs contentTypeH)
453-
<$> 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)
454478
case mrqbody of
455-
Nothing -> delayedFailFatal err415
456-
Just (Left e) -> delayedFailFatal err400 { errBody = cs e }
457-
Just (Right v) -> return v
479+
Left e -> delayedFailFatal err400 { errBody = cs e }
480+
Right v -> return v
458481

459482
-- | Make sure the incoming request starts with @"/path"@, strip it and
460483
-- pass the rest of the request path to @api@.

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

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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 three 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
@@ -152,23 +152,36 @@ toApplication ra request respond = ra request routingRespond
152152
-- it does not provide an input for the handler. Method checks
153153
-- are comparatively cheap.
154154
--
155-
-- 3. Body and accept header checks. The request body check can
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.
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.
159164
--
160165
data Delayed env c where
161166
Delayed :: { capturesD :: env -> DelayedIO captures
162167
, methodD :: DelayedIO ()
163168
, authD :: DelayedIO auth
164-
, bodyD :: DelayedIO body
165-
, serverD :: captures -> auth -> body -> Request -> RouteResult c
169+
, acceptD :: DelayedIO ()
170+
, contentD :: DelayedIO contentType
171+
, paramsD :: DelayedIO params
172+
, bodyD :: contentType -> DelayedIO body
173+
, serverD :: captures
174+
-> params
175+
-> auth
176+
-> body
177+
-> Request
178+
-> RouteResult c
166179
} -> Delayed env c
167180

168181
instance Functor (Delayed env) where
169182
fmap f Delayed{..} =
170183
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
172185
, ..
173186
} -- Note [Existential Record Update]
174187

@@ -200,7 +213,7 @@ runDelayedIO m req = transResourceT runRouteResultT $ runReaderT (runDelayedIO'
200213
-- | A 'Delayed' without any stored checks.
201214
emptyDelayed :: RouteResult a -> Delayed env a
202215
emptyDelayed result =
203-
Delayed (const r) r r r (\ _ _ _ _ -> result)
216+
Delayed (const r) r r r r r (const r) (\ _ _ _ _ _ -> result)
204217
where
205218
r = return ()
206219

@@ -225,10 +238,21 @@ addCapture :: Delayed env (a -> b)
225238
addCapture Delayed{..} new =
226239
Delayed
227240
{ 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
229242
, ..
230243
} -- Note [Existential Record Update]
231244

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+
232256
-- | Add a method check to the end of the method block.
233257
addMethodCheck :: Delayed env a
234258
-> DelayedIO ()
@@ -246,24 +270,29 @@ addAuthCheck :: Delayed env (a -> b)
246270
addAuthCheck Delayed{..} new =
247271
Delayed
248272
{ 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
250274
, ..
251275
} -- Note [Existential Record Update]
252276

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.
254281
addBodyCheck :: Delayed env (a -> b)
255-
-> DelayedIO a
282+
-> DelayedIO c -- ^ content type check
283+
-> (c -> DelayedIO a) -- ^ body check
256284
-> Delayed env b
257-
addBodyCheck Delayed{..} new =
285+
addBodyCheck Delayed{..} newContentD newBodyD =
258286
Delayed
259-
{ bodyD = (,) <$> bodyD <*> new
260-
, serverD = \ c a (z, v) req -> ($ v) <$> serverD c a z req
287+
{ contentD = (,) <$> contentD <*> newContentD
288+
, bodyD = \(content, c) -> (,) <$> bodyD content <*> newBodyD c
289+
, serverD = \ c p a (z, v) req -> ($ v) <$> serverD c p a z req
261290
, ..
262291
} -- Note [Existential Record Update]
263292

264293

265-
-- | Add an accept header check to the beginning of the body
266-
-- 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
267296
-- to take a bad body (400) response take precedence over a
268297
-- failed accept check (406). BUT to allow streaming the body,
269298
-- we cannot run the body check and then still backtrack.
@@ -277,7 +306,7 @@ addAcceptCheck :: Delayed env a
277306
-> Delayed env a
278307
addAcceptCheck Delayed{..} new =
279308
Delayed
280-
{ bodyD = new *> bodyD
309+
{ acceptD = acceptD *> new
281310
, ..
282311
} -- Note [Existential Record Update]
283312

@@ -287,7 +316,7 @@ addAcceptCheck Delayed{..} new =
287316
passToServer :: Delayed env (a -> b) -> (Request -> a) -> Delayed env b
288317
passToServer Delayed{..} x =
289318
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
291320
, ..
292321
} -- Note [Existential Record Update]
293322

@@ -301,16 +330,16 @@ runDelayed :: Delayed env a
301330
-> env
302331
-> Request
303332
-> ResourceT IO (RouteResult a)
304-
runDelayed Delayed{..} env req =
305-
runDelayedIO
306-
(do c <- capturesD env
307-
methodD
308-
a <- authD
309-
b <- bodyD
310-
r <- ask
311-
liftRouteResult (serverD c a b r)
312-
)
313-
req
333+
runDelayed Delayed{..} env = runDelayedIO $ do
334+
r <- ask
335+
c <- capturesD env
336+
methodD
337+
a <- authD
338+
acceptD
339+
content <- contentD
340+
p <- paramsD -- Has to be before body parsing, but after content-type checks
341+
b <- bodyD content
342+
liftRouteResult (serverD c p a b r)
314343

315344
-- | Runs a delayed server and the resulting action.
316345
-- Takes a continuation that lets us send a response.

0 commit comments

Comments
 (0)