packages feed

req 3.8.0 → 3.13.4

raw patch · 7 files changed

Files

CHANGELOG.md view
@@ -1,3 +1,63 @@+## Req 3.13.4++* Fixed empty ciphersuite list when compiling against `tls < 2.0.6`+  (see [PR 175](https://github.com/mrkkrp/req/pull/175)). As a side effect, now+  compatible with older versions of `crypton-connection` (>= 0.3).++## Req 3.13.3++* Works with `crypton-connection-0.4` and newer.++## Req 3.13.2++* Disable the problematic `httpbin-tests` test suite by default. Only enable+  it when the `dev` flag is enabled. In that case it is expected that an+  httpbin server is run locally at `localhost:1234`.++## Req 3.13.1++* Switched the non-pure test suite to use https://httpbun.org instead of+  https://httpbin.org since the latter proved to be highly unreliable+  lately.++* Switched from `connection` to `crypton-connection`.++* Builds with GHC 9.6.1.++## Req 3.13.0++* Add `headerRedacted` function to add header fields, which will be with+  redacted values on print.++## Req 3.12.0++* Add `isStatusCodeException` function.+* Add `instance HttpResponse (Network.HTTP.Client.Response ())`.++## Req 3.11.0++* Add the `queryParamToList` method to the `QueryParam` type class.+* Add the `formToQuery` function. [Issue 126](https://github.com/mrkkrp/req/issues/126).+* Add `FromForm` instances (in the `Web.FormUrlEncoded` module) to the+  `Option` and `FormUrlEncodedParam` types.++## Req 3.10.0++* Add `MonadHttp` instances for `transformers` types.++## Req 3.9.2++* The test suite works with `aeson-2.x.x.x`.++## Req 3.9.1++* Builds with GHC 9.0.++## Req 3.9.0++* The `useHttpURI` and `useHttpsURI` functions now preserve trailing+  slashes.+ ## Req 3.8.0  * Adjusted the value of the `httpConfigRetryJudgeException` field of
Network/HTTP/Req.hs view
@@ -1,20 +1,9 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-}-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE DeriveLift #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RoleAnnotations #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskellQuotes #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-}@@ -30,61 +19,40 @@ -- -- The documentation below is structured in such a way that the most -- important information is presented first: you learn how to do HTTP--- requests, how to embed them in any monad you have, and then it gives you+-- requests, how to embed them in the monad you have, and then it gives you -- details about less-common things you may want to know about. The -- documentation is written with sufficient coverage of details and -- examples, and it's designed to be a complete tutorial on its own. ----- /(A modest intro goes here, click on 'req' to start making requests.)/--- -- === About the library ----- Req is an easy-to-use, type-safe, expandable, high-level HTTP client--- library that just works without any fooling around.------ What does the phrase “easy-to-use” mean? It means that the library is--- designed to be beginner-friendly so it's simple to add to your monad--- stack, intuitive to work with, well-documented, and does not get in your--- way. Doing HTTP requests is a common task and Haskell library for this--- should be very approachable and clear to beginners, thus certain--- compromises were made. For example, one cannot currently modify--- 'L.ManagerSettings' of the default manager because the library always--- uses the same implicit global manager for simplicity and maximal--- connection sharing. There is a way to use your own manager with different--- settings, but it requires a bit more typing.+-- Req is an HTTP client library that attempts to be easy-to-use, type-safe,+-- and expandable. ----- “Type-safe” means that the library is protective and eliminates certain--- classes of errors. For example, we have correct-by-construction 'Url's,--- it's guaranteed that the user does not send the request body when using--- methods like 'GET' or 'OPTIONS', and the amount of implicit assumptions--- is minimized by making the user specify his\/her intentions in an--- explicit form (for example, it's not possible to avoid specifying the--- body or method of request). Authentication methods that assume HTTPS--- force the user to use HTTPS at the type level. The library also carefully--- hides underlying types from the lower-level @http-client@ package because--- those types are not safe enough (for example 'L.Request' is an instance--- of 'Data.String.IsString' and, if it's malformed, it will blow up at--- run-time).+-- “Easy-to-use” means that the library is designed to be beginner-friendly+-- so it's simple to add to your monad stack, intuitive to work with,+-- well-documented, and does not get in your way. Doing HTTP requests is a+-- common task and a Haskell library for this should be approachable and+-- clear to beginners, thus certain compromises were made. For example, one+-- cannot currently modify 'L.ManagerSettings' of the default manager+-- because the library always uses the same implicit global manager for+-- simplicity and maximal connection sharing. There is a way to use your own+-- manager with different settings, but it requires more typing. ----- “Expandable” refers to the ability of the library to be expanded without--- having to resort to ugly hacking. For example, it's possible to define--- your own HTTP methods, create new ways to construct the body of a--- request, create new authorization options, perform a request in a--- different way, and create your own methods to parse and represent a--- response. As the user extends the library to satisfy his\/her special--- needs, the new solutions will work just like the built-ins. However, all--- of the common cases are also covered by the library out-of-the-box.+-- “Type-safe” means that the library tries to eliminate certain classes of+-- errors. For example, we have correct-by-construction URLs; it is+-- guaranteed that the user does not send the request body when using+-- methods like GET or OPTIONS, and the amount of implicit assumptions is+-- minimized by making the user specify their intentions in an explicit+-- form. For example, it's not possible to avoid specifying the body or the+-- method of a request. Authentication methods that assume HTTPS force the+-- user to use HTTPS at the type level. ----- “High-level” means that there are less details to worry about. The--- library is a result of my experiences as a Haskell consultant. Working--- for several clients, who had very different projects, showed me that the--- library should adapt easily to any particular style of writing Haskell--- applications. For example, some people prefer throwing exceptions, while--- others are concerned with purity. Just define 'handleHttpException'--- accordingly when making your monad instance of 'MonadHttp' and it will--- play together seamlessly. Finally, the library cuts down boilerplate--- considerably, and helps you write concise, easy to read, and maintainable--- code.+-- “Expandable” refers to the ability to create new components without+-- having to resort to hacking. For example, it's possible to define your+-- own HTTP methods, create new ways to construct the body of a request,+-- create new authorization options, perform a request in a different way,+-- and create your own methods to parse a response. -- -- === Using with other libraries --@@ -176,11 +144,13 @@     -- $query-parameters     (=:),     queryFlag,+    formToQuery,     QueryParam (..),      -- *** Headers     header,     attachHeader,+    headerRedacted,      -- *** Cookies     -- $cookies@@ -227,58 +197,78 @@      -- * Other     HttpException (..),+    isStatusCodeException,     CanHaveBody (..),     Scheme (..),   ) where -import qualified Blaze.ByteString.Builder as BB+import Blaze.ByteString.Builder qualified as BB import Control.Applicative import Control.Arrow (first, second) import Control.Exception hiding (Handler (..), TypeError)+import Control.Monad (guard, void, (>=>)) import Control.Monad.Base import Control.Monad.Catch (Handler (..), MonadCatch, MonadMask, MonadThrow) import Control.Monad.IO.Class import Control.Monad.IO.Unlift-import Control.Monad.Reader+import Control.Monad.Reader (ReaderT (ReaderT), ask, lift, runReaderT)+import Control.Monad.Trans.Accum (AccumT)+import Control.Monad.Trans.Cont (ContT) import Control.Monad.Trans.Control+import Control.Monad.Trans.Except (ExceptT)+import Control.Monad.Trans.Identity (IdentityT)+import Control.Monad.Trans.Maybe (MaybeT)+import Control.Monad.Trans.RWS.CPS qualified as RWS.CPS+import Control.Monad.Trans.RWS.Lazy qualified as RWS.Lazy+import Control.Monad.Trans.RWS.Strict qualified as RWS.Strict+import Control.Monad.Trans.Select (SelectT)+import Control.Monad.Trans.State.Lazy qualified as State.Lazy+import Control.Monad.Trans.State.Strict qualified as State.Strict+import Control.Monad.Trans.Writer.CPS qualified as Writer.CPS+import Control.Monad.Trans.Writer.Lazy qualified as Writer.Lazy+import Control.Monad.Trans.Writer.Strict qualified as Writer.Strict import Control.Retry import Data.Aeson (FromJSON (..), ToJSON (..))-import qualified Data.Aeson as A+import Data.Aeson qualified as A import Data.ByteString (ByteString)-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as BL-import qualified Data.CaseInsensitive as CI+import Data.ByteString qualified as B+import Data.ByteString.Lazy qualified as BL+import Data.CaseInsensitive qualified as CI import Data.Data (Data)+import Data.Default.Class (def) import Data.Function (on) import Data.IORef import Data.Kind (Constraint, Type) import Data.List (foldl', nubBy) import Data.List.NonEmpty (NonEmpty (..))-import qualified Data.List.NonEmpty as NE+import Data.List.NonEmpty qualified as NE import Data.Maybe (fromMaybe) import Data.Proxy-import Data.Semigroup hiding (Option, option)+import Data.Semigroup (Endo (..))+import Data.Set qualified as S import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.Encoding as T+import Data.Text qualified as T+import Data.Text.Encoding qualified as T import Data.Typeable (Typeable, cast) import GHC.Generics import GHC.TypeLits-import qualified Language.Haskell.TH as TH-import qualified Language.Haskell.TH.Quote as TH-import qualified Language.Haskell.TH.Syntax as TH-import qualified Network.Connection as NC-import qualified Network.HTTP.Client as L-import qualified Network.HTTP.Client.Internal as LI-import qualified Network.HTTP.Client.MultipartFormData as LM-import qualified Network.HTTP.Client.TLS as L-import qualified Network.HTTP.Types as Y+import Language.Haskell.TH qualified as TH+import Language.Haskell.TH.Quote qualified as TH+import Language.Haskell.TH.Syntax qualified as TH+import Network.Connection qualified as NC+import Network.HTTP.Client qualified as L+import Network.HTTP.Client.Internal qualified as LI+import Network.HTTP.Client.MultipartFormData qualified as LM+import Network.HTTP.Client.TLS qualified as L+import Network.HTTP.Types qualified as Y import System.IO.Unsafe (unsafePerformIO) import Text.URI (URI)-import qualified Text.URI as URI-import qualified Text.URI.QQ as QQ-import qualified Web.Authenticate.OAuth as OAuth+import Text.URI qualified as URI+import Text.URI.QQ qualified as QQ+import Web.Authenticate.OAuth qualified as OAuth+import Web.FormUrlEncoded (FromForm (..), ToForm (..))+import Web.FormUrlEncoded qualified as Form import Web.HttpApiData (ToHttpApiData (..))  ----------------------------------------------------------------------------@@ -303,7 +293,7 @@ -- -- @body@ is a body option such as 'NoReqBody' or 'ReqBodyJson'. The -- tutorial has a section about HTTP bodies, but usage is very--- straightforward and should be clear from the examples below.+-- straightforward and should be clear from the examples. -- -- @response@ is a type hint how to make and interpret response of an HTTP -- request. Out-of-the-box it can be the following:@@ -499,7 +489,7 @@ -- -- @since 3.7.0 reqHandler ::-  MonadHttp m =>+  (MonadHttp m) =>   -- | How to get final result from a 'L.Response'   (L.Response L.BodyReader -> IO b) ->   -- | 'L.Request' to perform@@ -588,22 +578,22 @@           nubHeaders             <> getRequestMod options             <> getRequestMod config-            <> getRequestMod (Womb body :: Womb "body" body)+            <> getRequestMod (Tagged body :: Tagged "body" body)             <> getRequestMod url-            <> getRequestMod (Womb method :: Womb "method" method)+            <> getRequestMod (Tagged method :: Tagged "method" method)   request <- finalizeRequest options request'   withReqManager (m request)  -- | Perform an action using the global implicit 'L.Manager' that the rest -- of the library uses. This allows to reuse connections that the -- 'L.Manager' controls.-withReqManager :: MonadIO m => (L.Manager -> m a) -> m a+withReqManager :: (MonadIO m) => (L.Manager -> m a) -> m a withReqManager m = liftIO (readIORef globalManager) >>= m --- | Global 'L.Manager' that 'req' uses. Here we just go with the default--- settings, so users don't need to deal with this manager stuff at all, but--- when we create a request, instance 'HttpConfig' can affect the default--- settings via 'getHttpConfig'.+-- | The global 'L.Manager' that 'req' uses. Here we just go with the+-- default settings, so users don't need to deal with this manager stuff at+-- all, but when we create a request, instance 'HttpConfig' can affect the+-- default settings via 'getHttpConfig'. -- -- A note about safety, in case 'unsafePerformIO' looks suspicious to you. -- The value of 'globalManager' is named and lives on top level. This means@@ -617,7 +607,7 @@   let settings =         L.mkManagerSettingsContext           (Just context)-          (NC.TLSSettingsSimple False False False)+          def           Nothing   manager <- L.newManager settings   newIORef manager@@ -634,13 +624,13 @@ -- When writing a library, keep your API polymorphic in terms of -- 'MonadHttp', only define instance of 'MonadHttp' in final application. -- Another option is to use a @newtype@-wrapped monad stack and define--- 'MonadHttp' for it. As of version /0.4.0/, the 'Req' monad that follows--- this strategy is provided out-of-the-box (see below).+-- 'MonadHttp' for it. As of the version /0.4.0/, the 'Req' monad that+-- follows this strategy is provided out-of-the-box (see below).  -- | A type class for monads that support performing HTTP requests. -- Typically, you only need to define the 'handleHttpException' method -- unless you want to tweak 'HttpConfig'.-class MonadIO m => MonadHttp m where+class (MonadIO m) => MonadHttp m where   -- | This method describes how to deal with 'HttpException' that was   -- caught by the library. One option is to re-throw it if you are OK with   -- exceptions, but if you prefer working with something like@@ -648,17 +638,16 @@   -- 'Control.Monad.Except.throwError'.   handleHttpException :: HttpException -> m a -  -- | Return 'HttpConfig' to be used when performing HTTP requests. Default-  -- implementation returns its 'def' value, which is described in the-  -- documentation for the type. Common usage pattern with manually defined-  -- 'getHttpConfig' is to return some hard-coded value, or a value+  -- | Return the 'HttpConfig' to be used when performing HTTP requests.+  -- Default implementation returns its 'def' value, which is described in+  -- the documentation for the type. Common usage pattern with manually+  -- defined 'getHttpConfig' is to return some hard-coded value, or a value   -- extracted from 'Control.Monad.Reader.MonadReader' if a more flexible   -- approach to configuration is desirable.   getHttpConfig :: m HttpConfig   getHttpConfig = return defaultHttpConfig --- | 'HttpConfig' contains general and default settings to be used when--- making HTTP requests.+-- | 'HttpConfig' contains settings to be used when making HTTP requests. data HttpConfig = HttpConfig   { -- | Proxy to use. By default values of @HTTP_PROXY@ and @HTTPS_PROXY@     -- environment variables are respected, this setting overwrites them.@@ -702,8 +691,8 @@     -- | The retry policy to use for request retrying. By default 'def' is     -- used (see 'RetryPolicyM').     ---    -- __Note__: signature of this function was changed in the version-    -- /1.0.0/.+    -- __Note__: signature of this function was changed to disallow 'IO' in+    -- version /1.0.0/ and then changed back to its current form in /3.1.0/.     --     -- @since 0.3.0     httpConfigRetryPolicy :: RetryPolicyM IO,@@ -725,11 +714,11 @@     -- | Max length of preview fragment of response body.     --     -- @since 3.6.0-    httpConfigBodyPreviewLength :: forall a. Num a => a+    httpConfigBodyPreviewLength :: forall a. (Num a) => a   }   deriving (Typeable) --- | Default value of 'HttpConfig'.+-- | The default value of 'HttpConfig'. -- -- @since 2.0.0 defaultHttpConfig :: HttpConfig@@ -773,7 +762,7 @@         LI.requestManagerOverride = httpConfigAltManager       } --- | A monad that allows to run 'req' in any 'IO'-enabled monad without+-- | A monad that allows us to run 'req' in any 'IO'-enabled monad without -- having to define new instances. -- -- @since 0.4.0@@ -809,12 +798,87 @@   handleHttpException = Req . lift . throwIO   getHttpConfig = Req ask +-- | @since 3.10.0+instance (MonadHttp m, Monoid w) => MonadHttp (AccumT w m) where+  handleHttpException = lift . handleHttpException+  getHttpConfig = lift getHttpConfig++-- | @since 3.10.0+instance (MonadHttp m) => MonadHttp (ContT r m) where+  handleHttpException = lift . handleHttpException+  getHttpConfig = lift getHttpConfig++-- | @since 3.10.0+instance (MonadHttp m) => MonadHttp (ExceptT e m) where+  handleHttpException = lift . handleHttpException+  getHttpConfig = lift getHttpConfig++-- | @since 3.10.0+instance (MonadHttp m) => MonadHttp (IdentityT m) where+  handleHttpException = lift . handleHttpException+  getHttpConfig = lift getHttpConfig++-- | @since 3.10.0+instance (MonadHttp m) => MonadHttp (MaybeT m) where+  handleHttpException = lift . handleHttpException+  getHttpConfig = lift getHttpConfig++-- | @since 3.10.0+instance (MonadHttp m) => MonadHttp (ReaderT r m) where+  handleHttpException = lift . handleHttpException+  getHttpConfig = lift getHttpConfig++-- | @since 3.10.0+instance (MonadHttp m, Monoid w) => MonadHttp (RWS.CPS.RWST r w s m) where+  handleHttpException = lift . handleHttpException+  getHttpConfig = lift getHttpConfig++-- | @since 3.10.0+instance (MonadHttp m, Monoid w) => MonadHttp (RWS.Lazy.RWST r w s m) where+  handleHttpException = lift . handleHttpException+  getHttpConfig = lift getHttpConfig++-- | @since 3.10.0+instance (MonadHttp m, Monoid w) => MonadHttp (RWS.Strict.RWST r w s m) where+  handleHttpException = lift . handleHttpException+  getHttpConfig = lift getHttpConfig++-- | @since 3.10.0+instance (MonadHttp m) => MonadHttp (SelectT r m) where+  handleHttpException = lift . handleHttpException+  getHttpConfig = lift getHttpConfig++-- | @since 3.10.0+instance (MonadHttp m) => MonadHttp (State.Lazy.StateT s m) where+  handleHttpException = lift . handleHttpException+  getHttpConfig = lift getHttpConfig++-- | @since 3.10.0+instance (MonadHttp m) => MonadHttp (State.Strict.StateT s m) where+  handleHttpException = lift . handleHttpException+  getHttpConfig = lift getHttpConfig++-- | @since 3.10.0+instance (MonadHttp m, Monoid w) => MonadHttp (Writer.CPS.WriterT w m) where+  handleHttpException = lift . handleHttpException+  getHttpConfig = lift getHttpConfig++-- | @since 3.10.0+instance (MonadHttp m, Monoid w) => MonadHttp (Writer.Lazy.WriterT w m) where+  handleHttpException = lift . handleHttpException+  getHttpConfig = lift getHttpConfig++-- | @since 3.10.0+instance (MonadHttp m, Monoid w) => MonadHttp (Writer.Strict.WriterT w m) where+  handleHttpException = lift . handleHttpException+  getHttpConfig = lift getHttpConfig+ -- | Run a computation in the 'Req' monad with the given 'HttpConfig'. In--- case of exceptional situation an 'HttpException' will be thrown.+-- the case of an exceptional situation an 'HttpException' will be thrown. -- -- @since 0.4.0 runReq ::-  MonadIO m =>+  (MonadIO m) =>   -- | 'HttpConfig' to use   HttpConfig ->   -- | Computation to run@@ -919,7 +983,7 @@   -- | Return name of the method as a 'ByteString'.   httpMethodName :: Proxy a -> ByteString -instance HttpMethod method => RequestComponent (Womb "method" method) where+instance (HttpMethod method) => RequestComponent (Tagged "method" method) where   getRequestMod _ = Endo $ \x ->     x {L.method = httpMethodName (Proxy :: Proxy method)} @@ -966,18 +1030,15 @@ type role Url nominal  -- With template-haskell >=2.15 and text >=1.2.4 Lift can be derived, however--- the derived lift forgets the type of scheme.-instance Typeable scheme => TH.Lift (Url scheme) where+-- the derived lift forgets the type of the scheme.+instance (Typeable scheme) => TH.Lift (Url scheme) where   lift url =     TH.dataToExpQ (fmap liftText . cast) url `TH.sigE` case url of       Url Http _ -> [t|Url 'Http|]       Url Https _ -> [t|Url 'Https|]     where       liftText t = TH.AppE (TH.VarE 'T.pack) <$> TH.lift (T.unpack t)--#if MIN_VERSION_template_haskell(2,16,0)-  liftTyped url = TH.TExp <$> TH.lift url-#endif+  liftTyped = TH.Code . TH.unsafeTExpCoerce . TH.lift  -- | Given host name, produce a 'Url' which has “http” as its scheme and -- empty path. This also sets port to @80@.@@ -989,14 +1050,14 @@ https :: Text -> Url 'Https https = Url Https . pure --- | Grow given 'Url' appending a single path segment to it. Note that the+-- | Grow a given 'Url' appending a single path segment to it. Note that the -- path segment can be of any type that is an instance of 'ToHttpApiData'. infixl 5 /~ -(/~) :: ToHttpApiData a => Url scheme -> a -> Url scheme+(/~) :: (ToHttpApiData a) => Url scheme -> a -> Url scheme Url secure path /~ segment = Url secure (NE.cons (toUrlPiece segment) path) --- | Type-constrained version of @('/~')@ to remove ambiguity in the cases+-- | A type-constrained version of @('/~')@ to remove ambiguity in the cases -- when next URL piece is a 'Data.Text.Text' literal. infixl 5 /: @@ -1029,8 +1090,7 @@   urlHead <- http <$> uriHost uri   let url = case URI.uriPath uri of         Nothing -> urlHead-        Just (_, xs) ->-          foldl' (/:) urlHead (URI.unRText <$> NE.toList xs)+        Just uriPath -> uriPathToUrl uriPath urlHead   return (url, uriOptions uri)  -- | Just like 'useHttpURI', but expects the “https” scheme.@@ -1042,10 +1102,21 @@   urlHead <- https <$> uriHost uri   let url = case URI.uriPath uri of         Nothing -> urlHead-        Just (_, xs) ->-          foldl' (/:) urlHead (URI.unRText <$> NE.toList xs)+        Just uriPath -> uriPathToUrl uriPath urlHead   return (url, uriOptions uri) +-- | Convert URI path to a 'Url'. Internal.+--+-- @since 3.9.0+uriPathToUrl ::+  (Bool, NonEmpty (URI.RText 'URI.PathPiece)) ->+  Url scheme ->+  Url scheme+uriPathToUrl (trailingSlash, xs) urlHead =+  if trailingSlash then path /: T.empty else path+  where+    path = foldl' (/:) urlHead (URI.unRText <$> NE.toList xs)+ -- | A combination of 'useHttpURI' and 'useHttpsURI' for cases when scheme -- is not known in advance. --@@ -1162,21 +1233,21 @@ instance HttpBody NoReqBody where   getRequestBody NoReqBody = L.RequestBodyBS B.empty --- | This body option allows to use a JSON object as request body—probably--- the most popular format right now. Just wrap a data type that is an--- instance of 'ToJSON' type class and you are done: it will be converted to--- JSON and inserted as request body.+-- | This body option allows us to use a JSON object as the request+-- body—probably the most popular format right now. Just wrap a data type+-- that is an instance of 'ToJSON' type class and you are done: it will be+-- converted to JSON and inserted as request body. -- -- This body option sets the @Content-Type@ header to @\"application/json; -- charset=utf-8\"@ value. newtype ReqBodyJson a = ReqBodyJson a -instance ToJSON a => HttpBody (ReqBodyJson a) where+instance (ToJSON a) => HttpBody (ReqBodyJson a) where   getRequestBody (ReqBodyJson a) = L.RequestBodyLBS (A.encode a)   getRequestContentType _ = pure "application/json; charset=utf-8"  -- | This body option streams request body from a file. It is expected that--- the file size does not change during the streaming.+-- the file size does not change during streaming. -- -- Using of this body option does not set the @Content-Type@ header. newtype ReqBodyFile = ReqBodyFile FilePath@@ -1201,12 +1272,11 @@ instance HttpBody ReqBodyLbs where   getRequestBody (ReqBodyLbs bs) = L.RequestBodyLBS bs --- | Form URL-encoded body. This can hold a collection of parameters which--- are encoded similarly to query parameters at the end of query string,--- with the only difference that they are stored in request body. The--- similarity is reflected in the API as well, as you can use the same--- combinators you would use to add query parameters: @('=:')@ and--- 'queryFlag'.+-- | URL-encoded body. This can hold a collection of parameters which are+-- encoded similarly to query parameters at the end of query string, with+-- the only difference that they are stored in request body. The similarity+-- is reflected in the API as well, as you can use the same combinators you+-- would use to add query parameters: @('=:')@ and 'queryFlag'. -- -- This body option sets the @Content-Type@ header to -- @\"application/x-www-form-urlencoded\"@ value.@@ -1225,7 +1295,14 @@ instance QueryParam FormUrlEncodedParam where   queryParam name mvalue =     FormUrlEncodedParam [(name, toQueryParam <$> mvalue)]+  queryParamToList (FormUrlEncodedParam p) = p +-- | Use 'formToQuery'.+--+-- @since 3.11.0+instance FromForm FormUrlEncodedParam where+  fromForm = Right . formToQuery+ -- | Multipart form data. Please consult the -- "Network.HTTP.Client.MultipartFormData" module for how to construct -- parts, then use 'reqBodyMultipart' to create actual request body from the@@ -1264,7 +1341,7 @@ -- | Create 'ReqBodyMultipart' request body from a collection of 'LM.Part's. -- -- @since 0.2.0-reqBodyMultipart :: MonadIO m => [LM.Part] -> m ReqBodyMultipart+reqBodyMultipart :: (MonadIO m) => [LM.Part] -> m ReqBodyMultipart reqBodyMultipart parts = liftIO $ do   boundary <- LM.webkitBoundary   body <- LM.renderParts boundary parts@@ -1292,9 +1369,6 @@ -- | This type function allows any HTTP body if method says it -- 'CanHaveBody'. When the method says it should have 'NoBody', the only -- body option to use is 'NoReqBody'.------ __Note__: users of GHC 8.0.1 and later will see a slightly more friendly--- error message when method does not allow a body and body is provided. type family   HttpBodyAllowed     (allowsBody :: CanHaveBody)@@ -1307,8 +1381,8 @@     TypeError       ('Text "This HTTP method does not allow attaching a request body.") -instance HttpBody body => RequestComponent (Womb "body" body) where-  getRequestMod (Womb body) = Endo $ \x ->+instance (HttpBody body) => RequestComponent (Tagged "body" body) where+  getRequestMod (Tagged body) = Endo $ \x ->     x       { L.requestBody = getRequestBody body,         L.requestHeaders =@@ -1337,8 +1411,8 @@   = Option (Endo (Y.QueryText, L.Request)) (Maybe (L.Request -> IO L.Request))  -- NOTE 'QueryText' is just [(Text, Maybe Text)], we keep it along with--- Request to avoid appending to existing query string in request every--- time new parameter is added. Additional Maybe (L.Request -> IO+-- Request to avoid appending to an existing query string in request every+-- time new parameter is added. The additional Maybe (L.Request -> IO -- L.Request) is a finalizer that will be applied after all other -- transformations. This is for authentication methods that sign requests -- based on data in Request.@@ -1353,6 +1427,12 @@   mempty = Option mempty Nothing   mappend = (<>) +-- | Use 'formToQuery'.+--+-- @since 3.11.0+instance FromForm (Option scheme) where+  fromForm = Right . formToQuery+ -- | A helper to create an 'Option' that modifies only collection of query -- parameters. This helper is not a part of the public API. withQueryParams :: (Y.QueryText -> Y.QueryText) -> Option scheme@@ -1371,7 +1451,7 @@  -- | Finalize given 'L.Request' by applying a finalizer from the given -- 'Option' (if it has any).-finalizeRequest :: MonadIO m => Option scheme -> L.Request -> m L.Request+finalizeRequest :: (MonadIO m) => Option scheme -> L.Request -> m L.Request finalizeRequest (Option _ mfinalizer) = liftIO . fromMaybe pure mfinalizer  ----------------------------------------------------------------------------@@ -1395,7 +1475,7 @@ (=:) :: (QueryParam param, ToHttpApiData a) => Text -> a -> param name =: value = queryParam name (pure value) --- | Construct a flag, that is, valueless query parameter. For example, in+-- | Construct a flag, that is, a valueless query parameter. For example, in -- the following URL @\"a\"@ is a flag, while @\"b\"@ is a query parameter -- with a value: --@@ -1404,9 +1484,27 @@ -- This operator is defined in terms of 'queryParam': -- -- > queryFlag name = queryParam name (Nothing :: Maybe ())-queryFlag :: QueryParam param => Text -> param+queryFlag :: (QueryParam param) => Text -> param queryFlag name = queryParam name (Nothing :: Maybe ()) +-- | Construct query parameters from a 'ToForm' instance. This function+-- produces the same query params as 'Form.urlEncodeAsFormStable'.+--+-- Note that 'Form.Form' doesn't have the concept of parameters with the+-- empty value (i.e. what you can get by @key =: ""@). If the value is+-- empty, it will be encoded as a valueless parameter (i.e. what you can get+-- by @queryFlag key@).+--+-- @since 3.11.0+formToQuery :: (QueryParam param, Monoid param, ToForm f) => f -> param+formToQuery f = mconcat . fmap toParam . Form.toListStable $ toForm f+  where+    toParam (key, val) =+      queryParam key $+        if val == ""+          then Nothing+          else Just val+ -- | A type class for query-parameter-like things. The reason to have an -- overloaded 'queryParam' is to be able to use it as an 'Option' and as a -- 'FormUrlEncodedParam' when constructing form URL encoded request bodies.@@ -1416,11 +1514,17 @@   -- 'Nothing', it won't be included at all (i.e. you create a flag this   -- way). It's recommended to use @('=:')@ and 'queryFlag' instead of this   -- method, because they are easier to read.-  queryParam :: ToHttpApiData a => Text -> Maybe a -> param+  queryParam :: (ToHttpApiData a) => Text -> Maybe a -> param +  -- | Get the query parameter names and values set by 'queryParam'.+  --+  -- @since 3.11.0+  queryParamToList :: param -> [(Text, Maybe Text)]+ instance QueryParam (Option scheme) where   queryParam name mvalue =     withQueryParams ((:) (name, toQueryParam <$> mvalue))+  queryParamToList (Option f _) = fst $ appEndo f ([], L.defaultRequest)  ---------------------------------------------------------------------------- -- Request—Optional parameters—Headers@@ -1444,6 +1548,14 @@ attachHeader name value x =   x {L.requestHeaders = (CI.mk name, value) : L.requestHeaders x} +-- | Same as 'header', but with redacted values on print.+--+-- @since 3.13.0+headerRedacted :: ByteString -> ByteString -> Option scheme+headerRedacted name value = withRequest $ \x ->+  let y = attachHeader name value x+   in y {L.redactHeaders = CI.mk name `S.insert` L.redactHeaders y}+ ---------------------------------------------------------------------------- -- Request—Optional parameters—Cookies @@ -1585,7 +1697,7 @@  -- | Specify the port to connect to explicitly. Normally, 'Url' you use -- determines the default port: @80@ for HTTP and @443@ for HTTPS. This--- 'Option' allows to choose an arbitrary port overwriting the defaults.+-- 'Option' allows us to choose an arbitrary port overwriting the defaults. port :: Int -> Option scheme port n = withRequest $ \x ->   x {L.port = n}@@ -1600,8 +1712,8 @@ -- -- > decompress (const True) decompress ::-  -- | Predicate that is given MIME type, it-  -- returns 'True' when content should be decompressed on the fly.+  -- | Predicate that is given MIME type, it returns 'True' when content+  -- should be decompressed on the fly.   (ByteString -> Bool) ->   Option scheme decompress f = withRequest $ \x ->@@ -1651,7 +1763,7 @@ newtype JsonResponse a = JsonResponse (L.Response a)   deriving (Show) -instance FromJSON a => HttpResponse (JsonResponse a) where+instance (FromJSON a) => HttpResponse (JsonResponse a) where   type HttpResponseBody (JsonResponse a) = a   toVanillaResponse (JsonResponse r) = r   getHttpResponse r = do@@ -1703,7 +1815,7 @@ ---------------------------------------------------------------------------- -- Helpers for response interpretations --- | Fetch beginning of response and return it together with new+-- | Fetch beginning of the response and return it together with a new -- @'L.Response' 'L.BodyReader'@ that can be passed to 'getHttpResponse' and -- such. grabPreview ::@@ -1767,14 +1879,14 @@  -- | Get the response body. responseBody ::-  HttpResponse response =>+  (HttpResponse response) =>   response ->   HttpResponseBody response responseBody = L.responseBody . toVanillaResponse  -- | Get the response status code. responseStatusCode ::-  HttpResponse response =>+  (HttpResponse response) =>   response ->   Int responseStatusCode =@@ -1782,7 +1894,7 @@  -- | Get the response status message. responseStatusMessage ::-  HttpResponse response =>+  (HttpResponse response) =>   response ->   ByteString responseStatusMessage =@@ -1790,7 +1902,7 @@  -- | Lookup a particular header from a response. responseHeader ::-  HttpResponse response =>+  (HttpResponse response) =>   -- | Response interpretation   response ->   -- | Header to lookup@@ -1802,7 +1914,7 @@  -- | Get the response 'L.CookieJar'. responseCookieJar ::-  HttpResponse response =>+  (HttpResponse response) =>   response ->   L.CookieJar responseCookieJar = L.responseCookieJar . toVanillaResponse@@ -1815,9 +1927,9 @@ -- To create a new response interpretation you just need to make your data -- type an instance of the 'HttpResponse' type class. --- | A type class for response interpretations. It allows to describe how to--- consume response from a @'L.Response' 'L.BodyReader'@ and produce the--- final result that is to be returned to the user.+-- | A type class for response interpretations. It allows us to describe how+-- to consume the response from a @'L.Response' 'L.BodyReader'@ and produce+-- the final result that is to be returned to the user. class HttpResponse response where   -- | The associated type is the type of body that can be extracted from an   -- instance of 'HttpResponse'.@@ -1856,6 +1968,16 @@   acceptHeader :: Proxy response -> Maybe ByteString   acceptHeader Proxy = Nothing +-- | This instance has been added to make it easier to inspect 'L.Response'+-- using Req's functions like 'responseStatusCode', 'responseStatusMessage',+-- etc.+--+-- @since 3.12.0+instance HttpResponse (L.Response ()) where+  type HttpResponseBody (L.Response ()) = ()+  toVanillaResponse = id+  getHttpResponse = return . void+ ---------------------------------------------------------------------------- -- Other @@ -1881,7 +2003,7 @@ -- method@ and @'HttpBody' body => 'RequestComponent' body@ when it decides -- which instance to use (i.e. the constraints are taken into account later, -- when instance is already chosen).-newtype Womb (tag :: Symbol) a = Womb a+newtype Tagged (tag :: Symbol) a = Tagged a  -- | Exceptions that this library throws. data HttpException@@ -1893,6 +2015,20 @@   deriving (Show, Typeable, Generic)  instance Exception HttpException++-- | Return 'Just' if the given 'HttpException' is wrapping a http-client's+-- 'L.StatusCodeException'. Otherwise, return 'Nothing'.+--+-- @since 3.12.0+isStatusCodeException :: HttpException -> Maybe (L.Response ())+isStatusCodeException+  ( VanillaHttpException+      ( L.HttpExceptionRequest+          _+          (L.StatusCodeException r _)+        )+    ) = Just r+isStatusCodeException _ = Nothing  -- | A simple type isomorphic to 'Bool' that we only have for better error -- messages. We use it as a kind and its data constructors as type-level
README.md view
@@ -6,8 +6,6 @@ [![Stackage LTS](http://stackage.org/package/req/badge/lts)](http://stackage.org/lts/package/req) ![CI](https://github.com/mrkkrp/req/workflows/CI/badge.svg?branch=master) -* [Motivation and Req vs other libraries](#motivation-and-req-vs-other-libraries)-* [Unsolved problems](#unsolved-problems) * [Related packages](#related-packages) * [Blog posts](#blog-posts) * [Contribution](#contribution)@@ -43,49 +41,33 @@   liftIO $ print (responseBody r :: Value) ``` -Req is an easy-to-use, type-safe, expandable, high-level HTTP client library-that just works without any fooling around.+Req is an HTTP client library that attempts to be easy-to-use, type-safe,+and expandable. -What does the phrase “easy-to-use” mean? It means that the library is-designed to be beginner-friendly so it's simple to add to your monad stack,-intuitive to work with, well-documented, and does not get in your way. Doing-HTTP requests is a common task and Haskell library for this should be very-approachable and clear to beginners, thus certain compromises were made. For-example, one cannot currently modify `ManagerSettings` of the default-manager because the library always uses the same implicit global manager for-simplicity and maximal connection sharing. There is a way to use your own-manager with different settings, but it requires a bit more typing.+“Easy-to-use” means that the library is designed to be beginner-friendly so+it's simple to add to your monad stack, intuitive to work with,+well-documented, and does not get in your way. Doing HTTP requests is a+common task and a Haskell library for this should be approachable and clear+to beginners, thus certain compromises were made. For example, one cannot+currently modify `ManagerSettings` of the default manager because the+library always uses the same implicit global manager for simplicity and+maximal connection sharing. There is a way to use your own manager with+different settings, but it requires more typing. -“Type-safe” means that the library is protective and eliminates certain-classes of errors. For example, we have correct-by-construction URLs, it's-guaranteed that the user does not send the request body when using methods-like GET or OPTIONS, and the amount of implicit assumptions is minimized by-making the user specify his/her intentions in an explicit form (for example,-it's not possible to avoid specifying the body or method of request).+“Type-safe” means that the library tries to eliminate certain classes of+errors. For example, we have correct-by-construction URLs; it is guaranteed+that the user does not send the request body when using methods like GET or+OPTIONS, and the amount of implicit assumptions is minimized by making the+user specify their intentions in an explicit form. For example, it's not+possible to avoid specifying the body or the method of a request. Authentication methods that assume HTTPS force the user to use HTTPS at the-type level. The library also carefully hides underlying types from the-lower-level `http-client` package because those types are not safe enough-(for example `Request` is an instance of `IsString` and, if it's malformed,-it will blow up at run-time).--“Expandable” refers to the ability to create new components for dealing with-HTTP without having to resort to ugly hacking. For example, it's possible to-define your own HTTP methods, create new ways to construct the body of a-request, create new authorization options, perform a request in a different-way, and create your own methods to parse and represent a response. As the-user extends the library to satisfy his/her special needs, the new solutions-will work just like the built-ins. However, all of the common cases are also-covered by the library out-of-the-box.+type level. -“High-level” means that there are less details to worry about. The library-is a result of my experiences as a Haskell consultant. Working for several-clients, who had very different projects, showed me that the library should-adapt easily to any particular style of writing Haskell applications. For-example, some people prefer throwing exceptions, while others are concerned-with purity. Just define `handleHttpException` accordingly when making your-monad instance of `MonadHttp` and it will play together seamlessly. Finally,-the library cuts down boilerplate considerably, and helps you write concise,-easy to read, and maintainable code.+“Expandable” refers to the ability to create new components without having+to resort to hacking. For example, it's possible to define your own HTTP+methods, create new ways to construct the body of a request, create new+authorization options, perform a request in a different way, and create your+own methods to parse a response.  The library uses the following mature packages under the hood to guarantee you the best experience:@@ -95,88 +77,10 @@ * [`http-client-tls`](https://hackage.haskell.org/package/http-client-tls)—TLS   (HTTPS) support for `http-client`. -It's important to note that since we leverage well-known libraries that the+It is important to note that since we leverage well-known libraries that the whole Haskell ecosystem uses, there is no risk in using Req. The machinery for performing requests is the same as with `http-conduit` and Wreq. The only difference is the API.--## Motivation and Req vs other libraries--*This section is my opinion and it contains criticisms of other well-known-libraries. If you're a user/fan of one of these libraries, please remember-not to react aggressively and respect the fact that I may have different-views on API design from yours.*--I have spent time to write the library because sending HTTP requests is a-common need, but there is no high-level library for that in Haskell that I-could use with pleasure. I'll explain why.--First of all, there is `http-client` and `http-client-tls`. They just work.-I have no issues with the libraries except that they are too low-level for-my taste. Indeed, even the docs say that they are low-level and “intended as-a base layer for more user-friendly packages”. This is exactly how I use-them in Req, as base level. Req is nothing but a different API to-`http-client`, so it only works because of the hard work put into-`http-client`.--`http-conduit` definitely has its place. For one thing it allows you to-stream request and response bodies in constant memory, what other library-allows you to do that? On the other hand if you take a look at-`Network.HTTP.Simple`, then although it's said that it's a “higher level-API”, it's mostly the same as vanilla `http-client` in spirit/approach and-just adds `conduit`-powered functions to perform requests and allows to use-global implicit `Manager` (Req does the same). If I tried to frame what-exactly I don't like about `http-conduit` in words, then it would be “the-way requests are constructed”. You *set* parameters instead of *being-forced* to declare necessary bits and *being allowed* to declare optional-bits in a way that their combination is valid. Also, with `http-conduit` you-parse request from a string without the protection of TH that otherwise-saves the day as in Yesod.--Then there is Wreq. `wreq` [doesn't see much development-lately](https://github.com/bos/wreq/issues/93). `wreq` is by itself a weird-library, IMO. You have functions per method—not very good, as there may be-new methods, like PATCH which is not new but still missing (well, you have-`customMethod`, but what is the point of having per-method functions if you-have a more general way to use any method? you should be able to just insert-methods in the “argument slot” of `customMethod` and end up with a more-general solution). Now, every method function has a companion that takes-`Options` (like you have `get` and `getWith`). Why the duplication? Where is-generality and flexibility? This is not all though, because you cannot-really use `get` you see in the main module, because you want to have-connection sharing. Wreq's author does not take the gift of automatic-connection re-use `Manager` from `http-client` provides, he invents the-whole new thing of “sessions”. Only inside a session your connections will-be shared and re-used. However with the session stuff you have yet another-set of per-method functions like `get` and `getWith`—these are different-ones, to be used with sessions! Now if you have a multi-threaded app, here-is a surprise for you: you can't share connections between threads as-connections are shared only inside of the `withSession` friend and “session-will no longer be valid after that function returns”. There are valid uses-for sessions, but the point is that they are just too inconvenient for-common tasks.--I used `servant-client` a couple of times but the amount of boilerplate it-requires is frightening. If you have several query parameters, and you use-just one of them, you'll have to pass lots of `Nothing`s.--## Unsolved problems--AWS request signing is problematic because request body can be in the form-of an action to execute (and all that “popper” stuff for streaming), not-just a `ByteString` and so getting its digest (hash) is not trivial without-running the action and consuming body in its entirety before the request in-made. In Wreq the author chose to just use `error` when body is not a-(strict or lazy) `ByteString`. Maybe it's OK for Wreq, but I don't consider-this a proper solution for Req as we support full variety of body options.-For example, what if I want to upload 1 Gb file to S3? I want to stream it-in constant memory but at the same time I need to calculate its hash before-I start streaming. One solution to the problem seems to be in taking the-hash explicitly (as an argument of the hypothetical `awsAuth`) and making it-a responsibility of the user to calculate the hash correctly. I don't like-this because it's not user-friendly. So the question stays open, for now-there is no AWS signing functionality provided out-of-the-box. The best-solution for talking to AWS is the `amazonka` package so far.  ## Related packages 
− Setup.hs
@@ -1,6 +0,0 @@-module Main (main) where--import Distribution.Simple--main :: IO ()-main = defaultMain
httpbin-tests/Network/HTTP/ReqSpec.hs view
@@ -1,6 +1,6 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# OPTIONS_GHC -fno-warn-orphans #-}@@ -8,29 +8,28 @@ module Network.HTTP.ReqSpec (spec) where  import Control.Exception-import Control.Monad.Reader+import Control.Monad (forM_) import Control.Monad.Trans.Control import Data.Aeson (ToJSON (..), Value (..), object, (.=))-import qualified Data.Aeson as A-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as BL-import qualified Data.HashMap.Strict as HM+import Data.Aeson qualified as A+import Data.Aeson.KeyMap qualified as Aeson.KeyMap+import Data.ByteString qualified as B+import Data.ByteString.Lazy qualified as BL+import Data.Functor.Identity (runIdentity)+import Data.Maybe (fromJust) import Data.Proxy import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import qualified Data.Text.IO as TIO-import qualified Network.HTTP.Client as L-import qualified Network.HTTP.Client.MultipartFormData as LM-import Network.HTTP.Req-import qualified Network.HTTP.Types as Y+import Data.Text qualified as T+import Data.Text.Encoding qualified as T+import Data.Text.IO qualified as TIO+import Network.HTTP.Client qualified as L+import Network.HTTP.Client.MultipartFormData qualified as LM+import Network.HTTP.Req hiding (req)+import Network.HTTP.Req qualified as Req+import Network.HTTP.Types qualified as Y import Test.Hspec import Test.QuickCheck -#if !MIN_VERSION_base(4,13,0)-import Data.Semigroup ((<>))-#endif- spec :: Spec spec = do   describe "exception throwing on non-2xx status codes" $@@ -51,6 +50,11 @@         blindlyThrowing (req GET httpbin NoReqBody ignoreResponse mempty)           `shouldThrow` anyException +  describe "isStatusCodeException" $+    it "extracts non-2xx response" $+      req GET (httpbin /: "foo") NoReqBody ignoreResponse mempty+        `shouldThrow` selector404ByStatusCodeException+   describe "receiving user-agent header back" $     it "works" $ do       r <-@@ -81,8 +85,7 @@               .= object                 [ "Accept-Encoding" .= ("gzip" :: Text),                   "Foo" .= ("bar" :: Text),-                  "Baz" .= ("quux" :: Text),-                  "Host" .= ("httpbin.org" :: Text)+                  "Baz" .= ("quux" :: Text)                 ]           ]       responseStatusCode r `shouldBe` 200@@ -94,11 +97,10 @@       (stripFunnyHeaders . stripOrigin) (responseBody r)         `shouldBe` object           [ "args" .= emptyObject,-            "url" .= ("https://httpbin.org/get" :: Text),+            "url" .= ("http://localhost:1234/get" :: Text),             "headers"               .= object-                [ "Accept-Encoding" .= ("gzip" :: Text),-                  "Host" .= ("httpbin.org" :: Text)+                [ "Accept-Encoding" .= ("gzip" :: Text)                 ]           ]       responseHeader r "Content-Type" `shouldBe` return "application/json"@@ -115,12 +117,11 @@           [ "args" .= emptyObject,             "json" .= text,             "data" .= reflected,-            "url" .= ("https://httpbin.org/post" :: Text),+            "url" .= ("http://localhost:1234/post" :: Text),             "headers"               .= object                 [ "Content-Type" .= ("application/json; charset=utf-8" :: Text),                   "Accept-Encoding" .= ("gzip" :: Text),-                  "Host" .= ("httpbin.org" :: Text),                   "Content-Length" .= show (T.length reflected)                 ],             "files" .= emptyObject,@@ -138,18 +139,17 @@             LM.partBS "bar" "bar data!"           ]       r <- req POST (httpbin /: "post") body jsonResponse mempty-      let Just contentType = getRequestContentType body+      let contentType = fromJust (getRequestContentType body)       (stripFunnyHeaders . stripOrigin) (responseBody r)         `shouldBe` object           [ "args" .= emptyObject,             "json" .= Null,             "data" .= ("" :: Text),-            "url" .= ("https://httpbin.org/post" :: Text),+            "url" .= ("http://localhost:1234/post" :: Text),             "headers"               .= object                 [ "Content-Type" .= T.decodeUtf8 contentType,                   "Accept-Encoding" .= ("gzip" :: Text),-                  "Host" .= ("httpbin.org" :: Text),                   "Content-Length" .= ("242" :: Text)                 ],             "files" .= emptyObject,@@ -174,11 +174,10 @@           [ "args" .= emptyObject,             "json" .= Null,             "data" .= contents,-            "url" .= ("https://httpbin.org/patch" :: Text),+            "url" .= ("http://localhost:1234/patch" :: Text),             "headers"               .= object                 [ "Accept-Encoding" .= ("gzip" :: Text),-                  "Host" .= ("httpbin.org" :: Text),                   "Content-Length" .= show (T.length contents)                 ],             "files" .= emptyObject,@@ -200,12 +199,11 @@           [ "args" .= emptyObject,             "json" .= Null,             "data" .= ("" :: Text),-            "url" .= ("https://httpbin.org/put" :: Text),+            "url" .= ("http://localhost:1234/put" :: Text),             "headers"               .= object                 [ "Content-Type" .= ("application/x-www-form-urlencoded" :: Text),                   "Accept-Encoding" .= ("gzip" :: Text),-                  "Host" .= ("httpbin.org" :: Text),                   "Content-Length" .= ("18" :: Text)                 ],             "files" .= emptyObject,@@ -263,21 +261,17 @@   -- TODO /response-headers   -- TODO /redirect -  -- FIXME Redirects test is temporarily disabled due to-  ---  -- https://github.com/postmanlabs/httpbin/issues/617--  -- describe "redirects" $-  --   it "follows redirects" $ do-  --     r <--  --       req-  --         GET-  --         (httpbin /: "redirect-to")-  --         NoReqBody-  --         ignoreResponse-  --         ("url" =: ("https://httpbin.org" :: Text))-  --     responseStatusCode r `shouldBe` 200-  --     responseStatusMessage r `shouldBe` "OK"+  describe "redirects" $+    it "follows redirects" $ do+      r <-+        req+          GET+          (httpbin /: "redirect-to")+          NoReqBody+          ignoreResponse+          ("url" =: ("https://httpbin.org" :: Text))+      responseStatusCode r `shouldBe` 200+      responseStatusMessage r `shouldBe` "OK"    -- TODO /relative-redicet   -- TODO /absolute-redirect@@ -307,7 +301,7 @@             (httpbin /: "basic-auth" /~ user /~ password)             NoReqBody             ignoreResponse-            (basicAuth (T.encodeUtf8 user) (T.encodeUtf8 password))+            (basicAuthUnsafe (T.encodeUtf8 user) (T.encodeUtf8 password))         responseStatusCode r `shouldBe` 200         responseStatusMessage r `shouldBe` "OK" @@ -394,45 +388,81 @@ ---------------------------------------------------------------------------- -- Helpers --- | Run request with such settings that it does not signal error on adverse--- response status codes.+-- | Run a request with such settings that it does not signal errors. prepareForShit :: Req a -> IO a prepareForShit = runReq defaultHttpConfig {httpConfigCheckResponse = noNoise}   where     noNoise _ _ _ = Nothing --- | Run request with such settings that it throws on any response.+-- | Run a request with such settings that it throws on any response. blindlyThrowing :: Req a -> IO a blindlyThrowing = runReq defaultHttpConfig {httpConfigCheckResponse = doit}   where     doit _ _ = error "Oops!"  -- | 'Url' representing <https://httpbin.org>.-httpbin :: Url 'Https-httpbin = https "httpbin.org"+httpbin :: Url 'Http+httpbin = http "localhost" +req ::+  ( MonadHttp m,+    HttpMethod method,+    HttpBody body,+    HttpResponse response,+    HttpBodyAllowed (AllowsBody method) (ProvidesBody body)+  ) =>+  method ->+  Url scheme ->+  body ->+  Proxy response ->+  Option scheme ->+  m response+req method url body responseProxy options =+  Req.req method url body responseProxy (options <> defaultOptions)++-- | Options to apply by default.+defaultOptions :: Option scheme+defaultOptions = port 1234+ -- | Remove “origin” field from JSON value. Origin may change, we don't want -- to depend on that. stripOrigin :: Value -> Value-stripOrigin (Object m) = Object (HM.delete "origin" m)+stripOrigin (Object m) = Object (Aeson.KeyMap.delete "origin" m) stripOrigin value = value  -- | Remove funny headers that might break the tests. stripFunnyHeaders :: Value -> Value-stripFunnyHeaders (Object m) =-  let f (Object p) = Object $ HM.filterWithKey (\k _ -> k `elem` hs) p-      f value = value-      hs =-        [ "Content-Type",-          "Accept-Encoding",-          "Host",-          "Content-Length",-          "Foo",-          "Baz"-        ]-   in Object (HM.adjust f "headers" m)-stripFunnyHeaders value = value+stripFunnyHeaders = \case+  Object m ->+    Object+      ( runIdentity+          ( Aeson.KeyMap.alterF+              (pure . fmap stripFunnyHeaders')+              "headers"+              m+          )+      )+  value -> value +-- | Similar to 'stripFunnyHeaders', but acts directly on the argument+-- without trying to access its "headers" field.+stripFunnyHeaders' :: Value -> Value+stripFunnyHeaders' = \case+  Object p ->+    Object $+      Aeson.KeyMap.filterWithKey+        (\k _ -> k `elem` whitelistedHeaders)+        p+  value -> value+  where+    whitelistedHeaders =+      [ "Content-Type",+        "Accept-Encoding",+        "Content-Length",+        "Foo",+        "Baz"+      ]+ -- | This is a complete test case that makes use of <https://httpbin.org> to -- get various response status codes. checkStatusCode :: Int -> SpecWith ()@@ -461,12 +491,19 @@     L.responseStatus response == Y.status404 && not (B.null chunk) selector404 _ = False --- | Empty JSON 'Object'.+-- | Same as 'selector404' except that it uses 'isStatusCodeException'.+selector404ByStatusCodeException :: HttpException -> Bool+selector404ByStatusCodeException e =+  case isStatusCodeException e of+    Nothing -> False+    Just r -> responseStatusCode r == 404++-- | The empty JSON 'Object'. emptyObject :: Value-emptyObject = Object HM.empty+emptyObject = Object Aeson.KeyMap.empty --- | Get rendered JSON value as 'Text'.-reflectJSON :: ToJSON a => a -> Text+-- | Get a rendered JSON value as 'Text'.+reflectJSON :: (ToJSON a) => a -> Text reflectJSON = T.decodeUtf8 . BL.toStrict . A.encode  -- | Clarify to the type checker that the inner computation is in the 'IO'
pure-tests/Network/HTTP/ReqSpec.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleInstances #-}@@ -14,43 +13,41 @@  module Network.HTTP.ReqSpec (spec) where -import qualified Blaze.ByteString.Builder as BB+import Blaze.ByteString.Builder qualified as BB import Control.Exception (throwIO) import Control.Monad import Control.Retry import Data.Aeson (ToJSON (..))-import qualified Data.Aeson as A+import Data.Aeson qualified as A import Data.ByteString (ByteString)-import qualified Data.ByteString as B-import qualified Data.ByteString.Char8 as B8-import qualified Data.ByteString.Lazy as BL-import qualified Data.CaseInsensitive as CI+import Data.ByteString qualified as B+import Data.ByteString.Char8 qualified as B8+import Data.ByteString.Lazy qualified as BL+import Data.CaseInsensitive qualified as CI import Data.Either (isRight)-import qualified Data.List.NonEmpty as NE+import Data.List.NonEmpty qualified as NE import Data.Maybe (fromJust, fromMaybe, isJust, isNothing) import Data.Proxy import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.Encoding as T+import Data.Text qualified as T+import Data.Text.Encoding qualified as T import Data.Time import Data.Typeable (Typeable, eqT)+import GHC.Exts (IsList (..)) import GHC.Generics-import qualified Language.Haskell.TH as TH-import qualified Language.Haskell.TH.Quote as TH-import qualified Network.HTTP.Client as L+import Language.Haskell.TH qualified as TH+import Language.Haskell.TH.Quote qualified as TH+import Network.HTTP.Client qualified as L import Network.HTTP.Req-import qualified Network.HTTP.Types as Y-import qualified Network.HTTP.Types.Header as Y+import Network.HTTP.Types qualified as Y+import Network.HTTP.Types.Header qualified as Y import Test.Hspec import Test.Hspec.Core.Spec (SpecM) import Test.QuickCheck import Text.URI (URI)-import qualified Text.URI as URI-import qualified Text.URI.QQ as QQ--#if !MIN_VERSION_base(4,13,0)-import Data.Semigroup ((<>))-#endif+import Text.URI qualified as URI+import Text.URI.QQ qualified as QQ+import Web.FormUrlEncoded qualified as F  spec :: Spec spec = do@@ -164,11 +161,13 @@           let uriHttp = uri' {URI.uriScheme = Just [QQ.scheme|http|]}               uriHttps = uri' {URI.uriScheme = Just [QQ.scheme|https|]}           requestHttp <--            let Left (url', options) = fromJust (useURI uriHttp)-             in req_ GET url' NoReqBody options+            case fromJust (useURI uriHttp) of+              Left (url', options) -> req_ GET url' NoReqBody options+              _ -> error "(useURI uriHttp) should have returned Left"           requestHttps <--            let Right (url', options) = fromJust (useURI uriHttps)-             in req_ GET url' NoReqBody options+            case fromJust (useURI uriHttps) of+              Right (url', options) -> req_ GET url' NoReqBody options+              _ -> error "(useURI uriHttps) should have returned Right"           L.host requestHttp `shouldBe` uriHost uriHttp           L.host requestHttps `shouldBe` uriHost uriHttps           L.port requestHttp `shouldBe` uriPort 80 uriHttp@@ -238,6 +237,19 @@             L.RequestBodyLBS x -> x `shouldBe` renderQuery params             _ -> expectationFailure "Wrong request body constructor." +  describe "query params" $ do+    describe "FormUrlEncodedParam" $ do+      describe "formToQuery" $ do+        it "should produce the same parameters as F.urlEncodeFormStable" $+          property $ \form -> do+            request <- req_ POST url (ReqBodyUrlEnc $ formToQuery form) mempty+            case L.requestBody request of+              L.RequestBodyLBS x -> x `shouldBe` F.urlEncodeFormStable form+              _ -> expectationFailure "Wrong request body constructor"+      specParamToList (Proxy :: Proxy FormUrlEncodedParam)+    describe "Option" $ do+      specParamToList (Proxy :: Proxy (Option 'Http))+   describe "optional parameters" $ do     describe "header" $ do       it "sets specified header value" $@@ -266,11 +278,7 @@       it "cookie jar is set without modifications" $         property $ \cjar -> do           request <- req_ GET url NoReqBody (cookieJar cjar)-#if MIN_VERSION_http_client(0,7,0)           L.cookieJar request `shouldSatisfy` (maybe False (L.equalCookieJar cjar))-#else-          L.cookieJar request `shouldBe` Just cjar-#endif     describe "basicAuth" $ do       it "sets Authorization header to correct value" $         property $ \username password -> do@@ -405,7 +413,7 @@         -- 'Https, Option _) type checks, so we can catch if the type of scheme         -- is unspecified.         let testTypeOfQuoterResult ::-              forall a s. Typeable a => (a, Option s) -> Bool+              forall a s. (Typeable a) => (a, Option s) -> Bool             testTypeOfQuoterResult _ = isJust $ eqT @a @(Url 'Https)          in property $ testTypeOfQuoterResult [urlQ|https://example.org/|]       it "doesn't work for invalid urls" $@@ -495,6 +503,9 @@ instance Arbitrary DiffTime where   arbitrary = secondsToDiffTime <$> arbitrary +instance Arbitrary F.Form where+  arbitrary = (F.Form . fromList) <$> arbitrary+ ---------------------------------------------------------------------------- -- Helper types @@ -562,13 +573,14 @@   maybe def (fromIntegral) $     either (const Nothing) Just (URI.uriAuthority uri) >>= URI.authPort --- | Get path from 'URI'.+-- | Get the path from a 'URI'. uriPath :: URI -> ByteString uriPath uri = fromMaybe "" $ do-  (_, xs) <- URI.uriPath uri-  (return . encodePathPieces . fmap URI.unRText . NE.toList) xs+  (trailingSlash, xs) <- URI.uriPath uri+  let pref = (encodePathPieces . fmap URI.unRText . NE.toList) xs+  return $ if trailingSlash then pref <> "/" else pref --- | Get query string from 'URI'.+-- | Get the query string from a 'URI'. uriQuery :: URI -> ByteString uriQuery uri = do   let liftQueryParam = \case@@ -606,3 +618,15 @@ basicProxyAuthHeader username password =   fromJust . lookup Y.hProxyAuthorization . L.requestHeaders $     L.applyBasicProxyAuth username password L.defaultRequest++-- | Spec about 'paramToList' for the type @p@.+specParamToList :: (QueryParam p, Monoid p) => Proxy p -> Spec+specParamToList typeProxy = do+  describe "paramToList" $ do+    it "should reproduce the parameters given by queryParam" $+      property $ \(QueryParams params) -> do+        let queryParam0 =+              (mconcat $ fmap (uncurry queryParam) params)+                `asProxyTypeOf` typeProxy+            got = queryParamToList queryParam0+        got `shouldBe` params
req.cabal view
@@ -1,19 +1,15 @@-cabal-version:   1.18+cabal-version:   2.4 name:            req-version:         3.8.0-license:         BSD3+version:         3.13.4+license:         BSD-3-Clause license-file:    LICENSE.md maintainer:      Mark Karpov <markkarpov92@gmail.com> author:          Mark Karpov <markkarpov92@gmail.com>-tested-with:     ghc ==8.6.5 ghc ==8.8.4 ghc ==8.10.2+tested-with:     ghc ==9.6.3 ghc ==9.8.2 ghc ==9.10.1 homepage:        https://github.com/mrkkrp/req bug-reports:     https://github.com/mrkkrp/req/issues-synopsis:-    Easy-to-use, type-safe, expandable, high-level HTTP client library--description:-    Easy-to-use, type-safe, expandable, high-level HTTP client library.-+synopsis:        HTTP client library+description:     HTTP client library. category:        Network, Web build-type:      Simple data-files:@@ -35,97 +31,97 @@  library     exposed-modules:  Network.HTTP.Req-    default-language: Haskell2010+    default-language: GHC2021     build-depends:-        aeson >=0.9 && <1.6,-        authenticate-oauth >=1.5 && <1.7,-        base >=4.12 && <5.0,+        aeson >=0.9 && <3,+        authenticate-oauth >=1.5 && <1.8,+        base >=4.15 && <5,         blaze-builder >=0.3 && <0.5,-        bytestring >=0.10.8 && <0.11,+        bytestring >=0.10.8 && <0.13,         case-insensitive >=0.2 && <1.3,-        connection >=0.2.2 && <0.4,+        containers >=0.5 && <0.7,+        crypton-connection >=0.3 && <0.5,+        data-default-class,         exceptions >=0.6 && <0.11,-        http-api-data >=0.2 && <0.5,-        http-client >=0.5 && <0.8,+        http-api-data >=0.2 && <0.7,+        http-client >=0.7.13.1 && <0.8,         http-client-tls >=0.3.2 && <0.4,-        http-types >=0.8 && <10.0,+        http-types >=0.8 && <10,         modern-uri >=0.3 && <0.4,         monad-control >=1.0 && <1.1,         mtl >=2.0 && <3.0,-        retry >=0.8 && <0.9,-        template-haskell >=2.14 && <2.17,-        text >=0.2 && <1.3,-        time >=1.2 && <1.10,-        transformers >=0.4 && <0.6,-        transformers-base -any,+        retry >=0.8 && <0.10,+        template-haskell >=2.19 && <2.23,+        text >=0.2 && <2.2,+        transformers >=0.5.3.0 && <0.7,+        transformers-base,         unliftio-core >=0.1.1 && <0.3      if flag(dev)-        ghc-options: -O0 -Wall -Werror+        ghc-options:+            -Wall -Werror -Wpartial-fields -Wunused-packages+            -Wno-unused-imports      else         ghc-options: -O2 -Wall -    if flag(dev)-        ghc-options:-            -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns-            -Wnoncanonical-monad-instances- test-suite pure-tests-    type:             exitcode-stdio-1.0-    main-is:          Spec.hs-    build-tools:      hspec-discover >=2.0 && <3.0-    hs-source-dirs:   pure-tests-    other-modules:    Network.HTTP.ReqSpec-    default-language: Haskell2010+    type:               exitcode-stdio-1.0+    main-is:            Spec.hs+    build-tool-depends: hspec-discover:hspec-discover >=2 && <3+    hs-source-dirs:     pure-tests+    other-modules:      Network.HTTP.ReqSpec+    default-language:   GHC2021     build-depends:-        QuickCheck >=2.7 && <3.0,-        aeson >=0.9 && <1.6,-        base >=4.12 && <5.0,+        QuickCheck >=2.7 && <3,+        aeson >=0.9 && <3,+        base >=4.15 && <5.0,         blaze-builder >=0.3 && <0.5,-        bytestring >=0.10.8 && <0.11,+        bytestring >=0.10.8 && <0.13,         case-insensitive >=0.2 && <1.3,-        hspec >=2.0 && <3.0,-        hspec-core >=2.0 && <3.0,-        http-client >=0.5 && <0.8,-        http-types >=0.8 && <10.0,+        hspec >=2.0 && <3,+        hspec-core >=2.0 && <3,+        http-api-data >=0.2 && <0.7,+        http-client >=0.7 && <0.8,+        http-types >=0.8 && <10,         modern-uri >=0.3 && <0.4,-        mtl >=2.0 && <3.0,-        req -any,-        retry >=0.8 && <0.9,-        template-haskell >=2.14 && <2.17,-        text >=0.2 && <1.3,-        time >=1.2 && <1.10+        req,+        retry >=0.8 && <0.10,+        template-haskell >=2.19 && <2.23,+        text >=0.2 && <2.2,+        time >=1.2 && <1.13      if flag(dev)-        ghc-options: -O0 -Wall -Werror+        ghc-options:+            -Wall -Werror -Wredundant-constraints -Wpartial-fields+            -Wunused-packages      else         ghc-options: -O2 -Wall  test-suite httpbin-tests-    type:             exitcode-stdio-1.0-    main-is:          Spec.hs-    build-tools:      hspec-discover >=2.0 && <3.0-    hs-source-dirs:   httpbin-tests-    other-modules:    Network.HTTP.ReqSpec-    default-language: Haskell2010+    type:               exitcode-stdio-1.0+    main-is:            Spec.hs+    build-tool-depends: hspec-discover:hspec-discover >=2 && <3+    hs-source-dirs:     httpbin-tests+    other-modules:      Network.HTTP.ReqSpec+    default-language:   GHC2021     build-depends:-        QuickCheck >=2.7 && <3.0,-        aeson >=0.9 && <1.6,-        base >=4.12 && <5.0,-        bytestring >=0.10.8 && <0.11,+        QuickCheck >=2.7 && <3,+        aeson >=2 && <3,+        base >=4.15 && <5,+        bytestring >=0.10.8 && <0.13,         hspec >=2.0 && <3.0,-        http-client >=0.5 && <0.8,-        http-types >=0.8 && <10.0,+        http-client >=0.7 && <0.8,+        http-types >=0.8 && <10,         monad-control >=1.0 && <1.1,-        mtl >=2.0 && <3.0,-        req -any,-        text >=0.2 && <1.3,-        unordered-containers >=0.2.5 && <0.3+        req,+        text >=0.2 && <2.2      if flag(dev)-        ghc-options: -O0 -Wall -Werror+        ghc-options:+            -Wall -Werror -Wredundant-constraints -Wpartial-fields+            -Wunused-packages      else-        ghc-options: -O2 -Wall+        buildable: False