Skip to content

Commit a6cbb9c

Browse files
authored
Fix Optional ReqBody' (wrap value into Maybe), updated (#1816)
* Port the optional ReqBody' fix from 0.19 to 0.20+ * Create pr-1816 * Update pr-1816
1 parent e07e92a commit a6cbb9c

File tree

3 files changed

+77
-21
lines changed

3 files changed

+77
-21
lines changed

changelog.d/pr-1816

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
synopsis: Make Optional ReqBody wrap its type into Maybe
2+
packages: servant
3+
prs: #1816
4+
issues: #1346
5+
description: {
6+
Make Optional ReqBody wrap its type into Maybe
7+
}

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

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ module Servant.Server.Internal
1515
, module Servant.Server.Internal.ServerError
1616
) where
1717

18+
import Control.Applicative ((<|>))
1819
import Control.Monad
1920
(join, when, unless)
2021
import Control.Monad.Trans
2122
(liftIO, lift)
2223
import Control.Monad.Trans.Resource
2324
(runResourceT, ReleaseKey)
2425
import Data.Acquire
26+
27+
import Data.Bifunctor (first)
2528
import qualified Data.ByteString as B
2629
import qualified Data.ByteString.Builder as BB
2730
import qualified Data.ByteString.Char8 as BC8
@@ -47,8 +50,8 @@ import Network.HTTP.Types hiding
4750
import Network.Socket
4851
(SockAddr)
4952
import Network.Wai
50-
(Application, Request, Response, ResponseReceived, httpVersion, isSecure, lazyRequestBody,
51-
queryString, remoteHost, getRequestBodyChunk, requestHeaders, requestHeaderHost,
53+
(Application, Request, Response, ResponseReceived, RequestBodyLength (..), httpVersion, isSecure, lazyRequestBody,
54+
queryString, remoteHost, getRequestBodyChunk, requestBodyLength, requestHeaders, requestHeaderHost,
5255
requestMethod, responseLBS, responseStream, vault)
5356
import Servant.API
5457
((:<|>) (..), (:>), Accept (..), BasicAuth, Capture',
@@ -802,12 +805,13 @@ instance HasServer RawM context where
802805
-- > server = postBook
803806
-- > where postBook :: Book -> Handler Book
804807
-- > postBook book = ...insert into your db...
805-
instance ( AllCTUnrender list a, HasServer api context, SBoolI (FoldLenient mods)
808+
instance ( AllCTUnrender list a, HasServer api context
809+
, SBoolI (FoldRequired mods), SBoolI (FoldLenient mods)
806810
, HasContextEntry (MkContextWithErrorFormatter context) ErrorFormatters
807811
) => HasServer (ReqBody' mods list a :> api) context where
808812

809813
type ServerT (ReqBody' mods list a :> api) m =
810-
If (FoldLenient mods) (Either String a) a -> ServerT api m
814+
RequestArgument mods a -> ServerT api m
811815

812816
hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
813817

@@ -819,25 +823,44 @@ instance ( AllCTUnrender list a, HasServer api context, SBoolI (FoldLenient mods
819823
formatError = bodyParserErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context)
820824

821825
-- Content-Type check, we only lookup we can try to parse the request body
822-
ctCheck = withRequest $ \ request -> do
826+
ctCheck = withRequest $ \ request ->
823827
-- See HTTP RFC 2616, section 7.2.1
824828
-- http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
825829
-- See also "W3C Internet Media Type registration, consistency of use"
826830
-- http://www.w3.org/2001/tag/2002/0129-mime
827-
let contentTypeH = fromMaybe "application/octet-stream"
828-
$ lookup hContentType $ requestHeaders request
829-
case canHandleCTypeH (Proxy :: Proxy list) (BSL.fromStrict contentTypeH) :: Maybe (BSL.ByteString -> Either String a) of
830-
Nothing -> delayedFail err415
831-
Just f -> return f
832-
833-
-- Body check, we get a body parsing functions as the first argument.
834-
bodyCheck f = withRequest $ \ request -> do
835-
mrqbody <- f <$> liftIO (lazyRequestBody request)
836-
case sbool :: SBool (FoldLenient mods) of
837-
STrue -> return mrqbody
838-
SFalse -> case mrqbody of
839-
Left e -> delayedFailFatal $ formatError rep request e
840-
Right v -> return v
831+
let contentTypeHMaybe = lookup hContentType $ requestHeaders request
832+
contentTypeH = fromMaybe "application/octet-stream" contentTypeHMaybe
833+
canHandleContentTypeH :: Maybe (BSL.ByteString -> Either String a)
834+
canHandleContentTypeH = canHandleCTypeH (Proxy :: Proxy list) (BSL.fromStrict contentTypeH)
835+
836+
-- In case ReqBody' is Optional and neither request body nor Content-Type header was provided.
837+
noOptionalReqBody =
838+
case (sbool :: SBool (FoldRequired mods), contentTypeHMaybe, requestBodyLength request) of
839+
(SFalse, Nothing, KnownLength 0) -> Just . const $ Left "This value does not matter (it is ignored)"
840+
_ -> Nothing
841+
in
842+
case canHandleContentTypeH <|> noOptionalReqBody of
843+
Nothing -> delayedFail err415
844+
Just f -> return f
845+
846+
bodyCheck f = withRequest $ \ request ->
847+
let
848+
hasReqBody =
849+
case requestBodyLength request of
850+
KnownLength 0 -> False
851+
_ -> True
852+
853+
serverErr :: String -> ServerError
854+
serverErr = formatError rep request
855+
in
856+
fmap f (liftIO $ lazyRequestBody request) >>=
857+
case (sbool :: SBool (FoldRequired mods), sbool :: SBool (FoldLenient mods), hasReqBody) of
858+
(STrue, STrue, _) -> return . first T.pack
859+
(STrue, SFalse, _) -> either (delayedFailFatal . serverErr) return
860+
(SFalse, STrue, False) -> return . either (const Nothing) (Just . Right)
861+
(SFalse, SFalse, False) -> return . either (const Nothing) Just
862+
(SFalse, STrue, True) -> return . Just . first T.pack
863+
(SFalse, SFalse, True) -> either (delayedFailFatal . serverErr) (return . Just)
841864

842865
instance
843866
( FramingUnrender framing, FromSourceIO chunk a, MimeUnrender ctype chunk

servant-server/test/Servant/ServerSpec.hs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ import Servant.API
6262
IsSecure (..), JSON, Lenient, NoContent (..), NoContentVerb,
6363
NoFraming, OctetStream, Patch, PlainText, Post, Put,
6464
QueryFlag, QueryParam, QueryParams, QueryString, Raw,
65-
RemoteHost, ReqBody, SourceIO, StdMethod (..), Stream, Strict,
65+
RemoteHost, ReqBody, ReqBody', SourceIO, StdMethod (..), Stream, Strict,
6666
UVerb, Union, Verb, WithStatus (..), addHeader, addHeader')
6767
import Servant.API.QueryString (FromDeepQuery(..))
6868
import Servant.Server
@@ -580,6 +580,7 @@ fragmentSpec = do
580580
------------------------------------------------------------------------------
581581
type ReqBodyApi = ReqBody '[JSON] Person :> Post '[JSON] Person
582582
:<|> "blah" :> ReqBody '[JSON] Person :> Put '[JSON] Integer
583+
:<|> "meh" :> ReqBody' '[Optional, Strict] '[JSON] Person :> Put '[JSON] Integer
583584

584585
reqBodyApi :: Proxy ReqBodyApi
585586
reqBodyApi = Proxy
@@ -588,7 +589,7 @@ reqBodySpec :: Spec
588589
reqBodySpec = describe "Servant.API.ReqBody" $ do
589590

590591
let server :: Server ReqBodyApi
591-
server = return :<|> return . age
592+
server = return :<|> return . age :<|> return . maybe 0 age
592593
mkReq method x = THW.request method x
593594
[(hContentType, "application/json;charset=utf-8")]
594595

@@ -603,6 +604,31 @@ reqBodySpec = describe "Servant.API.ReqBody" $ do
603604
it "responds with 415 if the request body media type is unsupported" $ THW.request methodPost "/"
604605
[(hContentType, "application/nonsense")] "" `shouldRespondWith` 415
605606

607+
describe "optional request body" $ do
608+
it "request without body succeeds" $ do
609+
THW.request methodPut "/meh" [] mempty `shouldRespondWith` 200
610+
611+
it "request without body responds with proper default value" $ do
612+
response <- THW.request methodPut "/meh" [] mempty
613+
liftIO $ simpleBody response `shouldBe` encode (0 :: Integer)
614+
615+
it "responds with 415 if the request body media type is unsupported" $ do
616+
THW.request methodPut "/meh" [(hContentType, "application/nonsense")]
617+
(encode alice) `shouldRespondWith` 415
618+
THW.request methodPut "/meh" [(hContentType, "application/octet-stream")]
619+
(encode alice) `shouldRespondWith` 415
620+
621+
it "request without body and with content-type header succeeds" $ do
622+
mkReq methodPut "/meh" mempty `shouldRespondWith` 200
623+
624+
it "request without body and with content-type header returns default value" $ do
625+
response <- mkReq methodPut "/meh" mempty
626+
liftIO $ simpleBody response `shouldBe` encode (0 :: Integer)
627+
628+
it "optional request body can be provided" $ do
629+
response <- mkReq methodPut "/meh" (encode alice)
630+
liftIO $ simpleBody response `shouldBe` encode (age alice)
631+
606632
-- }}}
607633
------------------------------------------------------------------------------
608634
-- * headerSpec {{{

0 commit comments

Comments
 (0)