Skip to content

Commit 4b15358

Browse files
authored
Merge pull request #36 from maksbotan/maksbotan/error-message
Improve error messages from FromMultipart
2 parents 4d7bfdb + 1d7c091 commit 4b15358

File tree

4 files changed

+201
-39
lines changed

4 files changed

+201
-39
lines changed

exe/Upload.hs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import Control.Monad
88
import Control.Monad.IO.Class
99
import Network.Socket (withSocketsDo)
1010
import Network.HTTP.Client hiding (Proxy)
11-
import Network.Wai
1211
import Network.Wai.Handler.Warp
1312
import Servant
1413
import Servant.Multipart
@@ -66,14 +65,14 @@ main = do
6665
args <- getArgs
6766
case args of
6867
("run":_) -> withSocketsDo $ do
69-
forkIO startServer
68+
_ <- forkIO startServer
7069
-- we fork the server in a separate thread and send a test
7170
-- request to it from the main thread.
7271
manager <- newManager defaultManagerSettings
7372
boundary <- genBoundary
7473
let burl = BaseUrl Http "localhost" 8080 ""
75-
run cli = runClientM cli (mkClientEnv manager burl)
76-
resp <- run $ client clientApi (boundary, form)
74+
runC cli = runClientM cli (mkClientEnv manager burl)
75+
resp <- runC $ client clientApi (boundary, form)
7776
print resp
7877
_ -> putStrLn "Pass run to run"
7978

servant-multipart.cabal

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ version: 0.11.5
33
synopsis: multipart/form-data (e.g file upload) support for servant
44
description:
55
This package adds support for file upload to the servant ecosystem. It draws
6-
on ideas and code from several people who participated in the (in)famous
7-
[ticket #133](https://github.com/haskell-servant/servant/issues/133) on
6+
on ideas and code from several people who participated in the
7+
(in)famous [ticket #133](https://github.com/haskell-servant/servant/issues/133) on
88
servant's issue tracker.
99

1010
homepage: https://github.com/haskell-servant/servant-multipart#readme
@@ -44,11 +44,11 @@ library
4444
, servant-docs >=0.10 && <0.15
4545
, servant-foreign >=0.15 && <0.16
4646
, servant-server >=0.16 && <0.18
47+
, string-conversions >=0.4.0.1 && <0.5
4748
, wai >=3.2.1.2 && <3.3
4849
, wai-extra >=3.0.24.3 && <3.1
4950

50-
test-suite upload
51-
type: exitcode-stdio-1.0
51+
executable upload
5252
hs-source-dirs: exe
5353
main-is: Upload.hs
5454
default-language: Haskell2010
@@ -67,6 +67,22 @@ test-suite upload
6767
, wai
6868
, warp
6969

70+
test-suite servant-multipart-test
71+
type: exitcode-stdio-1.0
72+
hs-source-dirs: test
73+
main-is: Test.hs
74+
default-language: Haskell2010
75+
build-depends:
76+
base
77+
, bytestring
78+
, http-types
79+
, servant-multipart
80+
, servant-server
81+
, string-conversions
82+
, tasty
83+
, tasty-wai
84+
, text
85+
7086
source-repository head
7187
type: git
72-
location: https://github.com/haskell-servent/servant-multipart
88+
location: https://github.com/haskell-servant/servant-multipart

src/Servant/Multipart.hs

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{-# LANGUAGE CPP #-}
2+
{-# LANGUAGE AllowAmbiguousTypes #-}
23
{-# LANGUAGE DataKinds #-}
34
{-# LANGUAGE TypeFamilies #-}
45
{-# LANGUAGE InstanceSigs #-}
@@ -19,6 +20,7 @@
1920
-- an API. See haddocks of 'MultipartForm' for an introduction.
2021
module Servant.Multipart
2122
( MultipartForm
23+
, MultipartForm'
2224
, MultipartData(..)
2325
, FromMultipart(..)
2426
, lookupInput
@@ -45,20 +47,21 @@ import Control.Monad (replicateM)
4547
import Control.Monad.IO.Class
4648
import Control.Monad.Trans.Resource
4749
import Data.Array (listArray, (!))
48-
import Data.Foldable (foldMap, foldl')
49-
import Data.List (find)
50+
import Data.List (find, foldl')
5051
import Data.Maybe
5152
import Data.Monoid
53+
import Data.String.Conversions (cs)
5254
import Data.Text (Text, unpack)
5355
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
5456
import Data.Typeable
5557
import Network.HTTP.Media.MediaType ((//), (/:))
5658
import Network.Wai
5759
import Network.Wai.Parse
58-
import Servant
60+
import Servant hiding (contentType)
61+
import Servant.API.Modifiers (FoldLenient)
5962
import Servant.Client.Core (HasClient(..), RequestBody(RequestBodySource), setRequestBody)
60-
import Servant.Docs
61-
import Servant.Foreign
63+
import Servant.Docs hiding (samples)
64+
import Servant.Foreign hiding (contentType)
6265
import Servant.Server.Internal
6366
import Servant.Types.SourceT (SourceT(..), source, StepT(..), fromActionStep)
6467
import System.Directory
@@ -150,7 +153,10 @@ import qualified Data.ByteString.Lazy as LBS
150153
-- after your handler has run, if they are still there. It is
151154
-- therefore recommended to move or copy them somewhere in your
152155
-- handler code if you need to keep the content around.
153-
data MultipartForm tag a
156+
type MultipartForm tag a = MultipartForm' '[] tag a
157+
158+
-- | 'MultipartForm' which can be modified with 'Servant.API.Modifiers.Lenient'.
159+
data MultipartForm' (mods :: [*]) tag a
154160

155161
-- | What servant gets out of a @multipart/form-data@ form submission.
156162
--
@@ -209,8 +215,11 @@ deriving instance Eq (MultipartResult tag) => Eq (FileData tag)
209215
deriving instance Show (MultipartResult tag) => Show (FileData tag)
210216

211217
-- | Lookup a file input with the given @name@ attribute.
212-
lookupFile :: Text -> MultipartData tag -> Maybe (FileData tag)
213-
lookupFile iname = find ((==iname) . fdInputName) . files
218+
lookupFile :: Text -> MultipartData tag -> Either String (FileData tag)
219+
lookupFile iname =
220+
maybe (Left $ "File " <> cs iname <> " not found") Right
221+
. find ((==iname) . fdInputName)
222+
. files
214223

215224
-- | Representation for a textual input (any @\<input\>@ type but @file@).
216225
--
@@ -221,8 +230,11 @@ data Input = Input
221230
} deriving (Eq, Show)
222231

223232
-- | Lookup a textual input with the given @name@ attribute.
224-
lookupInput :: Text -> MultipartData tag -> Maybe Text
225-
lookupInput iname = fmap iValue . find ((==iname) . iName) . inputs
233+
lookupInput :: Text -> MultipartData tag -> Either String Text
234+
lookupInput iname =
235+
maybe (Left $ "Field " <> cs iname <> " not found") (Right . iValue)
236+
. find ((==iname) . iName)
237+
. inputs
226238

227239
-- | 'MultipartData' is the type representing
228240
-- @multipart/form-data@ form inputs. Sometimes
@@ -246,10 +258,10 @@ class FromMultipart tag a where
246258
-- in a list of textual inputs and another list for
247259
-- files, try to extract a value of type @a@. When
248260
-- extraction fails, servant errors out with status code 400.
249-
fromMultipart :: MultipartData tag -> Maybe a
261+
fromMultipart :: MultipartData tag -> Either String a
250262

251263
instance FromMultipart tag (MultipartData tag) where
252-
fromMultipart = Just
264+
fromMultipart = Right
253265

254266
-- | Allows you to tell servant how to turn a more structured type
255267
-- into a 'MultipartData', which is what is actually sent by the
@@ -281,11 +293,12 @@ instance ToMultipart tag (MultipartData tag) where
281293
instance ( FromMultipart tag a
282294
, MultipartBackend tag
283295
, LookupContext config (MultipartOptions tag)
296+
, SBoolI (FoldLenient mods)
284297
, HasServer sublayout config )
285-
=> HasServer (MultipartForm tag a :> sublayout) config where
298+
=> HasServer (MultipartForm' mods tag a :> sublayout) config where
286299

287-
type ServerT (MultipartForm tag a :> sublayout) m =
288-
a -> ServerT sublayout m
300+
type ServerT (MultipartForm' mods tag a :> sublayout) m =
301+
If (FoldLenient mods) (Either String a) a -> ServerT sublayout m
289302

290303
#if MIN_VERSION_servant_server(0,12,0)
291304
hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy sublayout) pc nt . s
@@ -299,16 +312,16 @@ instance ( FromMultipart tag a
299312
popts = Proxy :: Proxy (MultipartOptions tag)
300313
multipartOpts = fromMaybe (defaultMultipartOptions pbak)
301314
$ lookupContext popts config
302-
subserver' = addMultipartHandling pbak multipartOpts subserver
315+
subserver' = addMultipartHandling @tag @a @mods pbak multipartOpts subserver
303316

304317
-- | Upon seeing @MultipartForm a :> ...@ in an API type,
305318
-- servant-client will take a parameter of type @(LBS.ByteString, a)@,
306319
-- where the bytestring is the boundary to use (see 'genBoundary'), and
307320
-- replace the request body with the contents of the form.
308321
instance (ToMultipart tag a, HasClient m api, MultipartBackend tag)
309-
=> HasClient m (MultipartForm tag a :> api) where
322+
=> HasClient m (MultipartForm' mods tag a :> api) where
310323

311-
type Client m (MultipartForm tag a :> api) =
324+
type Client m (MultipartForm' mods tag a :> api) =
312325
(LBS.ByteString, a) -> Client m api
313326

314327
clientWithRoute pm _ req (boundary, param) =
@@ -352,7 +365,7 @@ genBoundary = LBS.pack
352365

353366
-- | Given a bytestring for the boundary, turns a `MultipartData` into
354367
-- a 'RequestBody'
355-
multipartToBody :: forall tag.
368+
multipartToBody :: forall tag.
356369
MultipartBackend tag
357370
=> LBS.ByteString
358371
-> MultipartData tag
@@ -373,7 +386,7 @@ multipartToBody boundary mp = RequestBodySource $ files' <> source ["--", bounda
373386
mempty' = SourceT ($ Stop)
374387
crlf = "\r\n"
375388
lencode = LBS.fromStrict . encodeUtf8
376-
renderInput input = renderPart (lencode . iName $ input)
389+
renderInput input = renderPart (lencode . iName $ input)
377390
"text/plain"
378391
""
379392
(source . pure . lencode . iValue $ input)
@@ -420,10 +433,11 @@ check pTag tag = withRequest $ \request -> do
420433
where parseOpts = generalOptions tag
421434

422435
-- Add multipart extraction support to a Delayed.
423-
addMultipartHandling :: forall tag multipart env a. (FromMultipart tag multipart, MultipartBackend tag)
436+
addMultipartHandling :: forall tag multipart (mods :: [*]) env a. (FromMultipart tag multipart, MultipartBackend tag)
437+
=> SBoolI (FoldLenient mods)
424438
=> Proxy tag
425439
-> MultipartOptions tag
426-
-> Delayed env (multipart -> a)
440+
-> Delayed env (If (FoldLenient mods) (Either String multipart) multipart -> a)
427441
-> Delayed env a
428442
addMultipartHandling pTag opts subserver =
429443
addBodyCheck subserver contentCheck bodyCheck
@@ -433,10 +447,11 @@ addMultipartHandling pTag opts subserver =
433447

434448
bodyCheck () = do
435449
mpd <- check pTag opts :: DelayedIO (MultipartData tag)
436-
case fromMultipart mpd of
437-
Nothing -> liftRouteResult $ FailFatal
438-
err400 { errBody = "fromMultipart returned Nothing" }
439-
Just x -> return x
450+
case (sbool :: SBool (FoldLenient mods), fromMultipart @tag @multipart mpd) of
451+
(SFalse, Left msg) -> liftRouteResult $ FailFatal
452+
err400 { errBody = "Could not decode multipart mime body: " <> cs msg }
453+
(SFalse, Right x) -> return x
454+
(STrue, res) -> return $ either (Left . cs) Right res
440455

441456
contentTypeH req = fromMaybe "application/octet-stream" $
442457
lookup "Content-Type" (requestHeaders req)
@@ -502,7 +517,7 @@ instance MultipartBackend Tmp where
502517
type MultipartBackendOptions Tmp = TmpBackendOptions
503518

504519
defaultBackendOptions _ = defaultTmpBackendOptions
505-
-- streams the file from disk
520+
-- streams the file from disk
506521
loadFile _ fp =
507522
SourceT $ \k ->
508523
withFile fp ReadMode $ \hdl ->
@@ -519,7 +534,7 @@ instance MultipartBackend Mem where
519534

520535
defaultBackendOptions _ = ()
521536
loadFile _ = source . pure
522-
backend _ opts _ = lbsBackEnd
537+
backend _ _ _ = lbsBackEnd
523538

524539
-- | Configuration for the temporary file based backend.
525540
--
@@ -563,8 +578,8 @@ instance LookupContext '[] a where
563578

564579
instance {-# OVERLAPPABLE #-}
565580
LookupContext cs a => LookupContext (c ': cs) a where
566-
lookupContext p (c :. cs) =
567-
lookupContext p cs
581+
lookupContext p (_ :. cxts) =
582+
lookupContext p cxts
568583

569584
instance {-# OVERLAPPING #-}
570585
LookupContext cs a => LookupContext (a ': cs) a where

0 commit comments

Comments
 (0)