on master help sign in
.github/dependabot.ymlnew file+6
since splitsnapshots
unifiedsplit
@@ -0,0 +1,6 @@
1+version: 2
2+updates:
3+ - package-ecosystem: "github-actions"
4+ directory: "/"
5+ schedule:
6+ interval: "weekly"
.github/workflows/ci.yamlnew file+47
@@ -0,0 +1,47 @@
1+name: CI
2+on:
3+ push:
4+ branches:
5+ - master
6+ pull_request:
7+ types:
8+ - opened
9+ - synchronize
10+jobs:
11+ ormolu:
12+ runs-on: ubuntu-latest
13+ steps:
14+ - uses: actions/checkout@v4
15+ - uses: haskell-actions/run-ormolu@v17
16+ build:
17+ runs-on: ubuntu-latest
18+ needs: ormolu
19+ strategy:
20+ matrix:
21+ cabal: ["3.12"]
22+ ghc: ["9.6.3", "9.8.2", "9.10.1"]
23+ steps:
24+ - uses: actions/checkout@v4
25+ - uses: haskell-actions/setup@v2
26+ id: setup-haskell-cabal
27+ with:
28+ ghc-version: ${{ matrix.ghc }}
29+ cabal-version: ${{ matrix.cabal }}
30+ - run: cabal update
31+ - run: cabal freeze
32+ - uses: actions/cache@v4.2.3
33+ with:
34+ path: |
35+ ${{ steps.setup-haskell-cabal.outputs.cabal-store }}
36+ dist-newstyle
37+ key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}
38+ restore-keys: |
39+ ${{ runner.os }}-${{ matrix.ghc }}-
40+ - run: cabal format && git diff --exit-code --color=always
41+ - run: cabal build
42+ - run: |
43+ docker pull kennethreitz/httpbin
44+ docker run -p 1234:80 kennethreitz/httpbin &
45+ cabal test
46+ - run: cabal haddock
47+ - run: cabal sdist
.gitignore+1
@@ -25,5 +25,6 @@ cabal.project.local
25cabal.sandbox.config
25cabal.sandbox.config
26dist-*/
26dist-*/
27dist/
27dist/
28+hie.yaml
28stack.yaml
29stack.yaml
29stack.yaml.lock
30stack.yaml.lock
.travis.ymldeleted−39
@@ -1,39 +0,0 @@
1−language: c
2−
3−sudo: false
4−
5−cache:
6− directories:
7− - ~/.cabal/packages
8− - ~/.cabal/store
9−
10−matrix:
11− include:
12− - env: CABALVER=3.0 GHCVER=8.6.5
13− addons: {apt: {packages: [cabal-install-3.0,ghc-8.6.5], sources: [hvr-ghc]}}
14− - env: CABALVER=3.0 GHCVER=8.8.3
15− addons: {apt: {packages: [cabal-install-3.0,ghc-8.8.3], sources: [hvr-ghc]}}
16− - env: CABALVER=3.0 GHCVER=8.10.1
17− addons: {apt: {packages: [cabal-install-3.0,ghc-8.10.1], sources: [hvr-ghc]}}
18−
19−before_install:
20− - export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$PATH
21−
22−install:
23− - cabal --version
24− - ghc --version
25− - travis_retry cabal new-update
26−
27−script:
28− # To avoid re-compilation it is important to use exactly the same
29− # configuration for all steps.
30− - export CONFIG="--enable-tests --enable-benchmarks --flags=dev"
31− - cabal format
32− - git diff --exit-code --color=always
33− - cabal new-build $CONFIG
34− - cabal new-test $CONFIG
35− - cabal new-haddock $CONFIG
36− - cabal new-sdist
37−
38−notifications:
39− email: false
CHANGELOG.md+89
@@ -1,8 +1,97 @@
1+## Req 3.13.4
2+
3+* Fixed empty ciphersuite list when compiling against `tls < 2.0.6`
4+ (see [PR 175](https://github.com/mrkkrp/req/pull/175)). As a side effect, now
5+ compatible with older versions of `crypton-connection` (>= 0.3).
6+
7+## Req 3.13.3
8+
9+* Works with `crypton-connection-0.4` and newer.
10+
11+## Req 3.13.2
12+
13+* Disable the problematic `httpbin-tests` test suite by default. Only enable
14+ it when the `dev` flag is enabled. In that case it is expected that an
15+ httpbin server is run locally at `localhost:1234`.
16+
17+## Req 3.13.1
18+
19+* Switched the non-pure test suite to use https://httpbun.org instead of
20+ https://httpbin.org since the latter proved to be highly unreliable
21+ lately.
22+
23+* Switched from `connection` to `crypton-connection`.
24+
25+* Builds with GHC 9.6.1.
26+
27+## Req 3.13.0
28+
29+* Add `headerRedacted` function to add header fields, which will be with
30+ redacted values on print.
31+
32+## Req 3.12.0
33+
34+* Add `isStatusCodeException` function.
35+* Add `instance HttpResponse (Network.HTTP.Client.Response ())`.
36+
37+## Req 3.11.0
38+
39+* Add the `queryParamToList` method to the `QueryParam` type class.
40+* Add the `formToQuery` function. [Issue 126](https://github.com/mrkkrp/req/issues/126).
41+* Add `FromForm` instances (in the `Web.FormUrlEncoded` module) to the
42+ `Option` and `FormUrlEncodedParam` types.
43+
44+## Req 3.10.0
45+
46+* Add `MonadHttp` instances for `transformers` types.
47+
48+## Req 3.9.2
49+
50+* The test suite works with `aeson-2.x.x.x`.
51+
52+## Req 3.9.1
53+
54+* Builds with GHC 9.0.
55+
56+## Req 3.9.0
57+
58+* The `useHttpURI` and `useHttpsURI` functions now preserve trailing
59+ slashes.
60+
61+## Req 3.8.0
62+
63+* Adjusted the value of the `httpConfigRetryJudgeException` field of
64+ `defaultHttpConfig` to retry on response timeouts and connection timeouts.
65+
66+## Req 3.7.0
67+
68+* Added `reqCb`, a function that allows you to modify the `Request` object
69+ but otherwise performs the requst for you.
70+
71+* Derived `MonadThrow`, `MonadCatch`, and `MonadMask` for the `Req` monad.
72+
73+## Req 3.6.0
74+
75+* Added the `httpConfigBodyPreviewLength` configuration parameter to
76+ `HttpConfig`.
77+
78+## Req 3.5.0
79+
80+* Made `Req` an instance of `MonadUnliftIO`. [Issue
81+ 100](https://github.com/mrkkrp/req/issues/100).
82+
1## Req 3.4.0
83## Req 3.4.0
2
84
3* Requests using `DELETE` method can now have a body. [Issue
85* Requests using `DELETE` method can now have a body. [Issue
4 89](https://github.com/mrkkrp/req/issues/89).
86 89](https://github.com/mrkkrp/req/issues/89).
5
87
88+* Added the `httpConfigRetryJudgeException` field to `HttpConfig` so that
89+ requests that result in exceptions can be retried. [Issue
90+ 93](https://github.com/mrkkrp/req/issues/93).
91+
92+* Added the function `renderUrl`. [Issue
93+ 83](https://github.com/mrkkrp/req/issues/83).
94+
6## Req 3.3.0
95## Req 3.3.0
7
96
8* Derived `Show` instances for response types `IgnoreResponse`,
97* Derived `Show` instances for response types `IgnoreResponse`,
Network/HTTP/Req.hs+419−186
@@ -1,19 +1,9 @@
1−{-# LANGUAGE BangPatterns #-}
2−{-# LANGUAGE CPP #-}
3{-# LANGUAGE DataKinds #-}
1{-# LANGUAGE DataKinds #-}
4−{-# LANGUAGE DeriveDataTypeable #-}
5−{-# LANGUAGE DeriveGeneric #-}
6−{-# LANGUAGE DeriveLift #-}
7−{-# LANGUAGE FlexibleInstances #-}
8−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
9{-# LANGUAGE LambdaCase #-}
2{-# LANGUAGE LambdaCase #-}
10−{-# LANGUAGE MultiParamTypeClasses #-}
11{-# LANGUAGE OverloadedStrings #-}
3{-# LANGUAGE OverloadedStrings #-}
12{-# LANGUAGE QuasiQuotes #-}
4{-# LANGUAGE QuasiQuotes #-}
13−{-# LANGUAGE RankNTypes #-}
14{-# LANGUAGE RecordWildCards #-}
5{-# LANGUAGE RecordWildCards #-}
15{-# LANGUAGE RoleAnnotations #-}
6{-# LANGUAGE RoleAnnotations #-}
16−{-# LANGUAGE ScopedTypeVariables #-}
17{-# LANGUAGE TemplateHaskellQuotes #-}
7{-# LANGUAGE TemplateHaskellQuotes #-}
18{-# LANGUAGE TypeFamilies #-}
8{-# LANGUAGE TypeFamilies #-}
19{-# LANGUAGE UndecidableInstances #-}
9{-# LANGUAGE UndecidableInstances #-}
@@ -29,61 +19,40 @@
29--
19--
30-- The documentation below is structured in such a way that the most
20-- The documentation below is structured in such a way that the most
31-- important information is presented first: you learn how to do HTTP
21-- important information is presented first: you learn how to do HTTP
32−-- requests, how to embed them in any monad you have, and then it gives you
22+-- requests, how to embed them in the monad you have, and then it gives you
33-- details about less-common things you may want to know about. The
23-- details about less-common things you may want to know about. The
34-- documentation is written with sufficient coverage of details and
24-- documentation is written with sufficient coverage of details and
35-- examples, and it's designed to be a complete tutorial on its own.
25-- examples, and it's designed to be a complete tutorial on its own.
36--
26--
37−-- /(A modest intro goes here, click on 'req' to start making requests.)/
38−--
39-- === About the library
27-- === About the library
40--
28--
41−-- Req is an easy-to-use, type-safe, expandable, high-level HTTP client
29+-- Req is an HTTP client library that attempts to be easy-to-use, type-safe,
42−-- library that just works without any fooling around.
30+-- and expandable.
43−--
31+--
44−-- What does the phrase “easy-to-use” mean? It means that the library is
32+-- “Easy-to-use” means that the library is designed to be beginner-friendly
45−-- designed to be beginner-friendly so it's simple to add to your monad
33+-- so it's simple to add to your monad stack, intuitive to work with,
46−-- stack, intuitive to work with, well-documented, and does not get in your
34+-- well-documented, and does not get in your way. Doing HTTP requests is a
47−-- way. Doing HTTP requests is a common task and Haskell library for this
35+-- common task and a Haskell library for this should be approachable and
48−-- should be very approachable and clear to beginners, thus certain
49−-- compromises were made. For example, one cannot currently modify
36+-- clear to beginners, thus certain compromises were made. For example, one
50−-- 'L.ManagerSettings' of the default manager because the library always
37+-- cannot currently modify 'L.ManagerSettings' of the default manager
51−-- uses the same implicit global manager for simplicity and maximal
38+-- because the library always uses the same implicit global manager for
52−-- connection sharing. There is a way to use your own manager with different
39+-- simplicity and maximal connection sharing. There is a way to use your own
53−-- settings, but it requires a bit more typing.
40+-- manager with different settings, but it requires more typing.
54−--
41+--
55−-- “Type-safe” means that the library is protective and eliminates certain
42+-- “Type-safe” means that the library tries to eliminate certain classes of
56−-- classes of errors. For example, we have correct-by-construction 'Url's,
43+-- errors. For example, we have correct-by-construction URLs; it is
57−-- it's guaranteed that the user does not send the request body when using
44+-- guaranteed that the user does not send the request body when using
58−-- methods like 'GET' or 'OPTIONS', and the amount of implicit assumptions
45+-- methods like GET or OPTIONS, and the amount of implicit assumptions is
59−-- is minimized by making the user specify his\/her intentions in an
46+-- minimized by making the user specify their intentions in an explicit
60−-- explicit form (for example, it's not possible to avoid specifying the
47+-- form. For example, it's not possible to avoid specifying the body or the
61−-- body or method of request). Authentication methods that assume HTTPS
48+-- method of a request. Authentication methods that assume HTTPS force the
62−-- force the user to use HTTPS at the type level. The library also carefully
49+-- user to use HTTPS at the type level.
63−-- hides underlying types from the lower-level @http-client@ package because
64−-- those types are not safe enough (for example 'L.Request' is an instance
65−-- of 'Data.String.IsString' and, if it's malformed, it will blow up at
66−-- run-time).
67−--
50+--
68−-- “Expandable” refers to the ability of the library to be expanded without
51+-- “Expandable” refers to the ability to create new components without
69−-- having to resort to ugly hacking. For example, it's possible to define
52+-- having to resort to hacking. For example, it's possible to define your
70−-- your own HTTP methods, create new ways to construct the body of a
53+-- own HTTP methods, create new ways to construct the body of a request,
71−-- request, create new authorization options, perform a request in a
54+-- create new authorization options, perform a request in a different way,
72−-- different way, and create your own methods to parse and represent a
55+-- and create your own methods to parse a response.
73−-- response. As the user extends the library to satisfy his\/her special
74−-- needs, the new solutions will work just like the built-ins. However, all
75−-- of the common cases are also covered by the library out-of-the-box.
76−--
77−-- “High-level” means that there are less details to worry about. The
78−-- library is a result of my experiences as a Haskell consultant. Working
79−-- for several clients, who had very different projects, showed me that the
80−-- library should adapt easily to any particular style of writing Haskell
81−-- applications. For example, some people prefer throwing exceptions, while
82−-- others are concerned with purity. Just define 'handleHttpException'
83−-- accordingly when making your monad instance of 'MonadHttp' and it will
84−-- play together seamlessly. Finally, the library cuts down boilerplate
85−-- considerably, and helps you write concise, easy to read, and maintainable
86−-- code.
87--
56--
88-- === Using with other libraries
57-- === Using with other libraries
89--
58--
@@ -112,6 +81,7 @@ module Network.HTTP.Req
112 -- $making-a-request
81 -- $making-a-request
113 req,
82 req,
114 reqBr,
83 reqBr,
84+ reqCb,
115 req',
85 req',
116 withReqManager,
86 withReqManager,
117
87
@@ -149,6 +119,7 @@ module Network.HTTP.Req
149 useHttpsURI,
119 useHttpsURI,
150 useURI,
120 useURI,
151 urlQ,
121 urlQ,
122+ renderUrl,
152
123
153 -- ** Body
124 -- ** Body
154 -- $body
125 -- $body
@@ -173,11 +144,13 @@ module Network.HTTP.Req
173 -- $query-parameters
144 -- $query-parameters
174 (=:),
145 (=:),
175 queryFlag,
146 queryFlag,
147+ formToQuery,
176 QueryParam (..),
148 QueryParam (..),
177
149
178 -- *** Headers
150 -- *** Headers
179 header,
151 header,
180 attachHeader,
152 attachHeader,
153+ headerRedacted,
181
154
182 -- *** Cookies
155 -- *** Cookies
183 -- $cookies
156 -- $cookies
@@ -224,56 +197,78 @@ module Network.HTTP.Req
224
197
225 -- * Other
198 -- * Other
226 HttpException (..),
199 HttpException (..),
200+ isStatusCodeException,
227 CanHaveBody (..),
201 CanHaveBody (..),
228 Scheme (..),
202 Scheme (..),
229 )
203 )
230where
204where
231
205
232−import qualified Blaze.ByteString.Builder as BB
206+import Blaze.ByteString.Builder qualified as BB
233import Control.Applicative
207import Control.Applicative
234import Control.Arrow (first, second)
208import Control.Arrow (first, second)
235−import Control.Exception hiding (TypeError)
209+import Control.Exception hiding (Handler (..), TypeError)
210+import Control.Monad (guard, void, (>=>))
236import Control.Monad.Base
211import Control.Monad.Base
212+import Control.Monad.Catch (Handler (..), MonadCatch, MonadMask, MonadThrow)
237import Control.Monad.IO.Class
213import Control.Monad.IO.Class
238−import Control.Monad.Reader
214+import Control.Monad.IO.Unlift
215+import Control.Monad.Reader (ReaderT (ReaderT), ask, lift, runReaderT)
216+import Control.Monad.Trans.Accum (AccumT)
217+import Control.Monad.Trans.Cont (ContT)
239import Control.Monad.Trans.Control
218import Control.Monad.Trans.Control
219+import Control.Monad.Trans.Except (ExceptT)
220+import Control.Monad.Trans.Identity (IdentityT)
221+import Control.Monad.Trans.Maybe (MaybeT)
222+import Control.Monad.Trans.RWS.CPS qualified as RWS.CPS
223+import Control.Monad.Trans.RWS.Lazy qualified as RWS.Lazy
224+import Control.Monad.Trans.RWS.Strict qualified as RWS.Strict
225+import Control.Monad.Trans.Select (SelectT)
226+import Control.Monad.Trans.State.Lazy qualified as State.Lazy
227+import Control.Monad.Trans.State.Strict qualified as State.Strict
228+import Control.Monad.Trans.Writer.CPS qualified as Writer.CPS
229+import Control.Monad.Trans.Writer.Lazy qualified as Writer.Lazy
230+import Control.Monad.Trans.Writer.Strict qualified as Writer.Strict
240import Control.Retry
231import Control.Retry
241import Data.Aeson (FromJSON (..), ToJSON (..))
232import Data.Aeson (FromJSON (..), ToJSON (..))
242−import qualified Data.Aeson as A
233+import Data.Aeson qualified as A
243import Data.ByteString (ByteString)
234import Data.ByteString (ByteString)
244−import qualified Data.ByteString as B
235+import Data.ByteString qualified as B
245−import qualified Data.ByteString.Lazy as BL
236+import Data.ByteString.Lazy qualified as BL
246−import qualified Data.CaseInsensitive as CI
237+import Data.CaseInsensitive qualified as CI
247import Data.Data (Data)
238import Data.Data (Data)
239+import Data.Default.Class (def)
248import Data.Function (on)
240import Data.Function (on)
249import Data.IORef
241import Data.IORef
250import Data.Kind (Constraint, Type)
242import Data.Kind (Constraint, Type)
251import Data.List (foldl', nubBy)
243import Data.List (foldl', nubBy)
252import Data.List.NonEmpty (NonEmpty (..))
244import Data.List.NonEmpty (NonEmpty (..))
253−import qualified Data.List.NonEmpty as NE
245+import Data.List.NonEmpty qualified as NE
254import Data.Maybe (fromMaybe)
246import Data.Maybe (fromMaybe)
255import Data.Proxy
247import Data.Proxy
256−import Data.Semigroup hiding (Option, option)
248+import Data.Semigroup (Endo (..))
249+import Data.Set qualified as S
257import Data.Text (Text)
250import Data.Text (Text)
258−import qualified Data.Text as T
251+import Data.Text qualified as T
259−import qualified Data.Text.Encoding as T
252+import Data.Text.Encoding qualified as T
260import Data.Typeable (Typeable, cast)
253import Data.Typeable (Typeable, cast)
261import GHC.Generics
254import GHC.Generics
262import GHC.TypeLits
255import GHC.TypeLits
263−import qualified Language.Haskell.TH as TH
256+import Language.Haskell.TH qualified as TH
264−import qualified Language.Haskell.TH.Quote as TH
257+import Language.Haskell.TH.Quote qualified as TH
265−import qualified Language.Haskell.TH.Syntax as TH
258+import Language.Haskell.TH.Syntax qualified as TH
266−import qualified Network.Connection as NC
259+import Network.Connection qualified as NC
267−import qualified Network.HTTP.Client as L
260+import Network.HTTP.Client qualified as L
268−import qualified Network.HTTP.Client.Internal as LI
261+import Network.HTTP.Client.Internal qualified as LI
269−import qualified Network.HTTP.Client.MultipartFormData as LM
262+import Network.HTTP.Client.MultipartFormData qualified as LM
270−import qualified Network.HTTP.Client.TLS as L
263+import Network.HTTP.Client.TLS qualified as L
271−import qualified Network.HTTP.Types as Y
264+import Network.HTTP.Types qualified as Y
272import System.IO.Unsafe (unsafePerformIO)
265import System.IO.Unsafe (unsafePerformIO)
273import Text.URI (URI)
266import Text.URI (URI)
274−import qualified Text.URI as URI
267+import Text.URI qualified as URI
275−import qualified Text.URI.QQ as QQ
268+import Text.URI.QQ qualified as QQ
276−import qualified Web.Authenticate.OAuth as OAuth
269+import Web.Authenticate.OAuth qualified as OAuth
270+import Web.FormUrlEncoded (FromForm (..), ToForm (..))
271+import Web.FormUrlEncoded qualified as Form
277import Web.HttpApiData (ToHttpApiData (..))
272import Web.HttpApiData (ToHttpApiData (..))
278
273
279----------------------------------------------------------------------------
274----------------------------------------------------------------------------
@@ -298,7 +293,7 @@ import Web.HttpApiData (ToHttpApiData (..))
298--
293--
299-- @body@ is a body option such as 'NoReqBody' or 'ReqBodyJson'. The
294-- @body@ is a body option such as 'NoReqBody' or 'ReqBodyJson'. The
300-- tutorial has a section about HTTP bodies, but usage is very
295-- tutorial has a section about HTTP bodies, but usage is very
301−-- straightforward and should be clear from the examples below.
296+-- straightforward and should be clear from the examples.
302--
297--
303-- @response@ is a type hint how to make and interpret response of an HTTP
298-- @response@ is a type hint how to make and interpret response of an HTTP
304-- request. Out-of-the-box it can be the following:
299-- request. Out-of-the-box it can be the following:
@@ -426,12 +421,7 @@ req ::
426 -- | Response
421 -- | Response
427 m response
422 m response
428req method url body responseProxy options =
423req method url body responseProxy options =
429− reqBr method url body (options <> extraOptions) getHttpResponse
424+ reqCb method url body responseProxy options pure
430− where
431− extraOptions =
432− case acceptHeader responseProxy of
433− Nothing -> mempty
434− Just accept -> header "Accept" accept
435
425
436-- | A version of 'req' that does not use one of the predefined instances of
426-- | A version of 'req' that does not use one of the predefined instances of
437-- 'HttpResponse' but instead allows the user to consume @'L.Response'
427-- 'HttpResponse' but instead allows the user to consume @'L.Response'
@@ -456,7 +446,58 @@ reqBr ::
456 (L.Response L.BodyReader -> IO a) ->
446 (L.Response L.BodyReader -> IO a) ->
457 -- | Result
447 -- | Result
458 m a
448 m a
459−reqBr method url body options consume = req' method url body options $ \request manager -> do
449+reqBr method url body options consume =
450+ req' method url body options (reqHandler consume)
451+
452+-- | A version of 'req' that takes a callback to modify the 'L.Request', but
453+-- otherwise performs the request identically.
454+--
455+-- @since 3.7.0
456+reqCb ::
457+ ( MonadHttp m,
458+ HttpMethod method,
459+ HttpBody body,
460+ HttpResponse response,
461+ HttpBodyAllowed (AllowsBody method) (ProvidesBody body)
462+ ) =>
463+ -- | HTTP method
464+ method ->
465+ -- | 'Url'—location of resource
466+ Url scheme ->
467+ -- | Body of the request
468+ body ->
469+ -- | A hint how to interpret response
470+ Proxy response ->
471+ -- | Collection of optional parameters
472+ Option scheme ->
473+ -- | Callback to modify the request
474+ (L.Request -> m L.Request) ->
475+ -- | Response
476+ m response
477+reqCb method url body responseProxy options adjustRequest =
478+ req' method url body (options <> extraOptions) $ \request manager -> do
479+ request' <- adjustRequest request
480+ reqHandler getHttpResponse request' manager
481+ where
482+ extraOptions =
483+ case acceptHeader responseProxy of
484+ Nothing -> mempty
485+ Just accept -> header "Accept" accept
486+
487+-- | The default handler function that the higher-level request functions
488+-- pass to 'req''. Internal function.
489+--
490+-- @since 3.7.0
491+reqHandler ::
492+ (MonadHttp m) =>
493+ -- | How to get final result from a 'L.Response'
494+ (L.Response L.BodyReader -> IO b) ->
495+ -- | 'L.Request' to perform
496+ L.Request ->
497+ -- | 'L.Manager' to use
498+ L.Manager ->
499+ m b
500+reqHandler consume request manager = do
460 HttpConfig {..} <- getHttpConfig
501 HttpConfig {..} <- getHttpConfig
461 let wrapVanilla = handle (throwIO . VanillaHttpException)
502 let wrapVanilla = handle (throwIO . VanillaHttpException)
462 wrapExc = handle (throwIO . LI.toHttpException request)
503 wrapExc = handle (throwIO . LI.toHttpException request)
@@ -472,12 +513,23 @@ reqBr method url body options consume = req' method url body options $ \request
472 r' <- L.responseOpen request manager
513 r' <- L.responseOpen request manager
473 writeIORef rref (Just r')
514 writeIORef rref (Just r')
474 return r'
515 return r'
516+ exceptionRetryPolicies =
517+ skipAsyncExceptions
518+ ++ [ \retryStatus -> Handler $ \e ->
519+ return $ httpConfigRetryJudgeException retryStatus e
520+ ]
475 r <-
521 r <-
476 retrying
522 retrying
477 httpConfigRetryPolicy
523 httpConfigRetryPolicy
478− (\st r -> return $ httpConfigRetryJudge st r)
524+ (\retryStatus r -> return $ httpConfigRetryJudge retryStatus r)
525+ ( const
526+ ( recovering
527+ httpConfigRetryPolicy
528+ exceptionRetryPolicies
479− (const openResponse)
529+ (const openResponse)
530+ )
531+ )
480− (preview, r') <- grabPreview bodyPreviewLength r
532+ (preview, r') <- grabPreview httpConfigBodyPreviewLength r
481 mapM_ LI.throwHttp (httpConfigCheckResponse request r' preview)
533 mapM_ LI.throwHttp (httpConfigCheckResponse request r' preview)
482 consume r'
534 consume r'
483 )
535 )
@@ -526,22 +578,22 @@ req' method url body options m = do
526 nubHeaders
578 nubHeaders
527 <> getRequestMod options
579 <> getRequestMod options
528 <> getRequestMod config
580 <> getRequestMod config
529− <> getRequestMod (Womb body :: Womb "body" body)
581+ <> getRequestMod (Tagged body :: Tagged "body" body)
530 <> getRequestMod url
582 <> getRequestMod url
531− <> getRequestMod (Womb method :: Womb "method" method)
583+ <> getRequestMod (Tagged method :: Tagged "method" method)
532 request <- finalizeRequest options request'
584 request <- finalizeRequest options request'
533 withReqManager (m request)
585 withReqManager (m request)
534
586
535-- | Perform an action using the global implicit 'L.Manager' that the rest
587-- | Perform an action using the global implicit 'L.Manager' that the rest
536−-- of the library uses. This allows to reuse connections that the
588+-- of the library uses. This allows us to reuse connections that the
537-- 'L.Manager' controls.
589-- 'L.Manager' controls.
538−withReqManager :: MonadIO m => (L.Manager -> m a) -> m a
590+withReqManager :: (MonadIO m) => (L.Manager -> m a) -> m a
539−withReqManager m = liftIO (readIORef globalManager) >>= m
591+withReqManager m = m globalManager
540
592
541−-- | Global 'L.Manager' that 'req' uses. Here we just go with the default
593+-- | The global 'L.Manager' that 'req' uses. Here we just go with the
542−-- settings, so users don't need to deal with this manager stuff at all, but
594+-- default settings, so users don't need to deal with this manager stuff at
543−-- when we create a request, instance 'HttpConfig' can affect the default
595+-- all, but when we create a request, instance 'HttpConfig' can affect the
544−-- settings via 'getHttpConfig'.
596+-- default settings via 'getHttpConfig'.
545--
597--
546-- A note about safety, in case 'unsafePerformIO' looks suspicious to you.
598-- A note about safety, in case 'unsafePerformIO' looks suspicious to you.
547-- The value of 'globalManager' is named and lives on top level. This means
599-- The value of 'globalManager' is named and lives on top level. This means
@@ -549,16 +601,16 @@ withReqManager m = liftIO (readIORef globalManager) >>= m
549-- manager. From that moment on the 'IORef' will be just reused—exactly the
601-- manager. From that moment on the 'IORef' will be just reused—exactly the
550-- behavior we want here in order to maximize connection sharing. GHC could
602-- behavior we want here in order to maximize connection sharing. GHC could
551-- spoil the plan by inlining the definition, hence the @NOINLINE@ pragma.
603-- spoil the plan by inlining the definition, hence the @NOINLINE@ pragma.
552−globalManager :: IORef L.Manager
604+globalManager :: L.Manager
553globalManager = unsafePerformIO $ do
605globalManager = unsafePerformIO $ do
554 context <- NC.initConnectionContext
606 context <- NC.initConnectionContext
555 let settings =
607 let settings =
556 L.mkManagerSettingsContext
608 L.mkManagerSettingsContext
557 (Just context)
609 (Just context)
558− (NC.TLSSettingsSimple False False False)
610+ def
559 Nothing
611 Nothing
560 manager <- L.newManager settings
612 manager <- L.newManager settings
561− newIORef manager
613+ return manager
562{-# NOINLINE globalManager #-}
614{-# NOINLINE globalManager #-}
563
615
564----------------------------------------------------------------------------
616----------------------------------------------------------------------------
@@ -572,13 +624,13 @@ globalManager = unsafePerformIO $ do
572-- When writing a library, keep your API polymorphic in terms of
624-- When writing a library, keep your API polymorphic in terms of
573-- 'MonadHttp', only define instance of 'MonadHttp' in final application.
625-- 'MonadHttp', only define instance of 'MonadHttp' in final application.
574-- Another option is to use a @newtype@-wrapped monad stack and define
626-- Another option is to use a @newtype@-wrapped monad stack and define
575−-- 'MonadHttp' for it. As of version /0.4.0/, the 'Req' monad that follows
627+-- 'MonadHttp' for it. As of the version /0.4.0/, the 'Req' monad that
576−-- this strategy is provided out-of-the-box (see below).
628+-- follows this strategy is provided out-of-the-box (see below).
577
629
578-- | A type class for monads that support performing HTTP requests.
630-- | A type class for monads that support performing HTTP requests.
579-- Typically, you only need to define the 'handleHttpException' method
631-- Typically, you only need to define the 'handleHttpException' method
580-- unless you want to tweak 'HttpConfig'.
632-- unless you want to tweak 'HttpConfig'.
581−class MonadIO m => MonadHttp m where
633+class (MonadIO m) => MonadHttp m where
582 -- | This method describes how to deal with 'HttpException' that was
634 -- | This method describes how to deal with 'HttpException' that was
583 -- caught by the library. One option is to re-throw it if you are OK with
635 -- caught by the library. One option is to re-throw it if you are OK with
584 -- exceptions, but if you prefer working with something like
636 -- exceptions, but if you prefer working with something like
@@ -586,17 +638,16 @@ class MonadIO m => MonadHttp m where
586 -- 'Control.Monad.Except.throwError'.
638 -- 'Control.Monad.Except.throwError'.
587 handleHttpException :: HttpException -> m a
639 handleHttpException :: HttpException -> m a
588
640
589− -- | Return 'HttpConfig' to be used when performing HTTP requests. Default
641+ -- | Return the 'HttpConfig' to be used when performing HTTP requests.
590− -- implementation returns its 'def' value, which is described in the
642+ -- Default implementation returns its 'def' value, which is described in
591− -- documentation for the type. Common usage pattern with manually defined
643+ -- the documentation for the type. Common usage pattern with manually
592− -- 'getHttpConfig' is to return some hard-coded value, or a value
644+ -- defined 'getHttpConfig' is to return some hard-coded value, or a value
593 -- extracted from 'Control.Monad.Reader.MonadReader' if a more flexible
645 -- extracted from 'Control.Monad.Reader.MonadReader' if a more flexible
594 -- approach to configuration is desirable.
646 -- approach to configuration is desirable.
595 getHttpConfig :: m HttpConfig
647 getHttpConfig :: m HttpConfig
596 getHttpConfig = return defaultHttpConfig
648 getHttpConfig = return defaultHttpConfig
597
649
598−-- | 'HttpConfig' contains general and default settings to be used when
650+-- | 'HttpConfig' contains settings to be used when making HTTP requests.
599−-- making HTTP requests.
600data HttpConfig = HttpConfig
651data HttpConfig = HttpConfig
601 { -- | Proxy to use. By default values of @HTTP_PROXY@ and @HTTPS_PROXY@
652 { -- | Proxy to use. By default values of @HTTP_PROXY@ and @HTTPS_PROXY@
602 -- environment variables are respected, this setting overwrites them.
653 -- environment variables are respected, this setting overwrites them.
@@ -640,8 +691,8 @@ data HttpConfig = HttpConfig
640 -- | The retry policy to use for request retrying. By default 'def' is
691 -- | The retry policy to use for request retrying. By default 'def' is
641 -- used (see 'RetryPolicyM').
692 -- used (see 'RetryPolicyM').
642 --
693 --
643− -- __Note__: signature of this function was changed in the version
694+ -- __Note__: signature of this function was changed to disallow 'IO' in
644− -- /1.0.0/.
695+ -- version /1.0.0/ and then changed back to its current form in /3.1.0/.
645 --
696 --
646 -- @since 0.3.0
697 -- @since 0.3.0
647 httpConfigRetryPolicy :: RetryPolicyM IO,
698 httpConfigRetryPolicy :: RetryPolicyM IO,
@@ -652,11 +703,21 @@ data HttpConfig = HttpConfig
652 -- /1.0.0/.
703 -- /1.0.0/.
653 --
704 --
654 -- @since 0.3.0
705 -- @since 0.3.0
655− httpConfigRetryJudge :: forall b. RetryStatus -> L.Response b -> Bool
706+ httpConfigRetryJudge :: forall b. RetryStatus -> L.Response b -> Bool,
707+ -- | Similar to 'httpConfigRetryJudge', but is used to decide when to
708+ -- retry requests that resulted in an exception. By default it retries
709+ -- on response timeout and connection timeout (changed in version
710+ -- /3.8.0/).
711+ --
712+ -- @since 3.4.0
713+ httpConfigRetryJudgeException :: RetryStatus -> SomeException -> Bool,
714+ -- | Max length of preview fragment of response body.
715+ --
716+ -- @since 3.6.0
717+ httpConfigBodyPreviewLength :: forall a. (Num a) => a
656 }
718 }
657− deriving (Typeable)
658
719
659−-- | Default value of 'HttpConfig'.
720+-- | The default value of 'HttpConfig'.
660--
721--
661-- @since 2.0.0
722-- @since 2.0.0
662defaultHttpConfig :: HttpConfig
723defaultHttpConfig :: HttpConfig
@@ -678,7 +739,16 @@ defaultHttpConfig =
678 524, -- A timeout occurred
739 524, -- A timeout occurred
679 598, -- (Informal convention) Network read timeout error
740 598, -- (Informal convention) Network read timeout error
680 599 -- (Informal convention) Network connect timeout error
741 599 -- (Informal convention) Network connect timeout error
681− ]
742+ ],
743+ httpConfigRetryJudgeException = \_ e ->
744+ case fromException e of
745+ Just (L.HttpExceptionRequest _ c) ->
746+ case c of
747+ L.ResponseTimeout -> True
748+ L.ConnectionTimeout -> True
749+ _ -> False
750+ _ -> False,
751+ httpConfigBodyPreviewLength = 1024
682 }
752 }
683 where
753 where
684 statusCode = Y.statusCode . L.responseStatus
754 statusCode = Y.statusCode . L.responseStatus
@@ -691,7 +761,7 @@ instance RequestComponent HttpConfig where
691 LI.requestManagerOverride = httpConfigAltManager
761 LI.requestManagerOverride = httpConfigAltManager
692 }
762 }
693
763
694−-- | A monad that allows to run 'req' in any 'IO'-enabled monad without
764+-- | A monad that allows us to run 'req' in any 'IO'-enabled monad without
695-- having to define new instances.
765-- having to define new instances.
696--
766--
697-- @since 0.4.0
767-- @since 0.4.0
@@ -700,9 +770,19 @@ newtype Req a = Req (ReaderT HttpConfig IO a)
700 ( Functor,
770 ( Functor,
701 Applicative,
771 Applicative,
702 Monad,
772 Monad,
703− MonadIO
773+ MonadIO,
774+ MonadUnliftIO
704 )
775 )
705
776
777+-- | @since 3.7.0
778+deriving instance MonadThrow Req
779+
780+-- | @since 3.7.0
781+deriving instance MonadCatch Req
782+
783+-- | @since 3.7.0
784+deriving instance MonadMask Req
785+
706instance MonadBase IO Req where
786instance MonadBase IO Req where
707 liftBase = liftIO
787 liftBase = liftIO
708
788
@@ -717,12 +797,87 @@ instance MonadHttp Req where
717 handleHttpException = Req . lift . throwIO
797 handleHttpException = Req . lift . throwIO
718 getHttpConfig = Req ask
798 getHttpConfig = Req ask
719
799
800+-- | @since 3.10.0
801+instance (MonadHttp m, Monoid w) => MonadHttp (AccumT w m) where
802+ handleHttpException = lift . handleHttpException
803+ getHttpConfig = lift getHttpConfig
804+
805+-- | @since 3.10.0
806+instance (MonadHttp m) => MonadHttp (ContT r m) where
807+ handleHttpException = lift . handleHttpException
808+ getHttpConfig = lift getHttpConfig
809+
810+-- | @since 3.10.0
811+instance (MonadHttp m) => MonadHttp (ExceptT e m) where
812+ handleHttpException = lift . handleHttpException
813+ getHttpConfig = lift getHttpConfig
814+
815+-- | @since 3.10.0
816+instance (MonadHttp m) => MonadHttp (IdentityT m) where
817+ handleHttpException = lift . handleHttpException
818+ getHttpConfig = lift getHttpConfig
819+
820+-- | @since 3.10.0
821+instance (MonadHttp m) => MonadHttp (MaybeT m) where
822+ handleHttpException = lift . handleHttpException
823+ getHttpConfig = lift getHttpConfig
824+
825+-- | @since 3.10.0
826+instance (MonadHttp m) => MonadHttp (ReaderT r m) where
827+ handleHttpException = lift . handleHttpException
828+ getHttpConfig = lift getHttpConfig
829+
830+-- | @since 3.10.0
831+instance (MonadHttp m, Monoid w) => MonadHttp (RWS.CPS.RWST r w s m) where
832+ handleHttpException = lift . handleHttpException
833+ getHttpConfig = lift getHttpConfig
834+
835+-- | @since 3.10.0
836+instance (MonadHttp m, Monoid w) => MonadHttp (RWS.Lazy.RWST r w s m) where
837+ handleHttpException = lift . handleHttpException
838+ getHttpConfig = lift getHttpConfig
839+
840+-- | @since 3.10.0
841+instance (MonadHttp m, Monoid w) => MonadHttp (RWS.Strict.RWST r w s m) where
842+ handleHttpException = lift . handleHttpException
843+ getHttpConfig = lift getHttpConfig
844+
845+-- | @since 3.10.0
846+instance (MonadHttp m) => MonadHttp (SelectT r m) where
847+ handleHttpException = lift . handleHttpException
848+ getHttpConfig = lift getHttpConfig
849+
850+-- | @since 3.10.0
851+instance (MonadHttp m) => MonadHttp (State.Lazy.StateT s m) where
852+ handleHttpException = lift . handleHttpException
853+ getHttpConfig = lift getHttpConfig
854+
855+-- | @since 3.10.0
856+instance (MonadHttp m) => MonadHttp (State.Strict.StateT s m) where
857+ handleHttpException = lift . handleHttpException
858+ getHttpConfig = lift getHttpConfig
859+
860+-- | @since 3.10.0
861+instance (MonadHttp m, Monoid w) => MonadHttp (Writer.CPS.WriterT w m) where
862+ handleHttpException = lift . handleHttpException
863+ getHttpConfig = lift getHttpConfig
864+
865+-- | @since 3.10.0
866+instance (MonadHttp m, Monoid w) => MonadHttp (Writer.Lazy.WriterT w m) where
867+ handleHttpException = lift . handleHttpException
868+ getHttpConfig = lift getHttpConfig
869+
870+-- | @since 3.10.0
871+instance (MonadHttp m, Monoid w) => MonadHttp (Writer.Strict.WriterT w m) where
872+ handleHttpException = lift . handleHttpException
873+ getHttpConfig = lift getHttpConfig
874+
720-- | Run a computation in the 'Req' monad with the given 'HttpConfig'. In
875-- | Run a computation in the 'Req' monad with the given 'HttpConfig'. In
721−-- case of exceptional situation an 'HttpException' will be thrown.
876+-- the case of an exceptional situation an 'HttpException' will be thrown.
722--
877--
723-- @since 0.4.0
878-- @since 0.4.0
724runReq ::
879runReq ::
725− MonadIO m =>
880+ (MonadIO m) =>
726 -- | 'HttpConfig' to use
881 -- | 'HttpConfig' to use
727 HttpConfig ->
882 HttpConfig ->
728 -- | Computation to run
883 -- | Computation to run
@@ -827,7 +982,7 @@ class HttpMethod a where
827 -- | Return name of the method as a 'ByteString'.
982 -- | Return name of the method as a 'ByteString'.
828 httpMethodName :: Proxy a -> ByteString
983 httpMethodName :: Proxy a -> ByteString
829
984
830−instance HttpMethod method => RequestComponent (Womb "method" method) where
985+instance (HttpMethod method) => RequestComponent (Tagged "method" method) where
831 getRequestMod _ = Endo $ \x ->
986 getRequestMod _ = Endo $ \x ->
832 x {L.method = httpMethodName (Proxy :: Proxy method)}
987 x {L.method = httpMethodName (Proxy :: Proxy method)}
833
988
@@ -869,23 +1024,20 @@ instance HttpMethod method => RequestComponent (Womb "method" method) where
869-- > -- https://%D1%8E%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4.%D1%80%D1%84
1024-- > -- https://%D1%8E%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4.%D1%80%D1%84
870data Url (scheme :: Scheme) = Url Scheme (NonEmpty Text)
1025data Url (scheme :: Scheme) = Url Scheme (NonEmpty Text)
871 -- NOTE The second value is the path segments in reversed order.
1026 -- NOTE The second value is the path segments in reversed order.
872− deriving (Eq, Ord, Show, Data, Typeable, Generic)
1027+ deriving (Eq, Ord, Show, Data, Generic)
873
1028
874type role Url nominal
1029type role Url nominal
875
1030
876-- With template-haskell >=2.15 and text >=1.2.4 Lift can be derived, however
1031-- With template-haskell >=2.15 and text >=1.2.4 Lift can be derived, however
877−-- the derived lift forgets the type of scheme.
1032+-- the derived lift forgets the type of the scheme.
878−instance Typeable scheme => TH.Lift (Url scheme) where
1033+instance (Typeable scheme) => TH.Lift (Url scheme) where
879 lift url =
1034 lift url =
880 TH.dataToExpQ (fmap liftText . cast) url `TH.sigE` case url of
1035 TH.dataToExpQ (fmap liftText . cast) url `TH.sigE` case url of
881 Url Http _ -> [t|Url 'Http|]
1036 Url Http _ -> [t|Url 'Http|]
882 Url Https _ -> [t|Url 'Https|]
1037 Url Https _ -> [t|Url 'Https|]
883 where
1038 where
884 liftText t = TH.AppE (TH.VarE 'T.pack) <$> TH.lift (T.unpack t)
1039 liftText t = TH.AppE (TH.VarE 'T.pack) <$> TH.lift (T.unpack t)
885−
886−#if MIN_VERSION_template_haskell(2,16,0)
887− liftTyped url = TH.TExp <$> TH.lift url
1040+ liftTyped = TH.Code . TH.unsafeTExpCoerce . TH.lift
888−#endif
889
1041
890-- | Given host name, produce a 'Url' which has “http” as its scheme and
1042-- | Given host name, produce a 'Url' which has “http” as its scheme and
891-- empty path. This also sets port to @80@.
1043-- empty path. This also sets port to @80@.
@@ -897,20 +1049,33 @@ http = Url Http . pure
897https :: Text -> Url 'Https
1049https :: Text -> Url 'Https
898https = Url Https . pure
1050https = Url Https . pure
899
1051
900−-- | Grow given 'Url' appending a single path segment to it. Note that the
1052+-- | Grow a given 'Url' appending a single path segment to it. Note that the
901-- path segment can be of any type that is an instance of 'ToHttpApiData'.
1053-- path segment can be of any type that is an instance of 'ToHttpApiData'.
902infixl 5 /~
1054infixl 5 /~
903
1055
904−(/~) :: ToHttpApiData a => Url scheme -> a -> Url scheme
1056+(/~) :: (ToHttpApiData a) => Url scheme -> a -> Url scheme
905Url secure path /~ segment = Url secure (NE.cons (toUrlPiece segment) path)
1057Url secure path /~ segment = Url secure (NE.cons (toUrlPiece segment) path)
906
1058
907−-- | Type-constrained version of @('/~')@ to remove ambiguity in the cases
1059+-- | A type-constrained version of @('/~')@ to remove ambiguity in the cases
908-- when next URL piece is a 'Data.Text.Text' literal.
1060-- when next URL piece is a 'Data.Text.Text' literal.
909infixl 5 /:
1061infixl 5 /:
910
1062
911(/:) :: Url scheme -> Text -> Url scheme
1063(/:) :: Url scheme -> Text -> Url scheme
912(/:) = (/~)
1064(/:) = (/~)
913
1065
1066+-- | Render a 'Url' as 'Text'.
1067+--
1068+-- @since 3.4.0
1069+renderUrl :: Url scheme -> Text
1070+renderUrl = \case
1071+ Url Https parts ->
1072+ "https://" <> renderParts parts
1073+ Url Http parts ->
1074+ "http://" <> renderParts parts
1075+ where
1076+ renderParts parts =
1077+ T.intercalate "/" (reverse $ NE.toList parts)
1078+
914-- | The 'useHttpURI' function provides an alternative method to get 'Url'
1079-- | The 'useHttpURI' function provides an alternative method to get 'Url'
915-- (possibly with some 'Option's) from a 'URI'. This is useful when you are
1080-- (possibly with some 'Option's) from a 'URI'. This is useful when you are
916-- given a URL to query dynamically and don't know it beforehand.
1081-- given a URL to query dynamically and don't know it beforehand.
@@ -924,8 +1089,7 @@ useHttpURI uri = do
924 urlHead <- http <$> uriHost uri
1089 urlHead <- http <$> uriHost uri
925 let url = case URI.uriPath uri of
1090 let url = case URI.uriPath uri of
926 Nothing -> urlHead
1091 Nothing -> urlHead
927− Just (_, xs) ->
1092+ Just uriPath -> uriPathToUrl uriPath urlHead
928− foldl' (/:) urlHead (URI.unRText <$> NE.toList xs)
929 return (url, uriOptions uri)
1093 return (url, uriOptions uri)
930
1094
931-- | Just like 'useHttpURI', but expects the “https” scheme.
1095-- | Just like 'useHttpURI', but expects the “https” scheme.
@@ -937,10 +1101,21 @@ useHttpsURI uri = do
937 urlHead <- https <$> uriHost uri
1101 urlHead <- https <$> uriHost uri
938 let url = case URI.uriPath uri of
1102 let url = case URI.uriPath uri of
939 Nothing -> urlHead
1103 Nothing -> urlHead
940− Just (_, xs) ->
1104+ Just uriPath -> uriPathToUrl uriPath urlHead
941− foldl' (/:) urlHead (URI.unRText <$> NE.toList xs)
942 return (url, uriOptions uri)
1105 return (url, uriOptions uri)
943
1106
1107+-- | Convert URI path to a 'Url'. Internal.
1108+--
1109+-- @since 3.9.0
1110+uriPathToUrl ::
1111+ (Bool, NonEmpty (URI.RText 'URI.PathPiece)) ->
1112+ Url scheme ->
1113+ Url scheme
1114+uriPathToUrl (trailingSlash, xs) urlHead =
1115+ if trailingSlash then path /: T.empty else path
1116+ where
1117+ path = foldl' (/:) urlHead (URI.unRText <$> NE.toList xs)
1118+
944-- | A combination of 'useHttpURI' and 'useHttpsURI' for cases when scheme
1119-- | A combination of 'useHttpURI' and 'useHttpsURI' for cases when scheme
945-- is not known in advance.
1120-- is not known in advance.
946--
1121--
@@ -1057,21 +1232,21 @@ data NoReqBody = NoReqBody
1057instance HttpBody NoReqBody where
1232instance HttpBody NoReqBody where
1058 getRequestBody NoReqBody = L.RequestBodyBS B.empty
1233 getRequestBody NoReqBody = L.RequestBodyBS B.empty
1059
1234
1060−-- | This body option allows to use a JSON object as request body—probably
1235+-- | This body option allows us to use a JSON object as the request
1061−-- the most popular format right now. Just wrap a data type that is an
1236+-- body—probably the most popular format right now. Just wrap a data type
1062−-- instance of 'ToJSON' type class and you are done: it will be converted to
1237+-- that is an instance of 'ToJSON' type class and you are done: it will be
1063−-- JSON and inserted as request body.
1238+-- converted to JSON and inserted as request body.
1064--
1239--
1065-- This body option sets the @Content-Type@ header to @\"application/json;
1240-- This body option sets the @Content-Type@ header to @\"application/json;
1066-- charset=utf-8\"@ value.
1241-- charset=utf-8\"@ value.
1067newtype ReqBodyJson a = ReqBodyJson a
1242newtype ReqBodyJson a = ReqBodyJson a
1068
1243
1069−instance ToJSON a => HttpBody (ReqBodyJson a) where
1244+instance (ToJSON a) => HttpBody (ReqBodyJson a) where
1070 getRequestBody (ReqBodyJson a) = L.RequestBodyLBS (A.encode a)
1245 getRequestBody (ReqBodyJson a) = L.RequestBodyLBS (A.encode a)
1071 getRequestContentType _ = pure "application/json; charset=utf-8"
1246 getRequestContentType _ = pure "application/json; charset=utf-8"
1072
1247
1073-- | This body option streams request body from a file. It is expected that
1248-- | This body option streams request body from a file. It is expected that
1074−-- the file size does not change during the streaming.
1249+-- the file size does not change during streaming.
1075--
1250--
1076-- Using of this body option does not set the @Content-Type@ header.
1251-- Using of this body option does not set the @Content-Type@ header.
1077newtype ReqBodyFile = ReqBodyFile FilePath
1252newtype ReqBodyFile = ReqBodyFile FilePath
@@ -1096,12 +1271,11 @@ newtype ReqBodyLbs = ReqBodyLbs BL.ByteString
1096instance HttpBody ReqBodyLbs where
1271instance HttpBody ReqBodyLbs where
1097 getRequestBody (ReqBodyLbs bs) = L.RequestBodyLBS bs
1272 getRequestBody (ReqBodyLbs bs) = L.RequestBodyLBS bs
1098
1273
1099−-- | Form URL-encoded body. This can hold a collection of parameters which
1274+-- | URL-encoded body. This can hold a collection of parameters which are
1100−-- are encoded similarly to query parameters at the end of query string,
1275+-- encoded similarly to query parameters at the end of query string, with
1101−-- with the only difference that they are stored in request body. The
1276+-- the only difference that they are stored in request body. The similarity
1102−-- similarity is reflected in the API as well, as you can use the same
1277+-- is reflected in the API as well, as you can use the same combinators you
1103−-- combinators you would use to add query parameters: @('=:')@ and
1278+-- would use to add query parameters: @('=:')@ and 'queryFlag'.
1104−-- 'queryFlag'.
1105--
1279--
1106-- This body option sets the @Content-Type@ header to
1280-- This body option sets the @Content-Type@ header to
1107-- @\"application/x-www-form-urlencoded\"@ value.
1281-- @\"application/x-www-form-urlencoded\"@ value.
@@ -1120,6 +1294,13 @@ newtype FormUrlEncodedParam = FormUrlEncodedParam [(Text, Maybe Text)]
1120instance QueryParam FormUrlEncodedParam where
1294instance QueryParam FormUrlEncodedParam where
1121 queryParam name mvalue =
1295 queryParam name mvalue =
1122 FormUrlEncodedParam [(name, toQueryParam <$> mvalue)]
1296 FormUrlEncodedParam [(name, toQueryParam <$> mvalue)]
1297+ queryParamToList (FormUrlEncodedParam p) = p
1298+
1299+-- | Use 'formToQuery'.
1300+--
1301+-- @since 3.11.0
1302+instance FromForm FormUrlEncodedParam where
1303+ fromForm = Right . formToQuery
1123
1304
1124-- | Multipart form data. Please consult the
1305-- | Multipart form data. Please consult the
1125-- "Network.HTTP.Client.MultipartFormData" module for how to construct
1306-- "Network.HTTP.Client.MultipartFormData" module for how to construct
@@ -1159,7 +1340,7 @@ instance HttpBody ReqBodyMultipart where
1159-- | Create 'ReqBodyMultipart' request body from a collection of 'LM.Part's.
1340-- | Create 'ReqBodyMultipart' request body from a collection of 'LM.Part's.
1160--
1341--
1161-- @since 0.2.0
1342-- @since 0.2.0
1162−reqBodyMultipart :: MonadIO m => [LM.Part] -> m ReqBodyMultipart
1343+reqBodyMultipart :: (MonadIO m) => [LM.Part] -> m ReqBodyMultipart
1163reqBodyMultipart parts = liftIO $ do
1344reqBodyMultipart parts = liftIO $ do
1164 boundary <- LM.webkitBoundary
1345 boundary <- LM.webkitBoundary
1165 body <- LM.renderParts boundary parts
1346 body <- LM.renderParts boundary parts
@@ -1187,9 +1368,6 @@ type family ProvidesBody body :: CanHaveBody where
1187-- | This type function allows any HTTP body if method says it
1368-- | This type function allows any HTTP body if method says it
1188-- 'CanHaveBody'. When the method says it should have 'NoBody', the only
1369-- 'CanHaveBody'. When the method says it should have 'NoBody', the only
1189-- body option to use is 'NoReqBody'.
1370-- body option to use is 'NoReqBody'.
1190−--
1191−-- __Note__: users of GHC 8.0.1 and later will see a slightly more friendly
1192−-- error message when method does not allow a body and body is provided.
1193type family
1371type family
1194 HttpBodyAllowed
1372 HttpBodyAllowed
1195 (allowsBody :: CanHaveBody)
1373 (allowsBody :: CanHaveBody)
@@ -1202,8 +1380,8 @@ type family
1202 TypeError
1380 TypeError
1203 ('Text "This HTTP method does not allow attaching a request body.")
1381 ('Text "This HTTP method does not allow attaching a request body.")
1204
1382
1205−instance HttpBody body => RequestComponent (Womb "body" body) where
1383+instance (HttpBody body) => RequestComponent (Tagged "body" body) where
1206− getRequestMod (Womb body) = Endo $ \x ->
1384+ getRequestMod (Tagged body) = Endo $ \x ->
1207 x
1385 x
1208 { L.requestBody = getRequestBody body,
1386 { L.requestBody = getRequestBody body,
1209 L.requestHeaders =
1387 L.requestHeaders =
@@ -1232,8 +1410,8 @@ data Option (scheme :: Scheme)
1232 = Option (Endo (Y.QueryText, L.Request)) (Maybe (L.Request -> IO L.Request))
1410 = Option (Endo (Y.QueryText, L.Request)) (Maybe (L.Request -> IO L.Request))
1233
1411
1234-- NOTE 'QueryText' is just [(Text, Maybe Text)], we keep it along with
1412-- NOTE 'QueryText' is just [(Text, Maybe Text)], we keep it along with
1235−-- Request to avoid appending to existing query string in request every
1413+-- Request to avoid appending to an existing query string in request every
1236−-- time new parameter is added. Additional Maybe (L.Request -> IO
1414+-- time new parameter is added. The additional Maybe (L.Request -> IO
1237-- L.Request) is a finalizer that will be applied after all other
1415-- L.Request) is a finalizer that will be applied after all other
1238-- transformations. This is for authentication methods that sign requests
1416-- transformations. This is for authentication methods that sign requests
1239-- based on data in Request.
1417-- based on data in Request.
@@ -1248,6 +1426,12 @@ instance Monoid (Option scheme) where
1248 mempty = Option mempty Nothing
1426 mempty = Option mempty Nothing
1249 mappend = (<>)
1427 mappend = (<>)
1250
1428
1429+-- | Use 'formToQuery'.
1430+--
1431+-- @since 3.11.0
1432+instance FromForm (Option scheme) where
1433+ fromForm = Right . formToQuery
1434+
1251-- | A helper to create an 'Option' that modifies only collection of query
1435-- | A helper to create an 'Option' that modifies only collection of query
1252-- parameters. This helper is not a part of the public API.
1436-- parameters. This helper is not a part of the public API.
1253withQueryParams :: (Y.QueryText -> Y.QueryText) -> Option scheme
1437withQueryParams :: (Y.QueryText -> Y.QueryText) -> Option scheme
@@ -1266,7 +1450,7 @@ instance RequestComponent (Option scheme) where
1266
1450
1267-- | Finalize given 'L.Request' by applying a finalizer from the given
1451-- | Finalize given 'L.Request' by applying a finalizer from the given
1268-- 'Option' (if it has any).
1452-- 'Option' (if it has any).
1269−finalizeRequest :: MonadIO m => Option scheme -> L.Request -> m L.Request
1453+finalizeRequest :: (MonadIO m) => Option scheme -> L.Request -> m L.Request
1270finalizeRequest (Option _ mfinalizer) = liftIO . fromMaybe pure mfinalizer
1454finalizeRequest (Option _ mfinalizer) = liftIO . fromMaybe pure mfinalizer
1271
1455
1272----------------------------------------------------------------------------
1456----------------------------------------------------------------------------
@@ -1290,7 +1474,7 @@ infix 7 =:
1290(=:) :: (QueryParam param, ToHttpApiData a) => Text -> a -> param
1474(=:) :: (QueryParam param, ToHttpApiData a) => Text -> a -> param
1291name =: value = queryParam name (pure value)
1475name =: value = queryParam name (pure value)
1292
1476
1293−-- | Construct a flag, that is, valueless query parameter. For example, in
1477+-- | Construct a flag, that is, a valueless query parameter. For example, in
1294-- the following URL @\"a\"@ is a flag, while @\"b\"@ is a query parameter
1478-- the following URL @\"a\"@ is a flag, while @\"b\"@ is a query parameter
1295-- with a value:
1479-- with a value:
1296--
1480--
@@ -1299,9 +1483,27 @@ name =: value = queryParam name (pure value)
1299-- This operator is defined in terms of 'queryParam':
1483-- This operator is defined in terms of 'queryParam':
1300--
1484--
1301-- > queryFlag name = queryParam name (Nothing :: Maybe ())
1485-- > queryFlag name = queryParam name (Nothing :: Maybe ())
1302−queryFlag :: QueryParam param => Text -> param
1486+queryFlag :: (QueryParam param) => Text -> param
1303queryFlag name = queryParam name (Nothing :: Maybe ())
1487queryFlag name = queryParam name (Nothing :: Maybe ())
1304
1488
1489+-- | Construct query parameters from a 'ToForm' instance. This function
1490+-- produces the same query params as 'Form.urlEncodeAsFormStable'.
1491+--
1492+-- Note that 'Form.Form' doesn't have the concept of parameters with the
1493+-- empty value (i.e. what you can get by @key =: ""@). If the value is
1494+-- empty, it will be encoded as a valueless parameter (i.e. what you can get
1495+-- by @queryFlag key@).
1496+--
1497+-- @since 3.11.0
1498+formToQuery :: (QueryParam param, Monoid param, ToForm f) => f -> param
1499+formToQuery f = mconcat . fmap toParam . Form.toListStable $ toForm f
1500+ where
1501+ toParam (key, val) =
1502+ queryParam key $
1503+ if val == ""
1504+ then Nothing
1505+ else Just val
1506+
1305-- | A type class for query-parameter-like things. The reason to have an
1507-- | A type class for query-parameter-like things. The reason to have an
1306-- overloaded 'queryParam' is to be able to use it as an 'Option' and as a
1508-- overloaded 'queryParam' is to be able to use it as an 'Option' and as a
1307-- 'FormUrlEncodedParam' when constructing form URL encoded request bodies.
1509-- 'FormUrlEncodedParam' when constructing form URL encoded request bodies.
@@ -1311,11 +1513,17 @@ class QueryParam param where
1311 -- 'Nothing', it won't be included at all (i.e. you create a flag this
1513 -- 'Nothing', it won't be included at all (i.e. you create a flag this
1312 -- way). It's recommended to use @('=:')@ and 'queryFlag' instead of this
1514 -- way). It's recommended to use @('=:')@ and 'queryFlag' instead of this
1313 -- method, because they are easier to read.
1515 -- method, because they are easier to read.
1314− queryParam :: ToHttpApiData a => Text -> Maybe a -> param
1516+ queryParam :: (ToHttpApiData a) => Text -> Maybe a -> param
1517+
1518+ -- | Get the query parameter names and values set by 'queryParam'.
1519+ --
1520+ -- @since 3.11.0
1521+ queryParamToList :: param -> [(Text, Maybe Text)]
1315
1522
1316instance QueryParam (Option scheme) where
1523instance QueryParam (Option scheme) where
1317 queryParam name mvalue =
1524 queryParam name mvalue =
1318 withQueryParams ((:) (name, toQueryParam <$> mvalue))
1525 withQueryParams ((:) (name, toQueryParam <$> mvalue))
1526+ queryParamToList (Option f _) = fst $ appEndo f ([], L.defaultRequest)
1319
1527
1320----------------------------------------------------------------------------
1528----------------------------------------------------------------------------
1321-- Request—Optional parameters—Headers
1529-- Request—Optional parameters—Headers
@@ -1339,6 +1547,14 @@ attachHeader :: ByteString -> ByteString -> L.Request -> L.Request
1339attachHeader name value x =
1547attachHeader name value x =
1340 x {L.requestHeaders = (CI.mk name, value) : L.requestHeaders x}
1548 x {L.requestHeaders = (CI.mk name, value) : L.requestHeaders x}
1341
1549
1550+-- | Same as 'header', but with redacted values on print.
1551+--
1552+-- @since 3.13.0
1553+headerRedacted :: ByteString -> ByteString -> Option scheme
1554+headerRedacted name value = withRequest $ \x ->
1555+ let y = attachHeader name value x
1556+ in y {L.redactHeaders = CI.mk name `S.insert` L.redactHeaders y}
1557+
1342----------------------------------------------------------------------------
1558----------------------------------------------------------------------------
1343-- Request—Optional parameters—Cookies
1559-- Request—Optional parameters—Cookies
1344
1560
@@ -1480,7 +1696,7 @@ customAuth = Option mempty . pure
1480
1696
1481-- | Specify the port to connect to explicitly. Normally, 'Url' you use
1697-- | Specify the port to connect to explicitly. Normally, 'Url' you use
1482-- determines the default port: @80@ for HTTP and @443@ for HTTPS. This
1698-- determines the default port: @80@ for HTTP and @443@ for HTTPS. This
1483−-- 'Option' allows to choose an arbitrary port overwriting the defaults.
1699+-- 'Option' allows us to choose an arbitrary port overwriting the defaults.
1484port :: Int -> Option scheme
1700port :: Int -> Option scheme
1485port n = withRequest $ \x ->
1701port n = withRequest $ \x ->
1486 x {L.port = n}
1702 x {L.port = n}
@@ -1495,8 +1711,8 @@ port n = withRequest $ \x ->
1495--
1711--
1496-- > decompress (const True)
1712-- > decompress (const True)
1497decompress ::
1713decompress ::
1498− -- | Predicate that is given MIME type, it
1714+ -- | Predicate that is given MIME type, it returns 'True' when content
1499− -- returns 'True' when content should be decompressed on the fly.
1715+ -- should be decompressed on the fly.
1500 (ByteString -> Bool) ->
1716 (ByteString -> Bool) ->
1501 Option scheme
1717 Option scheme
1502decompress f = withRequest $ \x ->
1718decompress f = withRequest $ \x ->
@@ -1546,7 +1762,7 @@ ignoreResponse = Proxy
1546newtype JsonResponse a = JsonResponse (L.Response a)
1762newtype JsonResponse a = JsonResponse (L.Response a)
1547 deriving (Show)
1763 deriving (Show)
1548
1764
1549−instance FromJSON a => HttpResponse (JsonResponse a) where
1765+instance (FromJSON a) => HttpResponse (JsonResponse a) where
1550 type HttpResponseBody (JsonResponse a) = a
1766 type HttpResponseBody (JsonResponse a) = a
1551 toVanillaResponse (JsonResponse r) = r
1767 toVanillaResponse (JsonResponse r) = r
1552 getHttpResponse r = do
1768 getHttpResponse r = do
@@ -1598,7 +1814,7 @@ lbsResponse = Proxy
1598----------------------------------------------------------------------------
1814----------------------------------------------------------------------------
1599-- Helpers for response interpretations
1815-- Helpers for response interpretations
1600
1816
1601−-- | Fetch beginning of response and return it together with new
1817+-- | Fetch beginning of the response and return it together with a new
1602-- @'L.Response' 'L.BodyReader'@ that can be passed to 'getHttpResponse' and
1818-- @'L.Response' 'L.BodyReader'@ that can be passed to 'getHttpResponse' and
1603-- such.
1819-- such.
1604grabPreview ::
1820grabPreview ::
@@ -1662,14 +1878,14 @@ brReadN br n = go 0 id id
1662
1878
1663-- | Get the response body.
1879-- | Get the response body.
1664responseBody ::
1880responseBody ::
1665− HttpResponse response =>
1881+ (HttpResponse response) =>
1666 response ->
1882 response ->
1667 HttpResponseBody response
1883 HttpResponseBody response
1668responseBody = L.responseBody . toVanillaResponse
1884responseBody = L.responseBody . toVanillaResponse
1669
1885
1670-- | Get the response status code.
1886-- | Get the response status code.
1671responseStatusCode ::
1887responseStatusCode ::
1672− HttpResponse response =>
1888+ (HttpResponse response) =>
1673 response ->
1889 response ->
1674 Int
1890 Int
1675responseStatusCode =
1891responseStatusCode =
@@ -1677,7 +1893,7 @@ responseStatusCode =
1677
1893
1678-- | Get the response status message.
1894-- | Get the response status message.
1679responseStatusMessage ::
1895responseStatusMessage ::
1680− HttpResponse response =>
1896+ (HttpResponse response) =>
1681 response ->
1897 response ->
1682 ByteString
1898 ByteString
1683responseStatusMessage =
1899responseStatusMessage =
@@ -1685,7 +1901,7 @@ responseStatusMessage =
1685
1901
1686-- | Lookup a particular header from a response.
1902-- | Lookup a particular header from a response.
1687responseHeader ::
1903responseHeader ::
1688− HttpResponse response =>
1904+ (HttpResponse response) =>
1689 -- | Response interpretation
1905 -- | Response interpretation
1690 response ->
1906 response ->
1691 -- | Header to lookup
1907 -- | Header to lookup
@@ -1697,7 +1913,7 @@ responseHeader r h =
1697
1913
1698-- | Get the response 'L.CookieJar'.
1914-- | Get the response 'L.CookieJar'.
1699responseCookieJar ::
1915responseCookieJar ::
1700− HttpResponse response =>
1916+ (HttpResponse response) =>
1701 response ->
1917 response ->
1702 L.CookieJar
1918 L.CookieJar
1703responseCookieJar = L.responseCookieJar . toVanillaResponse
1919responseCookieJar = L.responseCookieJar . toVanillaResponse
@@ -1710,9 +1926,9 @@ responseCookieJar = L.responseCookieJar . toVanillaResponse
1710-- To create a new response interpretation you just need to make your data
1926-- To create a new response interpretation you just need to make your data
1711-- type an instance of the 'HttpResponse' type class.
1927-- type an instance of the 'HttpResponse' type class.
1712
1928
1713−-- | A type class for response interpretations. It allows to describe how to
1929+-- | A type class for response interpretations. It allows us to describe how
1714−-- consume response from a @'L.Response' 'L.BodyReader'@ and produce the
1930+-- to consume the response from a @'L.Response' 'L.BodyReader'@ and produce
1715−-- final result that is to be returned to the user.
1931+-- the final result that is to be returned to the user.
1716class HttpResponse response where
1932class HttpResponse response where
1717 -- | The associated type is the type of body that can be extracted from an
1933 -- | The associated type is the type of body that can be extracted from an
1718 -- instance of 'HttpResponse'.
1934 -- instance of 'HttpResponse'.
@@ -1751,6 +1967,16 @@ class HttpResponse response where
1751 acceptHeader :: Proxy response -> Maybe ByteString
1967 acceptHeader :: Proxy response -> Maybe ByteString
1752 acceptHeader Proxy = Nothing
1968 acceptHeader Proxy = Nothing
1753
1969
1970+-- | This instance has been added to make it easier to inspect 'L.Response'
1971+-- using Req's functions like 'responseStatusCode', 'responseStatusMessage',
1972+-- etc.
1973+--
1974+-- @since 3.12.0
1975+instance HttpResponse (L.Response ()) where
1976+ type HttpResponseBody (L.Response ()) = ()
1977+ toVanillaResponse = id
1978+ getHttpResponse = return . void
1979+
1754----------------------------------------------------------------------------
1980----------------------------------------------------------------------------
1755-- Other
1981-- Other
1756
1982
@@ -1776,7 +2002,7 @@ class RequestComponent a where
1776-- method@ and @'HttpBody' body => 'RequestComponent' body@ when it decides
2002-- method@ and @'HttpBody' body => 'RequestComponent' body@ when it decides
1777-- which instance to use (i.e. the constraints are taken into account later,
2003-- which instance to use (i.e. the constraints are taken into account later,
1778-- when instance is already chosen).
2004-- when instance is already chosen).
1779−newtype Womb (tag :: Symbol) a = Womb a
2005+newtype Tagged (tag :: Symbol) a = Tagged a
1780
2006
1781-- | Exceptions that this library throws.
2007-- | Exceptions that this library throws.
1782data HttpException
2008data HttpException
@@ -1785,10 +2011,24 @@ data HttpException
1785 | -- | A wrapper with Aeson-produced 'String' describing why decoding
2011 | -- | A wrapper with Aeson-produced 'String' describing why decoding
1786 -- failed
2012 -- failed
1787 JsonHttpException String
2013 JsonHttpException String
1788− deriving (Show, Typeable, Generic)
2014+ deriving (Show, Generic)
1789
2015
1790instance Exception HttpException
2016instance Exception HttpException
1791
2017
2018+-- | Return 'Just' if the given 'HttpException' is wrapping a http-client's
2019+-- 'L.StatusCodeException'. Otherwise, return 'Nothing'.
2020+--
2021+-- @since 3.12.0
2022+isStatusCodeException :: HttpException -> Maybe (L.Response ())
2023+isStatusCodeException
2024+ ( VanillaHttpException
2025+ ( L.HttpExceptionRequest
2026+ _
2027+ (L.StatusCodeException r _)
2028+ )
2029+ ) = Just r
2030+isStatusCodeException _ = Nothing
2031+
1792-- | A simple type isomorphic to 'Bool' that we only have for better error
2032-- | A simple type isomorphic to 'Bool' that we only have for better error
1793-- messages. We use it as a kind and its data constructors as type-level
2033-- messages. We use it as a kind and its data constructors as type-level
1794-- tags.
2034-- tags.
@@ -1808,11 +2048,4 @@ data Scheme
1808 Http
2048 Http
1809 | -- | HTTPS
2049 | -- | HTTPS
1810 Https
2050 Https
1811− deriving (Eq, Ord, Show, Data, Typeable, Generic, TH.Lift)
2051+ deriving (Eq, Ord, Show, Data, Generic, TH.Lift)
1812−
1813−----------------------------------------------------------------------------
1814−-- Constants
1815−
1816−-- | Max length of preview fragment of response body.
1817−bodyPreviewLength :: Num a => a
1818−bodyPreviewLength = 1024
README.md+28−124
@@ -4,10 +4,8 @@
4[![Hackage](https://img.shields.io/hackage/v/req.svg?style=flat)](https://hackage.haskell.org/package/req)
4[![Hackage](https://img.shields.io/hackage/v/req.svg?style=flat)](https://hackage.haskell.org/package/req)
5[![Stackage Nightly](http://stackage.org/package/req/badge/nightly)](http://stackage.org/nightly/package/req)
5[![Stackage Nightly](http://stackage.org/package/req/badge/nightly)](http://stackage.org/nightly/package/req)
6[![Stackage LTS](http://stackage.org/package/req/badge/lts)](http://stackage.org/lts/package/req)
6[![Stackage LTS](http://stackage.org/package/req/badge/lts)](http://stackage.org/lts/package/req)
7−[![Build Status](https://travis-ci.org/mrkkrp/req.svg?branch=master)](https://travis-ci.org/mrkkrp/req)
7+[![CI](https://github.com/mrkkrp/req/actions/workflows/ci.yaml/badge.svg)](https://github.com/mrkkrp/req/actions/workflows/ci.yaml)
8
8
9−* [Motivation and Req vs other libraries](#motivation-and-req-vs-other-libraries)
10−* [Unsolved problems](#unsolved-problems)
11* [Related packages](#related-packages)
9* [Related packages](#related-packages)
12* [Blog posts](#blog-posts)
10* [Blog posts](#blog-posts)
13* [Contribution](#contribution)
11* [Contribution](#contribution)
@@ -43,49 +41,33 @@ main = runReq defaultHttpConfig $ do
43 liftIO $ print (responseBody r :: Value)
41 liftIO $ print (responseBody r :: Value)
44```
42```
45
43
46−Req is an easy-to-use, type-safe, expandable, high-level HTTP client library
44+Req is an HTTP client library that attempts to be easy-to-use, type-safe,
47−that just works without any fooling around.
45+and expandable.
48−
46+
49−What does the phrase “easy-to-use” mean? It means that the library is
47+“Easy-to-use” means that the library is designed to be beginner-friendly so
50−designed to be beginner-friendly so it's simple to add to your monad stack,
48+it's simple to add to your monad stack, intuitive to work with,
51−intuitive to work with, well-documented, and does not get in your way. Doing
49+well-documented, and does not get in your way. Doing HTTP requests is a
52−HTTP requests is a common task and Haskell library for this should be very
50+common task and a Haskell library for this should be approachable and clear
53−approachable and clear to beginners, thus certain compromises were made. For
51+to beginners, thus certain compromises were made. For example, one cannot
54−example, one cannot currently modify `ManagerSettings` of the default
52+currently modify `ManagerSettings` of the default manager because the
55−manager because the library always uses the same implicit global manager for
53+library always uses the same implicit global manager for simplicity and
56−simplicity and maximal connection sharing. There is a way to use your own
54+maximal connection sharing. There is a way to use your own manager with
57−manager with different settings, but it requires a bit more typing.
55+different settings, but it requires more typing.
58−
56+
59−“Type-safe” means that the library is protective and eliminates certain
57+“Type-safe” means that the library tries to eliminate certain classes of
60−classes of errors. For example, we have correct-by-construction URLs, it's
58+errors. For example, we have correct-by-construction URLs; it is guaranteed
61−guaranteed that the user does not send the request body when using methods
59+that the user does not send the request body when using methods like GET or
62−like GET or OPTIONS, and the amount of implicit assumptions is minimized by
60+OPTIONS, and the amount of implicit assumptions is minimized by making the
63−making the user specify his/her intentions in an explicit form (for example,
61+user specify their intentions in an explicit form. For example, it's not
64−it's not possible to avoid specifying the body or method of request).
62+possible to avoid specifying the body or the method of a request.
65Authentication methods that assume HTTPS force the user to use HTTPS at the
63Authentication methods that assume HTTPS force the user to use HTTPS at the
66−type level. The library also carefully hides underlying types from the
64+type level.
67−lower-level `http-client` package because those types are not safe enough
65+
68−(for example `Request` is an instance of `IsString` and, if it's malformed,
69−it will blow up at run-time).
70−
71−“Expandable” refers to the ability to create new components for dealing with
66+“Expandable” refers to the ability to create new components without having
72−HTTP without having to resort to ugly hacking. For example, it's possible to
67+to resort to hacking. For example, it's possible to define your own HTTP
73−define your own HTTP methods, create new ways to construct the body of a
68+methods, create new ways to construct the body of a request, create new
74−request, create new authorization options, perform a request in a different
69+authorization options, perform a request in a different way, and create your
75−way, and create your own methods to parse and represent a response. As the
70+own methods to parse a response.
76−user extends the library to satisfy his/her special needs, the new solutions
77−will work just like the built-ins. However, all of the common cases are also
78−covered by the library out-of-the-box.
79−
80−“High-level” means that there are less details to worry about. The library
81−is a result of my experiences as a Haskell consultant. Working for several
82−clients, who had very different projects, showed me that the library should
83−adapt easily to any particular style of writing Haskell applications. For
84−example, some people prefer throwing exceptions, while others are concerned
85−with purity. Just define `handleHttpException` accordingly when making your
86−monad instance of `MonadHttp` and it will play together seamlessly. Finally,
87−the library cuts down boilerplate considerably, and helps you write concise,
88−easy to read, and maintainable code.
89
71
90The library uses the following mature packages under the hood to guarantee
72The library uses the following mature packages under the hood to guarantee
91you the best experience:
73you the best experience:
@@ -95,89 +77,11 @@ you the best experience:
95* [`http-client-tls`](https://hackage.haskell.org/package/http-client-tls)—TLS
77* [`http-client-tls`](https://hackage.haskell.org/package/http-client-tls)—TLS
96 (HTTPS) support for `http-client`.
78 (HTTPS) support for `http-client`.
97
79
98−It's important to note that since we leverage well-known libraries that the
80+It is important to note that since we leverage well-known libraries that the
99whole Haskell ecosystem uses, there is no risk in using Req. The machinery
81whole Haskell ecosystem uses, there is no risk in using Req. The machinery
100for performing requests is the same as with `http-conduit` and Wreq. The
82for performing requests is the same as with `http-conduit` and Wreq. The
101only difference is the API.
83only difference is the API.
102
84
103−## Motivation and Req vs other libraries
104−
105−*This section is my opinion and it contains criticisms of other well-known
106−libraries. If you're a user/fan of one of these libraries, please remember
107−not to react aggressively and respect the fact that I may have different
108−views on API design from yours.*
109−
110−I have spent time to write the library because sending HTTP requests is a
111−common need, but there is no high-level library for that in Haskell that I
112−could use with pleasure. I'll explain why.
113−
114−First of all, there is `http-client` and `http-client-tls`. They just work.
115−I have no issues with the libraries except that they are too low-level for
116−my taste. Indeed, even the docs say that they are low-level and “intended as
117−a base layer for more user-friendly packages”. This is exactly how I use
118−them in Req, as base level. Req is nothing but a different API to
119−`http-client`, so it only works because of the hard work put into
120−`http-client`.
121−
122−`http-conduit` definitely has its place. For one thing it allows you to
123−stream request and response bodies in constant memory, what other library
124−allows you to do that? On the other hand if you take a look at
125−`Network.HTTP.Simple`, then although it's said that it's a “higher level
126−API”, it's mostly the same as vanilla `http-client` in spirit/approach and
127−just adds `conduit`-powered functions to perform requests and allows to use
128−global implicit `Manager` (Req does the same). If I tried to frame what
129−exactly I don't like about `http-conduit` in words, then it would be “the
130−way requests are constructed”. You *set* parameters instead of *being
131−forced* to declare necessary bits and *being allowed* to declare optional
132−bits in a way that their combination is valid. Also, with `http-conduit` you
133−parse request from a string without the protection of TH that otherwise
134−saves the day as in Yesod.
135−
136−Then there is Wreq. `wreq` [doesn't see much development
137−lately](https://github.com/bos/wreq/issues/93). `wreq` is by itself a weird
138−library, IMO. You have functions per method—not very good, as there may be
139−new methods, like PATCH which is not new but still missing (well, you have
140−`customMethod`, but what is the point of having per-method functions if you
141−have a more general way to use any method? you should be able to just insert
142−methods in the “argument slot” of `customMethod` and end up with a more
143−general solution). Now, every method function has a companion that takes
144−`Options` (like you have `get` and `getWith`). Why the duplication? Where is
145−generality and flexibility? This is not all though, because you cannot
146−really use `get` you see in the main module, because you want to have
147−connection sharing. Wreq's author does not take the gift of automatic
148−connection re-use `Manager` from `http-client` provides, he invents the
149−whole new thing of “sessions”. Only inside a session your connections will
150−be shared and re-used. However with the session stuff you have yet another
151−set of per-method functions like `get` and `getWith`—these are different
152−ones, to be used with sessions! Now if you have a multi-threaded app, here
153−is a surprise for you: you can't share connections between threads as
154−connections are shared only inside of the `withSession` friend and “session
155−will no longer be valid after that function returns”. There are valid uses
156−for sessions, but the point is that they are just too inconvenient for
157−common tasks.
158−
159−I used `servant-client` a couple of times but the amount of boilerplate it
160−requires is frightening. If you have several query parameters, and you use
161−just one of them, you'll have to pass lots of `Nothing`s.
162−
163−## Unsolved problems
164−
165−AWS request signing is problematic because request body can be in the form
166−of an action to execute (and all that “popper” stuff for streaming), not
167−just a `ByteString` and so getting its digest (hash) is not trivial without
168−running the action and consuming body in its entirety before the request in
169−made. In Wreq the author chose to just use `error` when body is not a
170−(strict or lazy) `ByteString`. Maybe it's OK for Wreq, but I don't consider
171−this a proper solution for Req as we support full variety of body options.
172−For example, what if I want to upload 1 Gb file to S3? I want to stream it
173−in constant memory but at the same time I need to calculate its hash before
174−I start streaming. One solution to the problem seems to be in taking the
175−hash explicitly (as an argument of the hypothetical `awsAuth`) and making it
176−a responsibility of the user to calculate the hash correctly. I don't like
177−this because it's not user-friendly. So the question stays open, for now
178−there is no AWS signing functionality provided out-of-the-box. The best
179−solution for talking to AWS is the `amazonka` package so far.
180−
181## Related packages
85## Related packages
182
86
183The following packages are designed to be used with Req:
87The following packages are designed to be used with Req:
Setup.hsdeleted−6
@@ -1,6 +0,0 @@
1−module Main (main) where
2−
3−import Distribution.Simple
4−
5−main :: IO ()
6−main = defaultMain
cabal.projectnew file+4
@@ -0,0 +1,4 @@
1+packages: .
2+tests: True
3+benchmarks: True
4+constraints: req +dev
httpbin-tests/Network/HTTP/ReqSpec.hs+169−150
@@ -1,6 +1,6 @@
1−{-# LANGUAGE CPP #-}
2{-# LANGUAGE DataKinds #-}
1{-# LANGUAGE DataKinds #-}
3{-# LANGUAGE FlexibleInstances #-}
2{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE LambdaCase #-}
4{-# LANGUAGE OverloadedStrings #-}
4{-# LANGUAGE OverloadedStrings #-}
5{-# LANGUAGE RankNTypes #-}
5{-# LANGUAGE RankNTypes #-}
6{-# OPTIONS_GHC -fno-warn-orphans #-}
6{-# OPTIONS_GHC -fno-warn-orphans #-}
@@ -8,53 +8,55 @@
8module Network.HTTP.ReqSpec (spec) where
8module Network.HTTP.ReqSpec (spec) where
9
9
10import Control.Exception
10import Control.Exception
11−import Control.Monad.Reader
11+import Control.Monad (forM_)
12import Control.Monad.Trans.Control
12import Control.Monad.Trans.Control
13−import Data.Aeson ((.=), ToJSON (..), Value (..), object)
13+import Data.Aeson (ToJSON (..), Value (..), object, (.=))
14−import qualified Data.Aeson as A
14+import Data.Aeson qualified as A
15+import Data.Aeson.KeyMap qualified as Aeson.KeyMap
15−import qualified Data.ByteString as B
16+import Data.ByteString qualified as B
16−import qualified Data.ByteString.Lazy as BL
17+import Data.ByteString.Lazy qualified as BL
17−import qualified Data.HashMap.Strict as HM
18+import Data.Functor.Identity (runIdentity)
19+import Data.Maybe (fromJust)
18import Data.Proxy
20import Data.Proxy
19import Data.Text (Text)
21import Data.Text (Text)
20−import qualified Data.Text as T
22+import Data.Text qualified as T
21−import qualified Data.Text.Encoding as T
23+import Data.Text.Encoding qualified as T
22−import qualified Data.Text.IO as TIO
24+import Data.Text.IO qualified as TIO
23−import qualified Network.HTTP.Client as L
25+import Network.HTTP.Client qualified as L
24−import qualified Network.HTTP.Client.MultipartFormData as LM
26+import Network.HTTP.Client.MultipartFormData qualified as LM
25−import Network.HTTP.Req
27+import Network.HTTP.Req hiding (req)
28+import Network.HTTP.Req qualified as Req
26−import qualified Network.HTTP.Types as Y
29+import Network.HTTP.Types qualified as Y
27import Test.Hspec
30import Test.Hspec
28import Test.QuickCheck
31import Test.QuickCheck
29
32
30−#if !MIN_VERSION_base(4,13,0)
31−import Data.Semigroup ((<>))
32−#endif
33−
34spec :: Spec
33spec :: Spec
35spec = do
34spec = do
36− describe "exception throwing on non-2xx status codes"
35+ describe "exception throwing on non-2xx status codes" $
37− $ it "throws indeed for non-2xx"
38− $ req GET (httpbin /: "foo") NoReqBody ignoreResponse mempty
39− `shouldThrow` selector404
40−
41− describe "exception throwing on non-2xx status codes (Req monad)"
42− $ it "throws indeed for non-2xx"
36+ it "throws indeed for non-2xx" $
43− $ asIO . runReq defaultHttpConfig
44− $ liftBaseWith
45− $ \run ->
46− run (req GET (httpbin /: "foo") NoReqBody ignoreResponse mempty)
37+ req GET (httpbin /: "foo") NoReqBody ignoreResponse mempty
47 `shouldThrow` selector404
38 `shouldThrow` selector404
48
39
40+ describe "exception throwing on non-2xx status codes (Req monad)" $
41+ it "throws indeed for non-2xx" $
42+ asIO . runReq defaultHttpConfig $
43+ liftBaseWith $ \run ->
44+ run (req GET (httpbin /: "foo") NoReqBody ignoreResponse mempty)
45+ `shouldThrow` selector404
46+
49− describe "response check via httpConfigCheckResponse"
47+ describe "response check via httpConfigCheckResponse" $
50− $ context "if it's set to always throw"
48+ context "if it's set to always throw" $
51− $ it "throws indeed"
49+ it "throws indeed" $
52− $ blindlyThrowing (req GET httpbin NoReqBody ignoreResponse mempty)
50+ blindlyThrowing (req GET httpbin NoReqBody ignoreResponse mempty)
53− `shouldThrow` anyException
51+ `shouldThrow` anyException
54−
52+
53+ describe "isStatusCodeException" $
54+ it "extracts non-2xx response" $
55+ req GET (httpbin /: "foo") NoReqBody ignoreResponse mempty
56+ `shouldThrow` selector404ByStatusCodeException
57+
55− describe "receiving user-agent header back"
58+ describe "receiving user-agent header back" $
56− $ it "works"
59+ it "works" $ do
57− $ do
58 r <-
60 r <-
59 req
61 req
60 GET
62 GET
@@ -68,9 +70,8 @@ spec = do
68 responseStatusCode r `shouldBe` 200
70 responseStatusCode r `shouldBe` 200
69 responseStatusMessage r `shouldBe` "OK"
71 responseStatusMessage r `shouldBe` "OK"
70
72
71− describe "receiving request headers back"
73+ describe "receiving request headers back" $
72− $ it "works"
74+ it "works" $ do
73− $ do
74 r <-
75 r <-
75 req
76 req
76 GET
77 GET
@@ -84,34 +85,30 @@ spec = do
84 .= object
85 .= object
85 [ "Accept-Encoding" .= ("gzip" :: Text),
86 [ "Accept-Encoding" .= ("gzip" :: Text),
86 "Foo" .= ("bar" :: Text),
87 "Foo" .= ("bar" :: Text),
87− "Baz" .= ("quux" :: Text),
88+ "Baz" .= ("quux" :: Text)
88− "Host" .= ("httpbin.org" :: Text)
89 ]
89 ]
90 ]
90 ]
91 responseStatusCode r `shouldBe` 200
91 responseStatusCode r `shouldBe` 200
92 responseStatusMessage r `shouldBe` "OK"
92 responseStatusMessage r `shouldBe` "OK"
93
93
94− describe "receiving GET data back"
94+ describe "receiving GET data back" $
95− $ it "works"
95+ it "works" $ do
96− $ do
97 r <- req GET (httpbin /: "get") NoReqBody jsonResponse mempty
96 r <- req GET (httpbin /: "get") NoReqBody jsonResponse mempty
98 (stripFunnyHeaders . stripOrigin) (responseBody r)
97 (stripFunnyHeaders . stripOrigin) (responseBody r)
99 `shouldBe` object
98 `shouldBe` object
100 [ "args" .= emptyObject,
99 [ "args" .= emptyObject,
101− "url" .= ("https://httpbin.org/get" :: Text),
100+ "url" .= ("http://localhost:1234/get" :: Text),
102 "headers"
101 "headers"
103 .= object
102 .= object
104− [ "Accept-Encoding" .= ("gzip" :: Text),
103+ [ "Accept-Encoding" .= ("gzip" :: Text)
105− "Host" .= ("httpbin.org" :: Text)
106 ]
104 ]
107 ]
105 ]
108 responseHeader r "Content-Type" `shouldBe` return "application/json"
106 responseHeader r "Content-Type" `shouldBe` return "application/json"
109 responseStatusCode r `shouldBe` 200
107 responseStatusCode r `shouldBe` 200
110 responseStatusMessage r `shouldBe` "OK"
108 responseStatusMessage r `shouldBe` "OK"
111
109
112− describe "receiving POST JSON data back"
110+ describe "receiving POST JSON data back" $
113− $ it "works"
111+ it "works" $ do
114− $ do
115 let text = "foo" :: Text
112 let text = "foo" :: Text
116 reflected = reflectJSON text
113 reflected = reflectJSON text
117 r <- req POST (httpbin /: "post") (ReqBodyJson text) jsonResponse mempty
114 r <- req POST (httpbin /: "post") (ReqBodyJson text) jsonResponse mempty
@@ -120,12 +117,11 @@ spec = do
120 [ "args" .= emptyObject,
117 [ "args" .= emptyObject,
121 "json" .= text,
118 "json" .= text,
122 "data" .= reflected,
119 "data" .= reflected,
123− "url" .= ("https://httpbin.org/post" :: Text),
120+ "url" .= ("http://localhost:1234/post" :: Text),
124 "headers"
121 "headers"
125 .= object
122 .= object
126 [ "Content-Type" .= ("application/json; charset=utf-8" :: Text),
123 [ "Content-Type" .= ("application/json; charset=utf-8" :: Text),
127 "Accept-Encoding" .= ("gzip" :: Text),
124 "Accept-Encoding" .= ("gzip" :: Text),
128− "Host" .= ("httpbin.org" :: Text),
129 "Content-Length" .= show (T.length reflected)
125 "Content-Length" .= show (T.length reflected)
130 ],
126 ],
131 "files" .= emptyObject,
127 "files" .= emptyObject,
@@ -135,27 +131,25 @@ spec = do
135 responseStatusCode r `shouldBe` 200
131 responseStatusCode r `shouldBe` 200
136 responseStatusMessage r `shouldBe` "OK"
132 responseStatusMessage r `shouldBe` "OK"
137
133
138− describe "receiving POST data back (multipart form data)"
134+ describe "receiving POST data back (multipart form data)" $
139− $ it "works"
135+ it "works" $ do
140− $ do
141 body <-
136 body <-
142 reqBodyMultipart
137 reqBodyMultipart
143 [ LM.partBS "foo" "foo data!",
138 [ LM.partBS "foo" "foo data!",
144 LM.partBS "bar" "bar data!"
139 LM.partBS "bar" "bar data!"
145 ]
140 ]
146 r <- req POST (httpbin /: "post") body jsonResponse mempty
141 r <- req POST (httpbin /: "post") body jsonResponse mempty
147− let Just contentType = getRequestContentType body
142+ let contentType = fromJust (getRequestContentType body)
148 (stripFunnyHeaders . stripOrigin) (responseBody r)
143 (stripFunnyHeaders . stripOrigin) (responseBody r)
149 `shouldBe` object
144 `shouldBe` object
150 [ "args" .= emptyObject,
145 [ "args" .= emptyObject,
151 "json" .= Null,
146 "json" .= Null,
152 "data" .= ("" :: Text),
147 "data" .= ("" :: Text),
153− "url" .= ("https://httpbin.org/post" :: Text),
148+ "url" .= ("http://localhost:1234/post" :: Text),
154 "headers"
149 "headers"
155 .= object
150 .= object
156 [ "Content-Type" .= T.decodeUtf8 contentType,
151 [ "Content-Type" .= T.decodeUtf8 contentType,
157 "Accept-Encoding" .= ("gzip" :: Text),
152 "Accept-Encoding" .= ("gzip" :: Text),
158− "Host" .= ("httpbin.org" :: Text),
159 "Content-Length" .= ("242" :: Text)
153 "Content-Length" .= ("242" :: Text)
160 ],
154 ],
161 "files" .= emptyObject,
155 "files" .= emptyObject,
@@ -169,9 +163,8 @@ spec = do
169 responseStatusCode r `shouldBe` 200
163 responseStatusCode r `shouldBe` 200
170 responseStatusMessage r `shouldBe` "OK"
164 responseStatusMessage r `shouldBe` "OK"
171
165
172− describe "receiving PATCHed file back"
166+ describe "receiving PATCHed file back" $
173− $ it "works"
167+ it "works" $ do
174− $ do
175 let file :: FilePath
168 let file :: FilePath
176 file = "httpbin-data/robots.txt"
169 file = "httpbin-data/robots.txt"
177 contents <- TIO.readFile file
170 contents <- TIO.readFile file
@@ -181,11 +174,10 @@ spec = do
181 [ "args" .= emptyObject,
174 [ "args" .= emptyObject,
182 "json" .= Null,
175 "json" .= Null,
183 "data" .= contents,
176 "data" .= contents,
184− "url" .= ("https://httpbin.org/patch" :: Text),
177+ "url" .= ("http://localhost:1234/patch" :: Text),
185 "headers"
178 "headers"
186 .= object
179 .= object
187 [ "Accept-Encoding" .= ("gzip" :: Text),
180 [ "Accept-Encoding" .= ("gzip" :: Text),
188− "Host" .= ("httpbin.org" :: Text),
189 "Content-Length" .= show (T.length contents)
181 "Content-Length" .= show (T.length contents)
190 ],
182 ],
191 "files" .= emptyObject,
183 "files" .= emptyObject,
@@ -195,9 +187,8 @@ spec = do
195 responseStatusCode r `shouldBe` 200
187 responseStatusCode r `shouldBe` 200
196 responseStatusMessage r `shouldBe` "OK"
188 responseStatusMessage r `shouldBe` "OK"
197
189
198− describe "receiving PUT form URL-encoded data back"
190+ describe "receiving PUT form URL-encoded data back" $
199− $ it "works"
191+ it "works" $ do
200− $ do
201 let params =
192 let params =
202 "foo" =: ("bar" :: Text)
193 "foo" =: ("bar" :: Text)
203 <> "baz" =: (5 :: Int)
194 <> "baz" =: (5 :: Int)
@@ -208,12 +199,11 @@ spec = do
208 [ "args" .= emptyObject,
199 [ "args" .= emptyObject,
209 "json" .= Null,
200 "json" .= Null,
210 "data" .= ("" :: Text),
201 "data" .= ("" :: Text),
211− "url" .= ("https://httpbin.org/put" :: Text),
202+ "url" .= ("http://localhost:1234/put" :: Text),
212 "headers"
203 "headers"
213 .= object
204 .= object
214 [ "Content-Type" .= ("application/x-www-form-urlencoded" :: Text),
205 [ "Content-Type" .= ("application/x-www-form-urlencoded" :: Text),
215 "Accept-Encoding" .= ("gzip" :: Text),
206 "Accept-Encoding" .= ("gzip" :: Text),
216− "Host" .= ("httpbin.org" :: Text),
217 "Content-Length" .= ("18" :: Text)
207 "Content-Length" .= ("18" :: Text)
218 ],
208 ],
219 "files" .= emptyObject,
209 "files" .= emptyObject,
@@ -230,9 +220,8 @@ spec = do
230
220
231 -- TODO /delete
221 -- TODO /delete
232
222
233− describe "receiving UTF-8 encoded Unicode data"
223+ describe "receiving UTF-8 encoded Unicode data" $
234− $ it "works"
224+ it "works" $ do
235− $ do
236 r <-
225 r <-
237 req
226 req
238 GET
227 GET
@@ -248,9 +237,8 @@ spec = do
248 -- TODO /gzip
237 -- TODO /gzip
249 -- TODO /deflate
238 -- TODO /deflate
250
239
251− describe "retrying"
240+ describe "retrying" $
252− $ it "retries as many times as specified"
241+ it "retries as many times as specified" $ do
253− $ do
254 -- FIXME We no longer can count retries because all the functions
242 -- FIXME We no longer can count retries because all the functions
255 -- responsible for controlling retrying are pure now.
243 -- responsible for controlling retrying are pure now.
256 let status = 408 :: Int
244 let status = 408 :: Int
@@ -273,22 +261,17 @@ spec = do
273 -- TODO /response-headers
261 -- TODO /response-headers
274 -- TODO /redirect
262 -- TODO /redirect
275
263
276− -- FIXME Redirects test is temporarily disabled due to
277− --
278− -- https://github.com/postmanlabs/httpbin/issues/617
279−
280− -- describe "redirects"
264+ describe "redirects" $
281− -- $ it "follows redirects"
265+ it "follows redirects" $ do
282− -- $ do
283− -- r <-
266+ r <-
284− -- req
267+ req
285− -- GET
268+ GET
286− -- (httpbin /: "redirect-to")
269+ (httpbin /: "redirect-to")
287− -- NoReqBody
270+ NoReqBody
288− -- ignoreResponse
271+ ignoreResponse
289− -- ("url" =: ("https://httpbin.org" :: Text))
272+ ("url" =: ("https://httpbin.org" :: Text))
290− -- responseStatusCode r `shouldBe` 200
273+ responseStatusCode r `shouldBe` 200
291− -- responseStatusMessage r `shouldBe` "OK"
274+ responseStatusMessage r `shouldBe` "OK"
292
275
293 -- TODO /relative-redicet
276 -- TODO /relative-redicet
294 -- TODO /absolute-redirect
277 -- TODO /absolute-redirect
@@ -298,9 +281,8 @@ spec = do
298 let user, password :: Text
281 let user, password :: Text
299 user = "Scooby"
282 user = "Scooby"
300 password = "Doo"
283 password = "Doo"
301− context "when we do not send appropriate basic auth data"
284+ context "when we do not send appropriate basic auth data" $
302− $ it "fails with 401"
285+ it "fails with 401" $ do
303− $ do
304 r <-
286 r <-
305 prepareForShit $
287 prepareForShit $
306 req
288 req
@@ -311,16 +293,15 @@ spec = do
311 mempty
293 mempty
312 responseStatusCode r `shouldBe` 401
294 responseStatusCode r `shouldBe` 401
313 responseStatusMessage r `shouldBe` "UNAUTHORIZED"
295 responseStatusMessage r `shouldBe` "UNAUTHORIZED"
314− context "when we provide appropriate basic auth data"
296+ context "when we provide appropriate basic auth data" $
315− $ it "succeeds"
297+ it "succeeds" $ do
316− $ do
317 r <-
298 r <-
318 req
299 req
319 GET
300 GET
320 (httpbin /: "basic-auth" /~ user /~ password)
301 (httpbin /: "basic-auth" /~ user /~ password)
321 NoReqBody
302 NoReqBody
322 ignoreResponse
303 ignoreResponse
323− (basicAuth (T.encodeUtf8 user) (T.encodeUtf8 password))
304+ (basicAuthUnsafe (T.encodeUtf8 user) (T.encodeUtf8 password))
324 responseStatusCode r `shouldBe` 200
305 responseStatusCode r `shouldBe` 200
325 responseStatusMessage r `shouldBe` "OK"
306 responseStatusMessage r `shouldBe` "OK"
326
307
@@ -332,9 +313,8 @@ spec = do
332 -- TODO /range
313 -- TODO /range
333 -- TODO /html
314 -- TODO /html
334
315
335− describe "robots.txt"
316+ describe "robots.txt" $
336− $ it "works"
317+ it "works" $ do
337− $ do
338 r <- req GET (httpbin /: "robots.txt") NoReqBody bsResponse mempty
318 r <- req GET (httpbin /: "robots.txt") NoReqBody bsResponse mempty
339 robots <- B.readFile "httpbin-data/robots.txt"
319 robots <- B.readFile "httpbin-data/robots.txt"
340 responseBody r `shouldBe` robots
320 responseBody r `shouldBe` robots
@@ -345,9 +325,8 @@ spec = do
345 -- TODO /cache
325 -- TODO /cache
346
326
347 describe "getting random bytes" $ do
327 describe "getting random bytes" $ do
348− it "works"
328+ it "works" $
349− $ property
329+ property $ \n' -> do
350− $ \n' -> do
351 let n :: Word
330 let n :: Word
352 n = getSmall n'
331 n = getSmall n'
353 r <-
332 r <-
@@ -360,9 +339,8 @@ spec = do
360 responseBody r `shouldSatisfy` ((== n) . fromIntegral . BL.length)
339 responseBody r `shouldSatisfy` ((== n) . fromIntegral . BL.length)
361 responseStatusCode r `shouldBe` 200
340 responseStatusCode r `shouldBe` 200
362 responseStatusMessage r `shouldBe` "OK"
341 responseStatusMessage r `shouldBe` "OK"
363− context "when we try to interpret 1000 random bytes as JSON"
342+ context "when we try to interpret 1000 random bytes as JSON" $
364− $ it "throws correct exception"
343+ it "throws correct exception" $ do
365− $ do
366 let selector :: HttpException -> Bool
344 let selector :: HttpException -> Bool
367 selector (JsonHttpException _) = True
345 selector (JsonHttpException _) = True
368 selector _ = False
346 selector _ = False
@@ -376,22 +354,21 @@ spec = do
376 mempty
354 mempty
377 `shouldThrow` selector
355 `shouldThrow` selector
378
356
379− describe "streaming random bytes"
357+ describe "streaming random bytes" $
380− $ it "works"
358+ it "works" $
381− $ property
359+ property $ \n' -> do
382− $ \n' -> do
383− let n :: Word
360+ let n :: Word
384− n = getSmall n'
361+ n = getSmall n'
385− r <-
362+ r <-
386− req
363+ req
387− GET
364+ GET
388− (httpbin /: "stream-bytes" /~ n)
365+ (httpbin /: "stream-bytes" /~ n)
389− NoReqBody
366+ NoReqBody
390− bsResponse
367+ bsResponse
391− mempty
368+ mempty
392− responseBody r `shouldSatisfy` ((== n) . fromIntegral . B.length)
369+ responseBody r `shouldSatisfy` ((== n) . fromIntegral . B.length)
393− responseStatusCode r `shouldBe` 200
370+ responseStatusCode r `shouldBe` 200
394− responseStatusMessage r `shouldBe` "OK"
371+ responseStatusMessage r `shouldBe` "OK"
395
372
396-- TODO /links
373-- TODO /links
397-- TODO /image
374-- TODO /image
@@ -411,52 +388,87 @@ instance MonadHttp IO where
411----------------------------------------------------------------------------
388----------------------------------------------------------------------------
412-- Helpers
389-- Helpers
413
390
414−-- | Run request with such settings that it does not signal error on adverse
391+-- | Run a request with such settings that it does not signal errors.
415−-- response status codes.
416prepareForShit :: Req a -> IO a
392prepareForShit :: Req a -> IO a
417prepareForShit = runReq defaultHttpConfig {httpConfigCheckResponse = noNoise}
393prepareForShit = runReq defaultHttpConfig {httpConfigCheckResponse = noNoise}
418 where
394 where
419 noNoise _ _ _ = Nothing
395 noNoise _ _ _ = Nothing
420
396
421−-- | Run request with such settings that it throws on any response.
397+-- | Run a request with such settings that it throws on any response.
422blindlyThrowing :: Req a -> IO a
398blindlyThrowing :: Req a -> IO a
423blindlyThrowing = runReq defaultHttpConfig {httpConfigCheckResponse = doit}
399blindlyThrowing = runReq defaultHttpConfig {httpConfigCheckResponse = doit}
424 where
400 where
425 doit _ _ = error "Oops!"
401 doit _ _ = error "Oops!"
426
402
427-- | 'Url' representing <https://httpbin.org>.
403-- | 'Url' representing <https://httpbin.org>.
428−httpbin :: Url 'Https
404+httpbin :: Url 'Http
429−httpbin = https "httpbin.org"
405+httpbin = http "localhost"
406+
407+req ::
408+ ( MonadHttp m,
409+ HttpMethod method,
410+ HttpBody body,
411+ HttpResponse response,
412+ HttpBodyAllowed (AllowsBody method) (ProvidesBody body)
413+ ) =>
414+ method ->
415+ Url scheme ->
416+ body ->
417+ Proxy response ->
418+ Option scheme ->
419+ m response
420+req method url body responseProxy options =
421+ Req.req method url body responseProxy (options <> defaultOptions)
422+
423+-- | Options to apply by default.
424+defaultOptions :: Option scheme
425+defaultOptions = port 1234
430
426
431-- | Remove “origin” field from JSON value. Origin may change, we don't want
427-- | Remove “origin” field from JSON value. Origin may change, we don't want
432-- to depend on that.
428-- to depend on that.
433stripOrigin :: Value -> Value
429stripOrigin :: Value -> Value
434−stripOrigin (Object m) = Object (HM.delete "origin" m)
430+stripOrigin (Object m) = Object (Aeson.KeyMap.delete "origin" m)
435stripOrigin value = value
431stripOrigin value = value
436
432
437-- | Remove funny headers that might break the tests.
433-- | Remove funny headers that might break the tests.
438stripFunnyHeaders :: Value -> Value
434stripFunnyHeaders :: Value -> Value
439−stripFunnyHeaders (Object m) =
435+stripFunnyHeaders = \case
440− let f (Object p) = Object $ HM.filterWithKey (\k _ -> k `elem` hs) p
436+ Object m ->
437+ Object
438+ ( runIdentity
439+ ( Aeson.KeyMap.alterF
440+ (pure . fmap stripFunnyHeaders')
441+ "headers"
442+ m
443+ )
444+ )
445+ value -> value
446+
447+-- | Similar to 'stripFunnyHeaders', but acts directly on the argument
448+-- without trying to access its "headers" field.
449+stripFunnyHeaders' :: Value -> Value
450+stripFunnyHeaders' = \case
451+ Object p ->
452+ Object $
453+ Aeson.KeyMap.filterWithKey
454+ (\k _ -> k `elem` whitelistedHeaders)
455+ p
441− f value = value
456+ value -> value
442− hs =
457+ where
458+ whitelistedHeaders =
443− [ "Content-Type",
459+ [ "Content-Type",
444− "Accept-Encoding",
460+ "Accept-Encoding",
445− "Host",
446− "Content-Length",
461+ "Content-Length",
447− "Foo",
462+ "Foo",
448− "Baz"
463+ "Baz"
449− ]
464+ ]
450− in Object (HM.adjust f "headers" m)
451−stripFunnyHeaders value = value
452
465
453-- | This is a complete test case that makes use of <https://httpbin.org> to
466-- | This is a complete test case that makes use of <https://httpbin.org> to
454-- get various response status codes.
467-- get various response status codes.
455checkStatusCode :: Int -> SpecWith ()
468checkStatusCode :: Int -> SpecWith ()
456checkStatusCode code =
469checkStatusCode code =
457− describe ("receiving status code " ++ show code)
470+ describe ("receiving status code " ++ show code) $
458− $ it "works"
471+ it "works" $ do
459− $ do
460 r <-
472 r <-
461 prepareForShit $
473 prepareForShit $
462 req
474 req
@@ -479,12 +491,19 @@ selector404
479 L.responseStatus response == Y.status404 && not (B.null chunk)
491 L.responseStatus response == Y.status404 && not (B.null chunk)
480selector404 _ = False
492selector404 _ = False
481
493
494+-- | Same as 'selector404' except that it uses 'isStatusCodeException'.
495+selector404ByStatusCodeException :: HttpException -> Bool
496+selector404ByStatusCodeException e =
497+ case isStatusCodeException e of
498+ Nothing -> False
499+ Just r -> responseStatusCode r == 404
500+
482−-- | Empty JSON 'Object'.
501+-- | The empty JSON 'Object'.
483emptyObject :: Value
502emptyObject :: Value
484−emptyObject = Object HM.empty
503+emptyObject = Object Aeson.KeyMap.empty
485
504
486−-- | Get rendered JSON value as 'Text'.
505+-- | Get a rendered JSON value as 'Text'.
487−reflectJSON :: ToJSON a => a -> Text
506+reflectJSON :: (ToJSON a) => a -> Text
488reflectJSON = T.decodeUtf8 . BL.toStrict . A.encode
507reflectJSON = T.decodeUtf8 . BL.toStrict . A.encode
489
508
490-- | Clarify to the type checker that the inner computation is in the 'IO'
509-- | Clarify to the type checker that the inner computation is in the 'IO'
pure-tests/Network/HTTP/ReqSpec.hs+211−199
@@ -1,4 +1,3 @@
1−{-# LANGUAGE CPP #-}
2{-# LANGUAGE DataKinds #-}
1{-# LANGUAGE DataKinds #-}
3{-# LANGUAGE DeriveGeneric #-}
2{-# LANGUAGE DeriveGeneric #-}
4{-# LANGUAGE FlexibleInstances #-}
3{-# LANGUAGE FlexibleInstances #-}
@@ -14,53 +13,50 @@
14
13
15module Network.HTTP.ReqSpec (spec) where
14module Network.HTTP.ReqSpec (spec) where
16
15
17−import qualified Blaze.ByteString.Builder as BB
16+import Blaze.ByteString.Builder qualified as BB
18import Control.Exception (throwIO)
17import Control.Exception (throwIO)
19import Control.Monad
18import Control.Monad
20import Control.Retry
19import Control.Retry
21import Data.Aeson (ToJSON (..))
20import Data.Aeson (ToJSON (..))
22−import qualified Data.Aeson as A
21+import Data.Aeson qualified as A
23import Data.ByteString (ByteString)
22import Data.ByteString (ByteString)
24−import qualified Data.ByteString as B
23+import Data.ByteString qualified as B
25−import qualified Data.ByteString.Char8 as B8
24+import Data.ByteString.Char8 qualified as B8
26−import qualified Data.ByteString.Lazy as BL
25+import Data.ByteString.Lazy qualified as BL
27−import qualified Data.CaseInsensitive as CI
26+import Data.CaseInsensitive qualified as CI
28import Data.Either (isRight)
27import Data.Either (isRight)
29−import qualified Data.List.NonEmpty as NE
28+import Data.List.NonEmpty qualified as NE
30import Data.Maybe (fromJust, fromMaybe, isJust, isNothing)
29import Data.Maybe (fromJust, fromMaybe, isJust, isNothing)
31import Data.Proxy
30import Data.Proxy
32import Data.Text (Text)
31import Data.Text (Text)
33−import qualified Data.Text as T
32+import Data.Text qualified as T
34−import qualified Data.Text.Encoding as T
33+import Data.Text.Encoding qualified as T
35import Data.Time
34import Data.Time
36import Data.Typeable (Typeable, eqT)
35import Data.Typeable (Typeable, eqT)
36+import GHC.Exts (IsList (..))
37import GHC.Generics
37import GHC.Generics
38−import qualified Language.Haskell.TH as TH
38+import Language.Haskell.TH qualified as TH
39−import qualified Language.Haskell.TH.Quote as TH
39+import Language.Haskell.TH.Quote qualified as TH
40−import qualified Network.HTTP.Client as L
40+import Network.HTTP.Client qualified as L
41import Network.HTTP.Req
41import Network.HTTP.Req
42−import qualified Network.HTTP.Types as Y
42+import Network.HTTP.Types qualified as Y
43−import qualified Network.HTTP.Types.Header as Y
43+import Network.HTTP.Types.Header qualified as Y
44import Test.Hspec
44import Test.Hspec
45import Test.Hspec.Core.Spec (SpecM)
45import Test.Hspec.Core.Spec (SpecM)
46import Test.QuickCheck
46import Test.QuickCheck
47import Text.URI (URI)
47import Text.URI (URI)
48−import qualified Text.URI as URI
48+import Text.URI qualified as URI
49−import qualified Text.URI.QQ as QQ
49+import Text.URI.QQ qualified as QQ
50−
50+import Web.FormUrlEncoded qualified as F
51−#if !MIN_VERSION_base(4,13,0)
52−import Data.Semigroup ((<>))
53−#endif
54
51
55spec :: Spec
52spec :: Spec
56spec = do
53spec = do
57− describe "config"
54+ describe "config" $
58− $ it "getHttpConfig has effect on resulting request"
55+ it "getHttpConfig has effect on resulting request" $
59− $ property
56+ property $ \config -> do
60− $ \config -> do
61− request <- runReq config (req_ GET url NoReqBody mempty)
57+ request <- runReq config (req_ GET url NoReqBody mempty)
62− L.proxy request `shouldBe` httpConfigProxy config
58+ L.proxy request `shouldBe` httpConfigProxy config
63− L.redirectCount request `shouldBe` httpConfigRedirectCount config
59+ L.redirectCount request `shouldBe` httpConfigRedirectCount config
64
60
65 describe "methods" $ do
61 describe "methods" $ do
66 let mnth ::
62 let mnth ::
@@ -72,9 +68,8 @@ spec = do
72 SpecM () ()
68 SpecM () ()
73 mnth method = do
69 mnth method = do
74 let name = httpMethodName (Proxy :: Proxy method)
70 let name = httpMethodName (Proxy :: Proxy method)
75− describe (B8.unpack name)
71+ describe (B8.unpack name) $
76− $ it "affects name of HTTP method"
72+ it "affects name of HTTP method" $ do
77− $ do
78 request <- req_ method url NoReqBody mempty
73 request <- req_ method url NoReqBody mempty
79 L.method request `shouldBe` name
74 L.method request `shouldBe` name
80 mnth GET
75 mnth GET
@@ -88,48 +83,42 @@ spec = do
88 mnth PATCH
83 mnth PATCH
89
84
90 describe "urls" $ do
85 describe "urls" $ do
91− describe "http"
86+ describe "http" $
92− $ it "sets all the params correctly"
87+ it "sets all the params correctly" $
93− $ property
94− $ \host -> do
88+ property $ \host -> do
95− request <- req_ GET (http host) NoReqBody mempty
89+ request <- req_ GET (http host) NoReqBody mempty
96− L.secure request `shouldBe` False
90+ L.secure request `shouldBe` False
97− L.port request `shouldBe` 80
91+ L.port request `shouldBe` 80
98− L.host request `shouldBe` urlEncode host
92+ L.host request `shouldBe` urlEncode host
99− describe "https"
93+ describe "https" $
100− $ it "sets all the params correctly"
94+ it "sets all the params correctly" $
101− $ property
102− $ \host -> do
95+ property $ \host -> do
103− request <- req_ GET (https host) NoReqBody mempty
96+ request <- req_ GET (https host) NoReqBody mempty
104− L.secure request `shouldBe` True
97+ L.secure request `shouldBe` True
105− L.port request `shouldBe` 443
98+ L.port request `shouldBe` 443
106− L.host request `shouldBe` urlEncode host
99+ L.host request `shouldBe` urlEncode host
107− describe "(/~)"
100+ describe "(/~)" $
108− $ it "attaches a path piece that is URL-encoded"
101+ it "attaches a path piece that is URL-encoded" $
109− $ property
110− $ \host pieces -> do
102+ property $ \host pieces -> do
111− let url' = foldl (/~) (https host) pieces
103+ let url' = foldl (/~) (https host) pieces
112− request <- req_ GET url' NoReqBody mempty
104+ request <- req_ GET url' NoReqBody mempty
113− L.host request `shouldBe` urlEncode host
105+ L.host request `shouldBe` urlEncode host
114− L.path request `shouldBe` encodePathPieces pieces
106+ L.path request `shouldBe` encodePathPieces pieces
115− describe "(/:)"
107+ describe "(/:)" $
116− $ it "attaches a path piece that is URL-encoded"
108+ it "attaches a path piece that is URL-encoded" $
117− $ property
118− $ \host pieces -> do
109+ property $ \host pieces -> do
119− let url' = foldl (/:) (https host) pieces
110+ let url' = foldl (/:) (https host) pieces
120− request <- req_ GET url' NoReqBody mempty
111+ request <- req_ GET url' NoReqBody mempty
121− L.host request `shouldBe` urlEncode host
112+ L.host request `shouldBe` urlEncode host
122− L.path request `shouldBe` encodePathPieces pieces
113+ L.path request `shouldBe` encodePathPieces pieces
123
114
124 describe "useHttpURI" $ do
115 describe "useHttpURI" $ do
125− it "does not recognize non-http schemes"
116+ it "does not recognize non-http schemes" $
126− $ property
117+ property $ \uri ->
127− $ \uri ->
128 when (URI.uriScheme uri /= Just [QQ.scheme|http|]) $
118 when (URI.uriScheme uri /= Just [QQ.scheme|http|]) $
129 useHttpURI uri `shouldSatisfy` isNothing
119 useHttpURI uri `shouldSatisfy` isNothing
130− it "accepts correct URLs"
120+ it "accepts correct URLs" $
131− $ property
121+ property $ \uri' -> do
132− $ \uri' -> do
133 unless (isRight (URI.uriAuthority uri')) discard
122 unless (isRight (URI.uriAuthority uri')) discard
134 let uri = uri' {URI.uriScheme = Just [QQ.scheme|http|]}
123 let uri = uri' {URI.uriScheme = Just [QQ.scheme|http|]}
135 (url', options) = fromJust (useHttpURI uri)
124 (url', options) = fromJust (useHttpURI uri)
@@ -141,14 +130,12 @@ spec = do
141 lookup "Authorization" (L.requestHeaders request)
130 lookup "Authorization" (L.requestHeaders request)
142 `shouldBe` uriBasicAuth uri
131 `shouldBe` uriBasicAuth uri
143 describe "useHttpsURI" $ do
132 describe "useHttpsURI" $ do
144− it "does not recognize non-https schemes"
133+ it "does not recognize non-https schemes" $
145− $ property
134+ property $ \uri ->
146− $ \uri ->
147 when (URI.uriScheme uri /= Just [QQ.scheme|https|]) $
135 when (URI.uriScheme uri /= Just [QQ.scheme|https|]) $
148 useHttpsURI uri `shouldSatisfy` isNothing
136 useHttpsURI uri `shouldSatisfy` isNothing
149− it "parses correct URLs"
137+ it "parses correct URLs" $
150− $ property
138+ property $ \uri' -> do
151− $ \uri' -> do
152 unless (isRight (URI.uriAuthority uri')) discard
139 unless (isRight (URI.uriAuthority uri')) discard
153 let uri = uri' {URI.uriScheme = Just [QQ.scheme|https|]}
140 let uri = uri' {URI.uriScheme = Just [QQ.scheme|https|]}
154 (url', options) = fromJust (useHttpsURI uri)
141 (url', options) = fromJust (useHttpsURI uri)
@@ -160,27 +147,27 @@ spec = do
160 lookup "Authorization" (L.requestHeaders request)
147 lookup "Authorization" (L.requestHeaders request)
161 `shouldBe` uriBasicAuth uri
148 `shouldBe` uriBasicAuth uri
162 describe "useURI" $ do
149 describe "useURI" $ do
163− it "does not recognize non-http and non-https schemes"
150+ it "does not recognize non-http and non-https schemes" $
164− $ property
151+ property $ \uri ->
165− $ \uri ->
166 when
152 when
167 ( ( URI.uriScheme uri /= Just [QQ.scheme|http|]
153 ( ( URI.uriScheme uri /= Just [QQ.scheme|http|]
168 && (URI.uriScheme uri /= Just [QQ.scheme|https|])
154 && (URI.uriScheme uri /= Just [QQ.scheme|https|])
169 )
155 )
170 )
156 )
171 $ useURI uri `shouldSatisfy` isNothing
157 $ useURI uri `shouldSatisfy` isNothing
172− it "parses correct URLs"
158+ it "parses correct URLs" $
173− $ property
159+ property $ \uri' -> do
174− $ \uri' -> do
175 unless (isRight (URI.uriAuthority uri')) discard
160 unless (isRight (URI.uriAuthority uri')) discard
176 let uriHttp = uri' {URI.uriScheme = Just [QQ.scheme|http|]}
161 let uriHttp = uri' {URI.uriScheme = Just [QQ.scheme|http|]}
177 uriHttps = uri' {URI.uriScheme = Just [QQ.scheme|https|]}
162 uriHttps = uri' {URI.uriScheme = Just [QQ.scheme|https|]}
178 requestHttp <-
163 requestHttp <-
179− let Left (url', options) = fromJust (useURI uriHttp)
164+ case fromJust (useURI uriHttp) of
180− in req_ GET url' NoReqBody options
165+ Left (url', options) -> req_ GET url' NoReqBody options
166+ _ -> error "(useURI uriHttp) should have returned Left"
181 requestHttps <-
167 requestHttps <-
182− let Right (url', options) = fromJust (useURI uriHttps)
168+ case fromJust (useURI uriHttps) of
183− in req_ GET url' NoReqBody options
169+ Right (url', options) -> req_ GET url' NoReqBody options
170+ _ -> error "(useURI uriHttps) should have returned Right"
184 L.host requestHttp `shouldBe` uriHost uriHttp
171 L.host requestHttp `shouldBe` uriHost uriHttp
185 L.host requestHttps `shouldBe` uriHost uriHttps
172 L.host requestHttps `shouldBe` uriHost uriHttps
186 L.port requestHttp `shouldBe` uriPort 80 uriHttp
173 L.port requestHttp `shouldBe` uriPort 80 uriHttp
@@ -194,57 +181,83 @@ spec = do
194 lookup "Authorization" (L.requestHeaders requestHttps)
181 lookup "Authorization" (L.requestHeaders requestHttps)
195 `shouldBe` uriBasicAuth uriHttps
182 `shouldBe` uriBasicAuth uriHttps
196
183
184+ describe "renderUrl" $ do
185+ context "http" $ do
186+ context "empty path" $
187+ it "renders correctly" $ do
188+ let (uriHttp, _) = [urlQ|http://httpbin.org|]
189+ renderUrl uriHttp `shouldBe` "http://httpbin.org"
190+ context "non-empty path" $
191+ it "renders correctly" $ do
192+ let (uriHttp, _) = [urlQ|http://httpbin.org/here/we/go|]
193+ renderUrl uriHttp `shouldBe` "http://httpbin.org/here/we/go"
194+ context "http" $ do
195+ context "empty path" $
196+ it "renders correctly" $ do
197+ let (uriHttp, _) = [urlQ|https://httpbin.org|]
198+ renderUrl uriHttp `shouldBe` "https://httpbin.org"
199+ context "non-empty path" $
200+ it "renders correctly" $ do
201+ let (uriHttp, _) = [urlQ|https://httpbin.org/here/we/go|]
202+ renderUrl uriHttp `shouldBe` "https://httpbin.org/here/we/go"
203+
197 describe "bodies" $ do
204 describe "bodies" $ do
198− describe "NoReqBody"
205+ describe "NoReqBody" $
199− $ it "sets body to empty byte string"
206+ it "sets body to empty byte string" $ do
200− $ do
201 request <- req_ POST url NoReqBody mempty
207 request <- req_ POST url NoReqBody mempty
202 case L.requestBody request of
208 case L.requestBody request of
203 L.RequestBodyBS x -> x `shouldBe` B.empty
209 L.RequestBodyBS x -> x `shouldBe` B.empty
204 _ -> expectationFailure "Wrong request body constructor."
210 _ -> expectationFailure "Wrong request body constructor."
205− describe "ReqBodyJson"
211+ describe "ReqBodyJson" $
206− $ it "sets body to correct lazy byte string"
212+ it "sets body to correct lazy byte string" $
207− $ property
208− $ \thing -> do
213+ property $ \thing -> do
209− request <- req_ POST url (ReqBodyJson thing) mempty
214+ request <- req_ POST url (ReqBodyJson thing) mempty
210− case L.requestBody request of
215+ case L.requestBody request of
211− L.RequestBodyLBS x -> x `shouldBe` A.encode (thing :: Thing)
216+ L.RequestBodyLBS x -> x `shouldBe` A.encode (thing :: Thing)
212− _ -> expectationFailure "Wrong request body constructor."
217+ _ -> expectationFailure "Wrong request body constructor."
213− describe "ReqBodyBs"
218+ describe "ReqBodyBs" $
214− $ it "sets body to specified strict byte string"
219+ it "sets body to specified strict byte string" $
215− $ property
220+ property $ \bs -> do
216− $ \bs -> do
217− request <- req_ POST url (ReqBodyBs bs) mempty
221+ request <- req_ POST url (ReqBodyBs bs) mempty
218− case L.requestBody request of
222+ case L.requestBody request of
219− L.RequestBodyBS x -> x `shouldBe` bs
223+ L.RequestBodyBS x -> x `shouldBe` bs
220− _ -> expectationFailure "Wrong request body constructor."
224+ _ -> expectationFailure "Wrong request body constructor."
221− describe "ReqBodyLbs"
225+ describe "ReqBodyLbs" $
222− $ it "sets body to specified lazy byte string"
226+ it "sets body to specified lazy byte string" $
223− $ property
224− $ \lbs -> do
227+ property $ \lbs -> do
225− request <- req_ POST url (ReqBodyLbs lbs) mempty
228+ request <- req_ POST url (ReqBodyLbs lbs) mempty
226− case L.requestBody request of
229+ case L.requestBody request of
227− L.RequestBodyLBS x -> x `shouldBe` lbs
230+ L.RequestBodyLBS x -> x `shouldBe` lbs
228− _ -> expectationFailure "Wrong request body constructor."
231+ _ -> expectationFailure "Wrong request body constructor."
229− describe "ReqBodyUrlEnc"
232+ describe "ReqBodyUrlEnc" $
230− $ it "sets body to correct lazy byte string"
233+ it "sets body to correct lazy byte string" $
231− $ property
232− $ \params -> do
234+ property $ \params -> do
233− request <- req_ POST url (ReqBodyUrlEnc (formUrlEnc params)) mempty
235+ request <- req_ POST url (ReqBodyUrlEnc (formUrlEnc params)) mempty
234− case L.requestBody request of
236+ case L.requestBody request of
235− L.RequestBodyLBS x -> x `shouldBe` renderQuery params
237+ L.RequestBodyLBS x -> x `shouldBe` renderQuery params
236− _ -> expectationFailure "Wrong request body constructor."
238+ _ -> expectationFailure "Wrong request body constructor."
239+
240+ describe "query params" $ do
241+ describe "FormUrlEncodedParam" $ do
242+ describe "formToQuery" $ do
243+ it "should produce the same parameters as F.urlEncodeFormStable" $
244+ property $ \form -> do
245+ request <- req_ POST url (ReqBodyUrlEnc $ formToQuery form) mempty
246+ case L.requestBody request of
247+ L.RequestBodyLBS x -> x `shouldBe` F.urlEncodeFormStable form
248+ _ -> expectationFailure "Wrong request body constructor"
249+ specParamToList (Proxy :: Proxy FormUrlEncodedParam)
250+ describe "Option" $ do
251+ specParamToList (Proxy :: Proxy (Option 'Http))
237
252
238 describe "optional parameters" $ do
253 describe "optional parameters" $ do
239 describe "header" $ do
254 describe "header" $ do
240− it "sets specified header value"
255+ it "sets specified header value" $
241− $ property
242− $ \name value -> do
256+ property $ \name value -> do
243 request <- req_ GET url NoReqBody (header name value)
257 request <- req_ GET url NoReqBody (header name value)
244 lookup (CI.mk name) (L.requestHeaders request) `shouldBe` pure value
258 lookup (CI.mk name) (L.requestHeaders request) `shouldBe` pure value
245− it "left header wins"
259+ it "left header wins" $
246− $ property
247− $ \name value0 value1 -> do
260+ property $ \name value0 value1 -> do
248 request <-
261 request <-
249 req_
262 req_
250 GET
263 GET
@@ -252,9 +265,8 @@ spec = do
252 NoReqBody
265 NoReqBody
253 (header name value0 <> header name value1)
266 (header name value0 <> header name value1)
254 lookup (CI.mk name) (L.requestHeaders request) `shouldBe` pure value0
267 lookup (CI.mk name) (L.requestHeaders request) `shouldBe` pure value0
255− it "overwrites headers set by other parts of the lib"
268+ it "overwrites headers set by other parts of the lib" $
256− $ property
269+ property $ \value -> do
257− $ \value -> do
258 request <-
270 request <-
259 req_
271 req_
260 POST
272 POST
@@ -262,26 +274,19 @@ spec = do
262 (ReqBodyUrlEnc mempty)
274 (ReqBodyUrlEnc mempty)
263 (header "Content-Type" value)
275 (header "Content-Type" value)
264 lookup "Content-Type" (L.requestHeaders request) `shouldBe` pure value
276 lookup "Content-Type" (L.requestHeaders request) `shouldBe` pure value
265− describe "cookieJar"
277+ describe "cookieJar" $
266− $ it "cookie jar is set without modifications"
278+ it "cookie jar is set without modifications" $
267− $ property
279+ property $ \cjar -> do
268− $ \cjar -> do
269− request <- req_ GET url NoReqBody (cookieJar cjar)
280+ request <- req_ GET url NoReqBody (cookieJar cjar)
270−#if MIN_VERSION_http_client(0,7,0)
271− L.cookieJar request `shouldSatisfy` (maybe False (L.equalCookieJar cjar))
281+ L.cookieJar request `shouldSatisfy` (maybe False (L.equalCookieJar cjar))
272−#else
273− L.cookieJar request `shouldBe` Just cjar
274−#endif
275 describe "basicAuth" $ do
282 describe "basicAuth" $ do
276− it "sets Authorization header to correct value"
283+ it "sets Authorization header to correct value" $
277− $ property
278− $ \username password -> do
284+ property $ \username password -> do
279 request <- req_ GET url NoReqBody (basicAuth username password)
285 request <- req_ GET url NoReqBody (basicAuth username password)
280 lookup "Authorization" (L.requestHeaders request)
286 lookup "Authorization" (L.requestHeaders request)
281 `shouldBe` Just (basicAuthHeader username password)
287 `shouldBe` Just (basicAuthHeader username password)
282− it "overwrites manual setting of header"
288+ it "overwrites manual setting of header" $
283− $ property
284− $ \username password value -> do
289+ property $ \username password value -> do
285 request0 <-
290 request0 <-
286 req_
291 req_
287 GET
292 GET
@@ -297,9 +302,8 @@ spec = do
297 let result = Just (basicAuthHeader username password)
302 let result = Just (basicAuthHeader username password)
298 lookup "Authorization" (L.requestHeaders request0) `shouldBe` result
303 lookup "Authorization" (L.requestHeaders request0) `shouldBe` result
299 lookup "Authorization" (L.requestHeaders request1) `shouldBe` result
304 lookup "Authorization" (L.requestHeaders request1) `shouldBe` result
300− it "left auth option wins"
305+ it "left auth option wins" $
301− $ property
302− $ \username0 password0 username1 password1 -> do
306+ property $ \username0 password0 username1 password1 -> do
303 request <-
307 request <-
304 req_
308 req_
305 GET
309 GET
@@ -309,15 +313,13 @@ spec = do
309 lookup "Authorization" (L.requestHeaders request)
313 lookup "Authorization" (L.requestHeaders request)
310 `shouldBe` Just (basicAuthHeader username0 password0)
314 `shouldBe` Just (basicAuthHeader username0 password0)
311 describe "oAuth2Bearer" $ do
315 describe "oAuth2Bearer" $ do
312− it "sets Authorization header to correct value"
316+ it "sets Authorization header to correct value" $
313− $ property
317+ property $ \token -> do
314− $ \token -> do
315 request <- req_ GET url NoReqBody (oAuth2Bearer token)
318 request <- req_ GET url NoReqBody (oAuth2Bearer token)
316 lookup "Authorization" (L.requestHeaders request)
319 lookup "Authorization" (L.requestHeaders request)
317 `shouldBe` pure ("Bearer " <> token)
320 `shouldBe` pure ("Bearer " <> token)
318− it "overwrites manual setting of header"
321+ it "overwrites manual setting of header" $
319− $ property
320− $ \token value -> do
322+ property $ \token value -> do
321 request0 <-
323 request0 <-
322 req_
324 req_
323 GET
325 GET
@@ -335,9 +337,8 @@ spec = do
335 `shouldBe` pure result
337 `shouldBe` pure result
336 lookup "Authorization" (L.requestHeaders request1)
338 lookup "Authorization" (L.requestHeaders request1)
337 `shouldBe` pure result
339 `shouldBe` pure result
338− it "left auth option wins"
340+ it "left auth option wins" $
339− $ property
340− $ \token0 token1 -> do
341+ property $ \token0 token1 -> do
341 request <-
342 request <-
342 req_
343 req_
343 GET
344 GET
@@ -346,23 +347,20 @@ spec = do
346 (oAuth2Bearer token0 <> oAuth2Bearer token1)
347 (oAuth2Bearer token0 <> oAuth2Bearer token1)
347 lookup "Authorization" (L.requestHeaders request)
348 lookup "Authorization" (L.requestHeaders request)
348 `shouldBe` pure ("Bearer " <> token0)
349 `shouldBe` pure ("Bearer " <> token0)
349− describe "ProxyAuthorization"
350+ describe "ProxyAuthorization" $
350− $ it "sets Authorization header to correct value"
351+ it "sets Authorization header to correct value" $
351− $ property
352− $ \username password -> do
352+ property $ \username password -> do
353− request <- req_ GET url NoReqBody (basicProxyAuth username password)
353+ request <- req_ GET url NoReqBody (basicProxyAuth username password)
354− lookup "Proxy-Authorization" (L.requestHeaders request)
354+ lookup "Proxy-Authorization" (L.requestHeaders request)
355− `shouldBe` pure (basicProxyAuthHeader username password)
355+ `shouldBe` pure (basicProxyAuthHeader username password)
356 describe "oAuth2Token" $ do
356 describe "oAuth2Token" $ do
357− it "sets Authorization header to correct value"
357+ it "sets Authorization header to correct value" $
358− $ property
358+ property $ \token -> do
359− $ \token -> do
360 request <- req_ GET url NoReqBody (oAuth2Token token)
359 request <- req_ GET url NoReqBody (oAuth2Token token)
361 lookup "Authorization" (L.requestHeaders request)
360 lookup "Authorization" (L.requestHeaders request)
362 `shouldBe` pure ("token " <> token)
361 `shouldBe` pure ("token " <> token)
363− it "overwrites manual setting of header"
362+ it "overwrites manual setting of header" $
364− $ property
365− $ \token value -> do
363+ property $ \token value -> do
366 request0 <-
364 request0 <-
367 req_
365 req_
368 GET
366 GET
@@ -380,9 +378,8 @@ spec = do
380 `shouldBe` pure result
378 `shouldBe` pure result
381 lookup "Authorization" (L.requestHeaders request1)
379 lookup "Authorization" (L.requestHeaders request1)
382 `shouldBe` pure result
380 `shouldBe` pure result
383− it "left auth option wins"
381+ it "left auth option wins" $
384− $ property
385− $ \token0 token1 -> do
382+ property $ \token0 token1 -> do
386 request <-
383 request <-
387 req_
384 req_
388 GET
385 GET
@@ -391,27 +388,24 @@ spec = do
391 (oAuth2Token token0 <> oAuth2Token token1)
388 (oAuth2Token token0 <> oAuth2Token token1)
392 lookup "Authorization" (L.requestHeaders request)
389 lookup "Authorization" (L.requestHeaders request)
393 `shouldBe` pure ("token " <> token0)
390 `shouldBe` pure ("token " <> token0)
394− describe "port"
391+ describe "port" $
395− $ it "sets port overwriting the defaults"
392+ it "sets port overwriting the defaults" $
396− $ property
397− $ \n -> do
393+ property $ \n -> do
398− request <- req_ GET url NoReqBody (port n)
394+ request <- req_ GET url NoReqBody (port n)
399− L.port request `shouldBe` n
395+ L.port request `shouldBe` n
400− describe "decompress"
396+ describe "decompress" $
401− $ it "sets decompress function overwriting the defaults"
397+ it "sets decompress function overwriting the defaults" $
402− $ property
403− $ \token -> do
398+ property $ \token -> do
404− request <- req_ GET url NoReqBody (decompress (/= token))
399+ request <- req_ GET url NoReqBody (decompress (/= token))
405− L.decompress request token `shouldBe` False
400+ L.decompress request token `shouldBe` False
406 -- FIXME Can't really test responseTimeout right new because the
401 -- FIXME Can't really test responseTimeout right new because the
407 -- ResponseTimeout data type does not implement Eq and its constructors
402 -- ResponseTimeout data type does not implement Eq and its constructors
408 -- are also not exported. Sent a PR.
403 -- are also not exported. Sent a PR.
409− describe "httpVersion"
404+ describe "httpVersion" $
410− $ it "sets HTTP version overwriting the defaults"
405+ it "sets HTTP version overwriting the defaults" $
411− $ property
412− $ \major minor -> do
406+ property $ \major minor -> do
413− request <- req_ GET url NoReqBody (httpVersion major minor)
407+ request <- req_ GET url NoReqBody (httpVersion major minor)
414− L.requestVersion request `shouldBe` Y.HttpVersion major minor
408+ L.requestVersion request `shouldBe` Y.HttpVersion major minor
415
409
416 describe "quasiquoter" $ do
410 describe "quasiquoter" $ do
417 it "works for valid urls" $
411 it "works for valid urls" $
@@ -419,12 +413,12 @@ spec = do
419 -- 'Https, Option _) type checks, so we can catch if the type of scheme
413 -- 'Https, Option _) type checks, so we can catch if the type of scheme
420 -- is unspecified.
414 -- is unspecified.
421 let testTypeOfQuoterResult ::
415 let testTypeOfQuoterResult ::
422− forall a s. Typeable a => (a, Option s) -> Bool
416+ forall a s. (Typeable a) => (a, Option s) -> Bool
423 testTypeOfQuoterResult _ = isJust $ eqT @a @(Url 'Https)
417 testTypeOfQuoterResult _ = isJust $ eqT @a @(Url 'Https)
424 in property $ testTypeOfQuoterResult [urlQ|https://example.org/|]
418 in property $ testTypeOfQuoterResult [urlQ|https://example.org/|]
425− it "doesn't work for invalid urls"
419+ it "doesn't work for invalid urls" $
426− $ property
420+ property $
427− $ TH.runQ (TH.quoteExp urlQ "not a url") `shouldThrow` anyIOException
421+ TH.runQ (TH.quoteExp urlQ "not a url") `shouldThrow` anyIOException
428
422
429----------------------------------------------------------------------------
423----------------------------------------------------------------------------
430-- Instances
424-- Instances
@@ -440,6 +434,8 @@ instance Arbitrary HttpConfig where
440 httpConfigCheckResponse _ _ _ = Nothing
434 httpConfigCheckResponse _ _ _ = Nothing
441 httpConfigRetryPolicy = retryPolicyDefault
435 httpConfigRetryPolicy = retryPolicyDefault
442 httpConfigRetryJudge _ _ = False
436 httpConfigRetryJudge _ _ = False
437+ httpConfigRetryJudgeException _ _ = False
438+ httpConfigBodyPreviewLength = 1024
443 return HttpConfig {..}
439 return HttpConfig {..}
444
440
445instance Show HttpConfig where
441instance Show HttpConfig where
@@ -507,6 +503,9 @@ instance Arbitrary Day where
507instance Arbitrary DiffTime where
503instance Arbitrary DiffTime where
508 arbitrary = secondsToDiffTime <$> arbitrary
504 arbitrary = secondsToDiffTime <$> arbitrary
509
505
506+instance Arbitrary F.Form where
507+ arbitrary = (F.Form . fromList) <$> arbitrary
508+
510----------------------------------------------------------------------------
509----------------------------------------------------------------------------
511-- Helper types
510-- Helper types
512
511
@@ -574,13 +573,14 @@ uriPort def uri =
574 maybe def (fromIntegral) $
573 maybe def (fromIntegral) $
575 either (const Nothing) Just (URI.uriAuthority uri) >>= URI.authPort
574 either (const Nothing) Just (URI.uriAuthority uri) >>= URI.authPort
576
575
577−-- | Get path from 'URI'.
576+-- | Get the path from a 'URI'.
578uriPath :: URI -> ByteString
577uriPath :: URI -> ByteString
579uriPath uri = fromMaybe "" $ do
578uriPath uri = fromMaybe "" $ do
580− (_, xs) <- URI.uriPath uri
579+ (trailingSlash, xs) <- URI.uriPath uri
581− (return . encodePathPieces . fmap URI.unRText . NE.toList) xs
580+ let pref = (encodePathPieces . fmap URI.unRText . NE.toList) xs
581+ return $ if trailingSlash then pref <> "/" else pref
582
582
583−-- | Get query string from 'URI'.
583+-- | Get the query string from a 'URI'.
584uriQuery :: URI -> ByteString
584uriQuery :: URI -> ByteString
585uriQuery uri = do
585uriQuery uri = do
586 let liftQueryParam = \case
586 let liftQueryParam = \case
@@ -618,3 +618,15 @@ basicProxyAuthHeader :: ByteString -> ByteString -> ByteString
618basicProxyAuthHeader username password =
618basicProxyAuthHeader username password =
619 fromJust . lookup Y.hProxyAuthorization . L.requestHeaders $
619 fromJust . lookup Y.hProxyAuthorization . L.requestHeaders $
620 L.applyBasicProxyAuth username password L.defaultRequest
620 L.applyBasicProxyAuth username password L.defaultRequest
621+
622+-- | Spec about 'paramToList' for the type @p@.
623+specParamToList :: (QueryParam p, Monoid p) => Proxy p -> Spec
624+specParamToList typeProxy = do
625+ describe "paramToList" $ do
626+ it "should reproduce the parameters given by queryParam" $
627+ property $ \(QueryParams params) -> do
628+ let queryParam0 =
629+ (mconcat $ fmap (uncurry queryParam) params)
630+ `asProxyTypeOf` typeProxy
631+ got = queryParamToList queryParam0
632+ got `shouldBe` params
req.cabal+68−70
@@ -1,19 +1,15 @@
1−cabal-version: 1.18
1+cabal-version: 2.4
2name: req
2name: req
3−version: 3.3.0
3+version: 3.13.4
4−license: BSD3
4+license: BSD-3-Clause
5license-file: LICENSE.md
5license-file: LICENSE.md
6maintainer: Mark Karpov <markkarpov92@gmail.com>
6maintainer: Mark Karpov <markkarpov92@gmail.com>
7author: Mark Karpov <markkarpov92@gmail.com>
7author: Mark Karpov <markkarpov92@gmail.com>
8−tested-with: ghc ==8.6.5 ghc ==8.8.3 ghc ==8.10.1
8+tested-with: ghc ==9.6.3 ghc ==9.8.2 ghc ==9.10.1
9homepage: https://github.com/mrkkrp/req
9homepage: https://github.com/mrkkrp/req
10bug-reports: https://github.com/mrkkrp/req/issues
10bug-reports: https://github.com/mrkkrp/req/issues
11−synopsis:
11+synopsis: HTTP client library
12− Easy-to-use, type-safe, expandable, high-level HTTP client library
13−
14−description:
12+description: HTTP client library.
15− Easy-to-use, type-safe, expandable, high-level HTTP client library.
16−
17category: Network, Web
13category: Network, Web
18build-type: Simple
14build-type: Simple
19data-files:
15data-files:
@@ -35,95 +31,97 @@ flag dev
35
31
36library
32library
37 exposed-modules: Network.HTTP.Req
33 exposed-modules: Network.HTTP.Req
38− default-language: Haskell2010
34+ default-language: GHC2021
39 build-depends:
35 build-depends:
40− aeson >=0.9 && <1.6,
36+ aeson >=0.9 && <3,
41− authenticate-oauth >=1.5 && <1.7,
37+ authenticate-oauth >=1.5 && <1.8,
42− base >=4.12 && <5.0,
38+ base >=4.15 && <5,
43 blaze-builder >=0.3 && <0.5,
39 blaze-builder >=0.3 && <0.5,
44− bytestring >=0.10.8 && <0.11,
40+ bytestring >=0.10.8 && <0.13,
45 case-insensitive >=0.2 && <1.3,
41 case-insensitive >=0.2 && <1.3,
42+ containers >=0.5 && <0.8,
46− connection >=0.2.2 && <0.4,
43+ crypton-connection >=0.4.2 && <0.5,
44+ data-default-class >=0.2.0.0,
45+ exceptions >=0.6 && <0.11,
47− http-api-data >=0.2 && <0.5,
46+ http-api-data >=0.2 && <0.7,
48− http-client >=0.5 && <0.8,
47+ http-client >=0.7.13.1 && <0.8,
49 http-client-tls >=0.3.2 && <0.4,
48 http-client-tls >=0.3.2 && <0.4,
50− http-types >=0.8 && <10.0,
49+ http-types >=0.8 && <10,
51 modern-uri >=0.3 && <0.4,
50 modern-uri >=0.3 && <0.4,
52 monad-control >=1.0 && <1.1,
51 monad-control >=1.0 && <1.1,
53 mtl >=2.0 && <3.0,
52 mtl >=2.0 && <3.0,
54− retry >=0.8 && <0.9,
53+ retry >=0.8 && <0.10,
55− template-haskell >=2.14 && <2.17,
54+ template-haskell >=2.19 && <2.23,
56− text >=0.2 && <1.3,
55+ text >=0.2 && <2.2,
57− time >=1.2 && <1.10,
58− transformers >=0.4 && <0.6,
56+ transformers >=0.5.3.0 && <0.7,
59− transformers-base -any
57+ transformers-base,
58+ unliftio-core >=0.1.1 && <0.3
60
59
61 if flag(dev)
60 if flag(dev)
62− ghc-options: -O0 -Wall -Werror
61+ ghc-options:
62+ -Wall -Werror -Wpartial-fields -Wunused-packages
63+ -Wno-unused-imports
63
64
64 else
65 else
65 ghc-options: -O2 -Wall
66 ghc-options: -O2 -Wall
66
67
67− if flag(dev)
68− ghc-options:
69− -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns
70− -Wnoncanonical-monad-instances
71−
72test-suite pure-tests
68test-suite pure-tests
73− type: exitcode-stdio-1.0
69+ type: exitcode-stdio-1.0
74− main-is: Spec.hs
70+ main-is: Spec.hs
75− build-tools: hspec-discover >=2.0 && <3.0
71+ build-tool-depends: hspec-discover:hspec-discover >=2 && <3
76− hs-source-dirs: pure-tests
72+ hs-source-dirs: pure-tests
77− other-modules: Network.HTTP.ReqSpec
73+ other-modules: Network.HTTP.ReqSpec
78− default-language: Haskell2010
74+ default-language: GHC2021
79 build-depends:
75 build-depends:
80− QuickCheck >=2.7 && <3.0,
76+ QuickCheck >=2.7 && <3,
81− aeson >=0.9 && <1.6,
77+ aeson >=0.9 && <3,
82− base >=4.12 && <5.0,
78+ base >=4.15 && <5.0,
83 blaze-builder >=0.3 && <0.5,
79 blaze-builder >=0.3 && <0.5,
84− bytestring >=0.10.8 && <0.11,
80+ bytestring >=0.10.8 && <0.13,
85 case-insensitive >=0.2 && <1.3,
81 case-insensitive >=0.2 && <1.3,
86− hspec >=2.0 && <3.0,
82+ hspec >=2.0 && <3,
87− hspec-core >=2.0 && <3.0,
83+ hspec-core >=2.0 && <3,
84+ http-api-data >=0.2 && <0.7,
88− http-client >=0.5 && <0.8,
85+ http-client >=0.7 && <0.8,
89− http-types >=0.8 && <10.0,
86+ http-types >=0.8 && <10,
90 modern-uri >=0.3 && <0.4,
87 modern-uri >=0.3 && <0.4,
91− mtl >=2.0 && <3.0,
92− req -any,
88+ req,
93− retry >=0.8 && <0.9,
89+ retry >=0.8 && <0.10,
94− template-haskell >=2.14 && <2.17,
90+ template-haskell >=2.19 && <2.23,
95− text >=0.2 && <1.3,
91+ text >=0.2 && <2.2,
96− time >=1.2 && <1.10
92+ time >=1.2 && <1.15
97
93
98 if flag(dev)
94 if flag(dev)
99− ghc-options: -O0 -Wall -Werror
95+ ghc-options:
96+ -Wall -Werror -Wredundant-constraints -Wpartial-fields
97+ -Wunused-packages
100
98
101 else
99 else
102 ghc-options: -O2 -Wall
100 ghc-options: -O2 -Wall
103
101
104test-suite httpbin-tests
102test-suite httpbin-tests
105− type: exitcode-stdio-1.0
103+ type: exitcode-stdio-1.0
106− main-is: Spec.hs
104+ main-is: Spec.hs
107− build-tools: hspec-discover >=2.0 && <3.0
105+ build-tool-depends: hspec-discover:hspec-discover >=2 && <3
108− hs-source-dirs: httpbin-tests
106+ hs-source-dirs: httpbin-tests
109− other-modules: Network.HTTP.ReqSpec
107+ other-modules: Network.HTTP.ReqSpec
110− default-language: Haskell2010
108+ default-language: GHC2021
111 build-depends:
109 build-depends:
112− QuickCheck >=2.7 && <3.0,
110+ QuickCheck >=2.7 && <3,
113− aeson >=0.9 && <1.6,
111+ aeson >=2 && <3,
114− base >=4.12 && <5.0,
112+ base >=4.15 && <5,
115− bytestring >=0.10.8 && <0.11,
113+ bytestring >=0.10.8 && <0.13,
116 hspec >=2.0 && <3.0,
114 hspec >=2.0 && <3.0,
117− http-client >=0.5 && <0.8,
115+ http-client >=0.7 && <0.8,
118− http-types >=0.8 && <10.0,
116+ http-types >=0.8 && <10,
119 monad-control >=1.0 && <1.1,
117 monad-control >=1.0 && <1.1,
120− mtl >=2.0 && <3.0,
121− req -any,
118+ req,
122− text >=0.2 && <1.3,
119+ text >=0.2 && <2.2
123− unordered-containers >=0.2.5 && <0.2.12
124
120
125 if flag(dev)
121 if flag(dev)
126− ghc-options: -O0 -Wall -Werror
122+ ghc-options:
123+ -Wall -Werror -Wredundant-constraints -Wpartial-fields
124+ -Wunused-packages
127
125
128 else
126 else
129− ghc-options: -O2 -Wall
127+ buildable: False