on master help sign in
Add support for multipart form data (#6)

Close #1, close #4.

commitc7ed1f940deb4c05f4bb9befd7cc8620a845113d authorMark Karpov <markkarpov@opmbx.org> dateSat 7 Jan 2017, 20:24 (10 years ago) parent5e93707
3 files changed+66−5
CHANGELOG.md+9 Network/HTTP/Req.hs+29−5 httpbin-tests/Network/HTTP/ReqSpec.hs+28
CHANGELOG.md+9
@@ -1,3 +1,12 @@
1+## Req 0.2.0
2+
3+* Added support for multipart form data in form of `ReqBodyMultipart` body
4+ option and `reqBodyMultipart` helper function. This also required a change
5+ in type signature of `getRequestContentType`, which now takes `body`, not
6+ `Proxy body` because we need to extract boundary from `body` and put it
7+ into `Content-Type` header. This change, however, shouldn't be too
8+ dangerous for end-users.
9+
110## Req 0.1.0
211
312* Initial release.
Network/HTTP/Req.hs+29−5
@@ -149,6 +149,8 @@ module Network.HTTP.Req
149149 , ReqBodyLbs (..)
150150 , ReqBodyUrlEnc (..)
151151 , FormUrlEncodedParam
152+ , ReqBodyMultipart
153+ , reqBodyMultipart
152154 , HttpBody (..)
153155 , ProvidesBody
154156 , HttpBodyAllowed
@@ -234,6 +236,7 @@ import qualified Data.Text.Encoding as T
234236import qualified Network.Connection as NC
235237import qualified Network.HTTP.Client as L
236238import qualified Network.HTTP.Client.Internal as LI
239+import qualified Network.HTTP.Client.MultipartFormData as LM
237240import qualified Network.HTTP.Client.TLS as L
238241import qualified Network.HTTP.Types as Y
239242
@@ -777,7 +780,7 @@ newtype ReqBodyJson a = ReqBodyJson a
777780
778781instance ToJSON a => HttpBody (ReqBodyJson a) where
779782 getRequestBody (ReqBodyJson a) = L.RequestBodyLBS (A.encode a)
780− getRequestContentType Proxy = pure "application/json; charset=utf-8"
783+ getRequestContentType _ = pure "application/json; charset=utf-8"
781784
782785-- | This body option streams request body from a file. It is expected that
783786-- the file size does not change during the streaming.
@@ -823,7 +826,7 @@ newtype ReqBodyUrlEnc = ReqBodyUrlEnc FormUrlEncodedParam
823826instance HttpBody ReqBodyUrlEnc where
824827 getRequestBody (ReqBodyUrlEnc (FormUrlEncodedParam params)) =
825828 (L.RequestBodyLBS . BB.toLazyByteString) (Y.renderQueryText False params)
826− getRequestContentType Proxy = pure "application/x-www-form-urlencoded"
829+ getRequestContentType _ = pure "application/x-www-form-urlencoded"
827830
828831-- | An opaque monoidal value that allows to collect URL-encoded parameters
829832-- to be wrapped in 'ReqBodyUrlEnc'.
@@ -835,6 +838,27 @@ instance QueryParam FormUrlEncodedParam where
835838 queryParam name mvalue =
836839 FormUrlEncodedParam [(name, toQueryParam <$> mvalue)]
837840
841+-- | Multipart form data. Please consult the
842+-- "Network.HTTP.Client.MultipartFormData" module for how to construct
843+-- parts, then use 'reqBodyMultipart' to create actual request body from the
844+-- parts. 'reqBodyMultipart' is the only way to get a value of type
845+-- 'ReqBodyMultipart', as its constructor is not exported on purpose.
846+
847+data ReqBodyMultipart = ReqBodyMultipart ByteString LI.RequestBody
848+
849+instance HttpBody ReqBodyMultipart where
850+ getRequestBody (ReqBodyMultipart _ body) = body
851+ getRequestContentType (ReqBodyMultipart boundary _) =
852+ pure ("multipart/form-data; boundary=" <> boundary)
853+
854+-- | Create 'ReqBodyMultipart' request body from a collection of 'LM.Part's.
855+
856+reqBodyMultipart :: MonadIO m => [LM.Part] -> m ReqBodyMultipart
857+reqBodyMultipart parts = liftIO $ do
858+ boundary <- LM.webkitBoundary
859+ body <- LM.renderParts boundary parts
860+ return (ReqBodyMultipart boundary body)
861+
838862-- | A type class for things that can be interpreted as HTTP
839863-- 'L.RequestBody'.
840864
@@ -850,8 +874,8 @@ class HttpBody body where
850874 -- header that should be used with particular body option. By default it
851875 -- returns 'Nothing' and so @Content-Type@ is not set.
852876
853− getRequestContentType :: Proxy body -> Maybe ByteString
854− getRequestContentType Proxy = Nothing
877+ getRequestContentType :: body -> Maybe ByteString
878+ getRequestContentType = const Nothing
855879
856880-- | The type function recognizes 'NoReqBody' as having 'NoBody', while any
857881-- other body option 'CanHaveBody'. This forces user to use 'NoReqBody' with
@@ -883,7 +907,7 @@ instance HttpBody body => RequestComponent (Womb "body" body) where
883907 x { L.requestBody = getRequestBody body
884908 , L.requestHeaders =
885909 let old = L.requestHeaders x in
886− case getRequestContentType (Proxy :: Proxy body) of
910+ case getRequestContentType body of
887911 Nothing -> old
888912 Just contentType ->
889913 (Y.hContentType, contentType) : old }
httpbin-tests/Network/HTTP/ReqSpec.hs+28
@@ -60,6 +60,7 @@ import qualified Data.Text as T
6060import qualified Data.Text.Encoding as T
6161import qualified Data.Text.IO as TIO
6262import qualified Network.HTTP.Client as L
63+import qualified Network.HTTP.Client.MultipartFormData as LM
6364import qualified Network.HTTP.Types as Y
6465
6566#if !MIN_VERSION_base(4,8,0)
@@ -143,6 +144,33 @@ spec = do
143144 responseStatusCode r `shouldBe` 200
144145 responseStatusMessage r `shouldBe` "OK"
145146
147+ describe "receiving POST data back (multipart form data)" $
148+ it "works" $ do
149+ body <- reqBodyMultipart
150+ [ LM.partBS "foo" "foo data!"
151+ , LM.partBS "bar" "bar data!" ]
152+ r <- req POST (httpbin /: "post") body jsonResponse mempty
153+ let Just contentType = getRequestContentType body
154+ stripOrigin (responseBody r) `shouldBe` object
155+ [ "args" .= emptyObject
156+ , "json" .= Null
157+ , "data" .= ("" :: Text)
158+ , "url" .= ("https://httpbin.org/post" :: Text)
159+ , "headers" .= object
160+ [ "Content-Type" .= T.decodeUtf8 contentType
161+ , "Accept-Encoding" .= ("gzip" :: Text)
162+ , "Host" .= ("httpbin.org" :: Text)
163+ , "Content-Length" .= ("242" :: Text)
164+ ]
165+ , "files" .= emptyObject
166+ , "form" .= object
167+ [ "foo" .= ("foo data!" :: Text)
168+ , "bar" .= ("bar data!" :: Text) ]
169+ ]
170+ responseHeader r "Content-Type" `shouldBe` return "application/json"
171+ responseStatusCode r `shouldBe` 200
172+ responseStatusMessage r `shouldBe` "OK"
173+
146174 describe "receiving PATCHed file back" $
147175 it "works" $ do
148176 let file :: FilePath