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+14
@@ -1,10 +1,19 @@
1*#
1*#
2+*.aux
2*.chi
3*.chi
3*.chs.h
4*.chs.h
5+*.dyn_hi
6+*.dyn_o
7+*.eventlog
4*.hi
8*.hi
9+*.hp
5*.o
10*.o
11+*.prof
6*~
12*~
13+.HTF/
7.cabal-sandbox/
14.cabal-sandbox/
15+.ghc.environment.*
16+.hpc
8.hsenv
17.hsenv
9.stack-work/
18.stack-work/
10.virtualenv
19.virtualenv
@@ -12,5 +21,10 @@ TAGS
12benchmarks.tix
21benchmarks.tix
13cabal-dev
22cabal-dev
14cabal.config
23cabal.config
24+cabal.project.local
15cabal.sandbox.config
25cabal.sandbox.config
26+dist-*/
16dist/
27dist/
28+hie.yaml
29+stack.yaml
30+stack.yaml.lock
.travis.ymldeleted−46
@@ -1,46 +0,0 @@
1−language: c
2−
3−sudo: false
4−
5−cache:
6− directories:
7− - ~/.ghc
8− - ~/.cabal
9−
10−matrix:
11− include:
12− - env: CABALVER=1.24 GHCVER=7.8.4
13− addons: {apt: {packages: [cabal-install-1.24,ghc-7.8.4], sources: [hvr-ghc]}}
14− - env: CABALVER=1.24 GHCVER=7.10.3
15− addons: {apt: {packages: [cabal-install-1.24,ghc-7.10.3],sources: [hvr-ghc]}}
16− - env: CABALVER=1.24 GHCVER=8.0.2
17− addons: {apt: {packages: [cabal-install-1.24,ghc-8.0.2], sources: [hvr-ghc]}}
18− - env: CABALVER=1.24 GHCVER=8.2.1
19− addons: {apt: {packages: [cabal-install-1.24,ghc-8.2.1], sources: [hvr-ghc]}}
20−
21−before_install:
22− - export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$PATH
23−
24−install:
25− - cabal --version
26− - echo "$(ghc --version) [$(ghc --print-project-git-commit-id 2> /dev/null || echo '?')]"
27− - travis_retry cabal update
28− - cabal install --only-dependencies --enable-tests
29− - cabal install hpc-coveralls
30−
31−script:
32− - cabal configure --enable-tests --enable-coverage -v2 -f dev
33− - cabal build
34− - cabal test --show-details=always
35− - cabal sdist
36− - cabal haddock | grep "100%" | wc -l | grep "1"
37−
38−after_script:
39− - export PATH=~/.cabal/bin:$PATH
40− - hpc-coveralls --coverage-mode=StrictlyFullLines
41− --exclude-dir=pure-tests
42− --exclude-dir=httpbin-tests
43− pure-tests httpbin-tests
44−
45−notifications:
46− email: false
CHANGELOG.md+201−7
@@ -1,7 +1,201 @@
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+
83+## Req 3.4.0
84+
85+* Requests using `DELETE` method can now have a body. [Issue
86+ 89](https://github.com/mrkkrp/req/issues/89).
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+
95+## Req 3.3.0
96+
97+* Derived `Show` instances for response types `IgnoreResponse`,
98+ `JsonResponse`, `BsResponse`, and `LbsResponse`.
99+
100+## Req 3.2.0
101+
102+* Made the tests pass with `http-client-0.7` and later.
103+
104+* Added a quasiquoter for URL creation, `urlQ`.
105+
106+## Req 3.1.0
107+
108+* Changed signature of `httpConfigRetryPolicy` to `RetryPolicyM IO`.
109+
110+## Req 3.0.0
111+
112+* Dropped functions `parseUrlHttp`, `parseUrlHttps`, and `parseUrl`. Instead
113+ we now have `useHttpURI`, `useHttpsURI`, and `useURI` take `URI`s from
114+ `modern-uri` as their argument. You first parse your URL with the
115+ `modern-uri` package and then pass it to those functions. This allows us
116+ to work with typed URI representations and seamlessly convert them to
117+ something `req` can work with. As a side effect basic auth from the `URI`s
118+ is now taken into consideration. In the future we may also start to
119+ respect fragments if `http-client` starts to support this.
120+
121+* Dropped support for GHC 8.2 and older.
122+
123+## Req 2.1.0
124+
125+* Dropped support for GHC 7.10.
126+
127+* Added the new `acceptHeader` method to the `HttpResponse` type class.
128+ Notably, the `jsonResponse` method now sets `"Accept"` header to
129+ `"application/json"`.
130+
131+## Req 2.0.1
132+
133+* Fixed the `httpbin` tests (they changed something on the server again).
134+
135+## Req 2.0.0
136+
137+* Got rid of `data-default-class` dependency, now we export
138+ `defaultHttpConfig` instead.
139+
140+## Req 1.2.1
141+
142+* Fixed a typo in the type signature of `parseUrl`.
143+
144+## Req 1.2.0
145+
146+* Added the `parseUrl` function.
147+
148+## Req 1.1.0
149+
150+* Added `customAuth` and `attachHeader` to facilitate creation of custom
151+ authentication options.
152+
153+* Added `basicProxyAuth` authentication option.
154+
155+## Req 1.0.0
156+
157+* Added the `reqBr` function allowing to consume `Response BodyReader`
158+ without using a pre-defined instance of `HttpResponse`, in a custom way.
159+
160+* Now streaming of response body does not happen until we've checked headers
161+ and status code with `httpConfigCheckResponse`. It also doesn't happen on
162+ every retry. Streaming and obtaining of final response value happens only
163+ once when we're happy with everything.
164+
165+ Previously we first tried to consume and interpret response body before
166+ checking status code and determining whether we should retry the request.
167+ This was not good, because we could expect a JSON response but get a
168+ response with status code 500, and then still we would try to parse it as
169+ JSON first before letting `httpConfigCheckResponse` throw an exception.
170+
171+ The corrected behavior should also make retrying more efficient.
172+
173+* Changed signatures of several fields of `HttpConfig`:
174+ `httpConfigCheckResponse`, `httpConfigRetryPolicy`, and
175+ `httpConfigRetryJudge` in order to eliminate redundant `IO` and prevent
176+ the possibility that these functions could start consuming `BodyReader`.
177+
178+* Removed the `makeResponsePreview` method from the `HttpResponse` type
179+ class. Preview business is handled by the library automatically on a lower
180+ level now. Users do not need to concern themselves with such stuff.
181+
182+* Changed the type signature of the `getHttpResponse` method of the
183+ `HttpResponse` type class. Previously it left too much freedom (and
184+ responsibility) to implementers of the method. In fact, we now limit what
185+ `getHttpResponse` does to just consuming and interpreting `Response
186+ BodyReader`, so we can properly control details of connection
187+ opening/closing etc., for the user.
188+
189+* Dropped support for GHC 7.8.
190+
191+* Minor documentation improvements.
192+
1## Req 0.5.0
193## Req 0.5.0
2
194
3* Changed the signature of the `makeResponseBodyPreview` from `response ->
195* Changed the signature of the `makeResponseBodyPreview` from `response ->
4− IO ByteString` to `ByteString`.
196+ IO ByteString` to `response -> ByteString`.
197+
198+* Minor documentation improvements.
5
199
6## Req 0.4.0
200## Req 0.4.0
7
201
@@ -39,12 +233,12 @@
39
233
40## Req 0.2.0
234## Req 0.2.0
41
235
42−* Added support for multipart form data in form of `ReqBodyMultipart` body
236+* Added support for multipart form data in the form of `ReqBodyMultipart`
43− option and `reqBodyMultipart` helper function. This also required a change
237+ body option and `reqBodyMultipart` helper function. This also required a
44− in type signature of `getRequestContentType`, which now takes `body`, not
238+ change in the type signature of `getRequestContentType`, which now takes
45− `Proxy body` because we need to extract boundary from `body` and put it
239+ `body`, not `Proxy body` because we need to extract boundary from `body`
46− into `Content-Type` header. This change, however, shouldn't be too
240+ and put it into `Content-Type` header. This change, however, shouldn't be
47− dangerous for end-users.
241+ too dangerous for end-users.
48
242
49* Added support for OAuth 1.0 authentication via `oAuth1` option.
243* Added support for OAuth 1.0 authentication via `oAuth1` option.
50
244
LICENSE.md+1−1
@@ -1,4 +1,4 @@
1−Copyright © 2016–2017 Mark Karpov
1+Copyright © 2016–present Mark Karpov
2
2
3All rights reserved.
3All rights reserved.
4
4
Network/HTTP/Req.hs+1212−733
@@ -1,6 +1,16 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE LambdaCase #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE QuasiQuotes #-}
5+{-# LANGUAGE RecordWildCards #-}
6+{-# LANGUAGE RoleAnnotations #-}
7+{-# LANGUAGE TemplateHaskellQuotes #-}
8+{-# LANGUAGE TypeFamilies #-}
9+{-# LANGUAGE UndecidableInstances #-}
10+
1-- |
11-- |
2-- Module : Network.HTTP.Req
12-- Module : Network.HTTP.Req
3−-- Copyright : © 2016–2017 Mark Karpov
13+-- Copyright : © 2016–present Mark Karpov
4-- License : BSD 3 clause
14-- License : BSD 3 clause
5--
15--
6-- Maintainer : Mark Karpov <markkarpov92@gmail.com>
16-- Maintainer : Mark Karpov <markkarpov92@gmail.com>
@@ -9,61 +19,40 @@
9--
19--
10-- 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
11-- important information is presented first: you learn how to do HTTP
21-- important information is presented first: you learn how to do HTTP
12−-- 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
13-- 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
14-- documentation is written with sufficient coverage of details and
24-- documentation is written with sufficient coverage of details and
15-- 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.
16--
26--
17−-- /(A modest intro goes here, click on 'req' to start making requests.)/
18−--
19-- === About the library
27-- === About the library
20--
28--
21−-- Req is an easy-to-use, type-safe, expandable, high-level HTTP library
29+-- Req is an HTTP client library that attempts to be easy-to-use, type-safe,
22−-- that just works without any fooling around.
30+-- and expandable.
23−--
31+--
24−-- 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
25−-- 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,
26−-- 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
27−-- way. Doing HTTP requests is a common task and a Haskell library for this
35+-- common task and a Haskell library for this should be approachable and
28−-- should be very approachable and clear to beginners, thus certain
29−-- compromises were made. For example, one cannot currently modify
36+-- clear to beginners, thus certain compromises were made. For example, one
30−-- 'L.ManagerSettings' of the default manager because the library always
37+-- cannot currently modify 'L.ManagerSettings' of the default manager
31−-- uses the same implicit global manager for simplicity and maximal
38+-- because the library always uses the same implicit global manager for
32−-- 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
33−-- settings, but it requires a bit more typing.
40+-- manager with different settings, but it requires more typing.
34−--
41+--
35−-- “Type-safe” means that the library is protective and eliminates certain
42+-- “Type-safe” means that the library tries to eliminate certain classes of
36−-- classes of errors. For example, we have correct-by-construction 'Url's,
43+-- errors. For example, we have correct-by-construction URLs; it is
37−-- 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
38−-- methods like 'GET' or 'OPTIONS', and the amount of implicit assumptions
45+-- methods like GET or OPTIONS, and the amount of implicit assumptions is
39−-- is minimized by making the user specify his\/her intentions in an
46+-- minimized by making the user specify their intentions in an explicit
40−-- 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
41−-- body or method of a request). Authentication methods that assume HTTPS
48+-- method of a request. Authentication methods that assume HTTPS force the
42−-- force the user to use HTTPS at the type level. The library also carefully
49+-- user to use HTTPS at the type level.
43−-- hides underlying types from the lower-level @http-client@ package because
44−-- those types are not safe enough (for example 'L.Request' is an instance
45−-- of 'Data.String.IsString' and, if it's malformed, it will blow up at
46−-- run-time).
47−--
50+--
48−-- “Expandable” refers to the ability of the library to be expanded without
51+-- “Expandable” refers to the ability to create new components without
49−-- 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
50−-- 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,
51−-- request, create new authorization options, perform a request in a
54+-- create new authorization options, perform a request in a different way,
52−-- different way, and create your own methods to parse and represent a
55+-- and create your own methods to parse a response.
53−-- response. As a user extends the library to satisfy his\/her special
54−-- needs, the new solutions will work just like the built-ins. However, all
55−-- of the common cases are also covered by the library out-of-the-box.
56−--
57−-- “High-level” means that there are less details to worry about. The
58−-- library is a result of my experiences as a Haskell consultant. Working
59−-- for several clients, who had very different projects, showed me that the
60−-- library should adapt easily to any particular style of writing Haskell
61−-- applications. For example, some people prefer throwing exceptions, while
62−-- others are concerned with purity. Just define 'handleHttpException'
63−-- accordingly when making your monad instance of 'MonadHttp' and it will
64−-- play together seamlessly. Finally, the library cuts boilerplate down
65−-- considerably, and helps you write concise, easy to read, and maintainable
66−-- code.
67--
56--
68-- === Using with other libraries
57-- === Using with other libraries
69--
58--
@@ -87,186 +76,207 @@
87-- the whole Haskell ecosystem uses, there is no risk in using @req@. The
76-- the whole Haskell ecosystem uses, there is no risk in using @req@. The
88-- machinery for performing requests is the same as with @http-conduit@ and
77-- machinery for performing requests is the same as with @http-conduit@ and
89-- @wreq@. The only difference is the API.
78-- @wreq@. The only difference is the API.
90−
91−{-# LANGUAGE CPP #-}
92−{-# LANGUAGE DataKinds #-}
93−{-# LANGUAGE DeriveDataTypeable #-}
94−{-# LANGUAGE DeriveGeneric #-}
95−{-# LANGUAGE FlexibleInstances #-}
96−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
97−{-# LANGUAGE KindSignatures #-}
98−{-# LANGUAGE MultiParamTypeClasses #-}
99−{-# LANGUAGE OverloadedStrings #-}
100−{-# LANGUAGE RankNTypes #-}
101−{-# LANGUAGE RecordWildCards #-}
102−{-# LANGUAGE ScopedTypeVariables #-}
103−{-# LANGUAGE TypeFamilies #-}
104−
105−#if MIN_VERSION_base(4,9,0)
106−{-# LANGUAGE UndecidableInstances #-}
107−#endif
108−
109−#if __GLASGOW_HASKELL__ < 710
110−{-# LANGUAGE ConstraintKinds #-}
111−#endif
112−
113−#if __GLASGOW_HASKELL__ >= 800
114−{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
115−#endif
116−
117module Network.HTTP.Req
79module Network.HTTP.Req
118 ( -- * Making a request
80 ( -- * Making a request
119 -- $making-a-request
81 -- $making-a-request
120− req
82+ req,
83+ reqBr,
84+ reqCb,
121− , req'
85+ req',
122− , withReqManager
86+ withReqManager,
87+
123− -- * Embedding requests into your monad
88+ -- * Embedding requests in your monad
124 -- $embedding-requests
89 -- $embedding-requests
125− , MonadHttp (..)
90+ MonadHttp (..),
126− , HttpConfig (..)
91+ HttpConfig (..),
92+ defaultHttpConfig,
127− , Req
93+ Req,
128− , runReq
94+ runReq,
95+
129 -- * Request
96 -- * Request
97+
130 -- ** Method
98 -- ** Method
131 -- $method
99 -- $method
132− , GET (..)
100+ GET (..),
133− , POST (..)
101+ POST (..),
134− , HEAD (..)
102+ HEAD (..),
135− , PUT (..)
103+ PUT (..),
136− , DELETE (..)
104+ DELETE (..),
137− , TRACE (..)
105+ TRACE (..),
138− , CONNECT (..)
106+ CONNECT (..),
139− , OPTIONS (..)
107+ OPTIONS (..),
140− , PATCH (..)
108+ PATCH (..),
141− , HttpMethod (..)
109+ HttpMethod (..),
110+
142 -- ** URL
111 -- ** URL
143 -- $url
112 -- $url
144− , Url
113+ Url,
145− , http
114+ http,
146− , https
115+ https,
147− , (/~)
116+ (/~),
148− , (/:)
117+ (/:),
149− , parseUrlHttp
118+ useHttpURI,
150− , parseUrlHttps
119+ useHttpsURI,
120+ useURI,
121+ urlQ,
122+ renderUrl,
123+
151 -- ** Body
124 -- ** Body
152 -- $body
125 -- $body
153− , NoReqBody (..)
126+ NoReqBody (..),
154− , ReqBodyJson (..)
127+ ReqBodyJson (..),
155− , ReqBodyFile (..)
128+ ReqBodyFile (..),
156− , ReqBodyBs (..)
129+ ReqBodyBs (..),
157− , ReqBodyLbs (..)
130+ ReqBodyLbs (..),
158− , ReqBodyUrlEnc (..)
131+ ReqBodyUrlEnc (..),
159− , FormUrlEncodedParam
132+ FormUrlEncodedParam,
160− , ReqBodyMultipart
133+ ReqBodyMultipart,
161− , reqBodyMultipart
134+ reqBodyMultipart,
162− , HttpBody (..)
135+ HttpBody (..),
163− , ProvidesBody
136+ ProvidesBody,
164− , HttpBodyAllowed
137+ HttpBodyAllowed,
138+
165 -- ** Optional parameters
139 -- ** Optional parameters
166 -- $optional-parameters
140 -- $optional-parameters
167− , Option
141+ Option,
142+
168 -- *** Query parameters
143 -- *** Query parameters
169 -- $query-parameters
144 -- $query-parameters
170− , (=:)
145+ (=:),
171− , queryFlag
146+ queryFlag,
147+ formToQuery,
172− , QueryParam (..)
148+ QueryParam (..),
149+
173 -- *** Headers
150 -- *** Headers
174− , header
151+ header,
152+ attachHeader,
153+ headerRedacted,
154+
175 -- *** Cookies
155 -- *** Cookies
176 -- $cookies
156 -- $cookies
177− , cookieJar
157+ cookieJar,
158+
178 -- *** Authentication
159 -- *** Authentication
179 -- $authentication
160 -- $authentication
180− , basicAuth
161+ basicAuth,
181− , basicAuthUnsafe
162+ basicAuthUnsafe,
163+ basicProxyAuth,
182− , oAuth1
164+ oAuth1,
183− , oAuth2Bearer
165+ oAuth2Bearer,
184− , oAuth2Token
166+ oAuth2Token,
167+ customAuth,
168+
185 -- *** Other
169 -- *** Other
186− , port
170+ port,
187− , decompress
171+ decompress,
188− , responseTimeout
172+ responseTimeout,
189− , httpVersion
173+ httpVersion,
174+
190 -- * Response
175 -- * Response
176+
191 -- ** Response interpretations
177 -- ** Response interpretations
192− , IgnoreResponse
178+ IgnoreResponse,
193− , ignoreResponse
179+ ignoreResponse,
194− , JsonResponse
180+ JsonResponse,
195− , jsonResponse
181+ jsonResponse,
196− , BsResponse
182+ BsResponse,
197− , bsResponse
183+ bsResponse,
198− , LbsResponse
184+ LbsResponse,
199− , lbsResponse
185+ lbsResponse,
186+
200 -- ** Inspecting a response
187 -- ** Inspecting a response
201− , responseBody
188+ responseBody,
202− , responseStatusCode
189+ responseStatusCode,
203− , responseStatusMessage
190+ responseStatusMessage,
204− , responseHeader
191+ responseHeader,
205− , responseCookieJar
192+ responseCookieJar,
193+
206 -- ** Defining your own interpretation
194 -- ** Defining your own interpretation
207 -- $new-response-interpretation
195 -- $new-response-interpretation
208− , HttpResponse (..)
196+ HttpResponse (..),
197+
209 -- * Other
198 -- * Other
210− , HttpException (..)
199+ HttpException (..),
200+ isStatusCodeException,
211− , CanHaveBody (..)
201+ CanHaveBody (..),
212− , Scheme (..) )
202+ Scheme (..),
203+ )
213where
204where
214
205
206+import Blaze.ByteString.Builder qualified as BB
215import Control.Applicative
207import Control.Applicative
216import Control.Arrow (first, second)
208import Control.Arrow (first, second)
217−import Control.Exception (Exception, try, handle, throwIO)
209+import Control.Exception hiding (Handler (..), TypeError)
218−import Control.Monad
210+import Control.Monad (guard, void, (>=>))
219import Control.Monad.Base
211import Control.Monad.Base
212+import Control.Monad.Catch (Handler (..), MonadCatch, MonadMask, MonadThrow)
220import Control.Monad.IO.Class
213import Control.Monad.IO.Class
221−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)
222import 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
223import Control.Retry
231import Control.Retry
224−import Data.Aeson (ToJSON (..), FromJSON (..))
232+import Data.Aeson (FromJSON (..), ToJSON (..))
233+import Data.Aeson qualified as A
225import Data.ByteString (ByteString)
234import Data.ByteString (ByteString)
235+import Data.ByteString qualified as B
236+import Data.ByteString.Lazy qualified as BL
237+import Data.CaseInsensitive qualified as CI
226import Data.Data (Data)
238import Data.Data (Data)
227−import Data.Default.Class
239+import Data.Default.Class (def)
228import Data.Function (on)
240import Data.Function (on)
229import Data.IORef
241import Data.IORef
230−import Data.List (nubBy)
242+import Data.Kind (Constraint, Type)
243+import Data.List (foldl', nubBy)
231import Data.List.NonEmpty (NonEmpty (..))
244import Data.List.NonEmpty (NonEmpty (..))
245+import Data.List.NonEmpty qualified as NE
232import Data.Maybe (fromMaybe)
246import Data.Maybe (fromMaybe)
233import Data.Proxy
247import Data.Proxy
234−import Data.Semigroup hiding (Option, option)
248+import Data.Semigroup (Endo (..))
249+import Data.Set qualified as S
235import Data.Text (Text)
250import Data.Text (Text)
251+import Data.Text qualified as T
252+import Data.Text.Encoding qualified as T
236−import Data.Typeable (Typeable)
253+import Data.Typeable (Typeable, cast)
237import GHC.Generics
254import GHC.Generics
238import GHC.TypeLits
255import GHC.TypeLits
256+import Language.Haskell.TH qualified as TH
257+import Language.Haskell.TH.Quote qualified as TH
258+import Language.Haskell.TH.Syntax qualified as TH
259+import Network.Connection qualified as NC
260+import Network.HTTP.Client qualified as L
261+import Network.HTTP.Client.Internal qualified as LI
262+import Network.HTTP.Client.MultipartFormData qualified as LM
263+import Network.HTTP.Client.TLS qualified as L
264+import Network.HTTP.Types qualified as Y
239import System.IO.Unsafe (unsafePerformIO)
265import System.IO.Unsafe (unsafePerformIO)
266+import Text.URI (URI)
267+import Text.URI qualified as URI
268+import Text.URI.QQ qualified as QQ
269+import Web.Authenticate.OAuth qualified as OAuth
270+import Web.FormUrlEncoded (FromForm (..), ToForm (..))
271+import Web.FormUrlEncoded qualified as Form
240import Web.HttpApiData (ToHttpApiData (..))
272import Web.HttpApiData (ToHttpApiData (..))
241−import qualified Blaze.ByteString.Builder as BB
242−import qualified Data.Aeson as A
243−import qualified Data.ByteString as B
244−import qualified Data.ByteString.Lazy as BL
245−import qualified Data.CaseInsensitive as CI
246−import qualified Data.List.NonEmpty as NE
247−import qualified Data.Text as T
248−import qualified Data.Text.Encoding as T
249−import qualified Data.Text.Read as TR
250−import qualified Network.Connection as NC
251−import qualified Network.HTTP.Client as L
252−import qualified Network.HTTP.Client.Internal as LI
253−import qualified Network.HTTP.Client.MultipartFormData as LM
254−import qualified Network.HTTP.Client.TLS as L
255−import qualified Network.HTTP.Types as Y
256−import qualified Web.Authenticate.OAuth as OAuth
257−
258−#if MIN_VERSION_base(4,9,0)
259−import Data.Kind (Constraint)
260−#else
261−import GHC.Exts (Constraint)
262−#endif
263
273
264----------------------------------------------------------------------------
274----------------------------------------------------------------------------
265-- Making a request
275-- Making a request
266
276
267-- $making-a-request
277-- $making-a-request
268--
278--
269−-- To make an HTTP request you need only one function: 'req'.
279+-- To make an HTTP request you normally need only one function: 'req'.
270
280
271-- | Make an HTTP request. The function takes 5 arguments, 4 of which
281-- | Make an HTTP request. The function takes 5 arguments, 4 of which
272-- specify required parameters and the final 'Option' argument is a
282-- specify required parameters and the final 'Option' argument is a
@@ -283,7 +293,7 @@ import GHC.Exts (Constraint)
283--
293--
284-- @body@ is a body option such as 'NoReqBody' or 'ReqBodyJson'. The
294-- @body@ is a body option such as 'NoReqBody' or 'ReqBodyJson'. The
285-- tutorial has a section about HTTP bodies, but usage is very
295-- tutorial has a section about HTTP bodies, but usage is very
286−-- straightforward and should be clear from the examples below.
296+-- straightforward and should be clear from the examples.
287--
297--
288-- @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
289-- request. Out-of-the-box it can be the following:
299-- request. Out-of-the-box it can be the following:
@@ -309,12 +319,13 @@ import GHC.Exts (Constraint)
309-- First, this is a piece of boilerplate that should be in place before you
319-- First, this is a piece of boilerplate that should be in place before you
310-- try the examples:
320-- try the examples:
311--
321--
322+-- > {-# LANGUAGE DeriveGeneric #-}
312-- > {-# LANGUAGE OverloadedStrings #-}
323-- > {-# LANGUAGE OverloadedStrings #-}
313-- >
324-- >
314-- > module Main (main) where
325-- > module Main (main) where
315-- >
326-- >
316−-- > import Control.Exception (throwIO)
317-- > import Control.Monad
327-- > import Control.Monad
328+-- > import Control.Monad.IO.Class
318-- > import Data.Aeson
329-- > import Data.Aeson
319-- > import Data.Maybe (fromJust)
330-- > import Data.Maybe (fromJust)
320-- > import Data.Monoid ((<>))
331-- > import Data.Monoid ((<>))
@@ -322,32 +333,30 @@ import GHC.Exts (Constraint)
322-- > import GHC.Generics
333-- > import GHC.Generics
323-- > import Network.HTTP.Req
334-- > import Network.HTTP.Req
324-- > import qualified Data.ByteString.Char8 as B
335-- > import qualified Data.ByteString.Char8 as B
325−-- >
336+-- > import qualified Text.URI as URI
326−-- > instance MonadHttp IO where
327−-- > handleHttpException = throwIO
328--
337--
329-- We will be making requests against the <https://httpbin.org> service.
338-- We will be making requests against the <https://httpbin.org> service.
330--
339--
331-- Make a GET request, grab 5 random bytes:
340-- Make a GET request, grab 5 random bytes:
332--
341--
333-- > main :: IO ()
342-- > main :: IO ()
334−-- > main = do
343+-- > main = runReq defaultHttpConfig $ do
335-- > let n :: Int
344-- > let n :: Int
336-- > n = 5
345-- > n = 5
337-- > bs <- req GET (https "httpbin.org" /: "bytes" /~ n) NoReqBody bsResponse mempty
346-- > bs <- req GET (https "httpbin.org" /: "bytes" /~ n) NoReqBody bsResponse mempty
338−-- > B.putStrLn (responseBody bs)
347+-- > liftIO $ B.putStrLn (responseBody bs)
339--
348--
340-- The same, but now we use a query parameter named @\"seed\"@ to control
349-- The same, but now we use a query parameter named @\"seed\"@ to control
341-- seed of the generator:
350-- seed of the generator:
342--
351--
343-- > main :: IO ()
352-- > main :: IO ()
344−-- > main = do
353+-- > main = runReq defaultHttpConfig $ do
345-- > let n, seed :: Int
354-- > let n, seed :: Int
346-- > n = 5
355-- > n = 5
347-- > seed = 100
356-- > seed = 100
348-- > bs <- req GET (https "httpbin.org" /: "bytes" /~ n) NoReqBody bsResponse $
357-- > bs <- req GET (https "httpbin.org" /: "bytes" /~ n) NoReqBody bsResponse $
349-- > "seed" =: seed
358-- > "seed" =: seed
350−-- > B.putStrLn (responseBody bs)
359+-- > liftIO $ B.putStrLn (responseBody bs)
351--
360--
352-- POST JSON data and get some info about the POST request:
361-- POST JSON data and get some info about the POST request:
353--
362--
@@ -360,108 +369,231 @@ import GHC.Exts (Constraint)
360-- > instance FromJSON MyData
369-- > instance FromJSON MyData
361-- >
370-- >
362-- > main :: IO ()
371-- > main :: IO ()
363−-- > main = do
372+-- > main = runReq defaultHttpConfig $ do
364-- > let myData = MyData
373-- > let myData = MyData
365-- > { size = 6
374-- > { size = 6
366-- > , color = "Green" }
375-- > , color = "Green" }
367-- > v <- req POST (https "httpbin.org" /: "post") (ReqBodyJson myData) jsonResponse mempty
376-- > v <- req POST (https "httpbin.org" /: "post") (ReqBodyJson myData) jsonResponse mempty
368−-- > print (responseBody v :: Value)
377+-- > liftIO $ print (responseBody v :: Value)
369--
378--
370-- Sending URL-encoded body:
379-- Sending URL-encoded body:
371--
380--
372-- > main :: IO ()
381-- > main :: IO ()
373−-- > main = do
382+-- > main = runReq defaultHttpConfig $ do
374-- > let params =
383-- > let params =
375-- > "foo" =: ("bar" :: Text) <>
384-- > "foo" =: ("bar" :: Text) <>
376-- > queryFlag "baz"
385-- > queryFlag "baz"
377-- > response <- req POST (https "httpbin.org" /: "post") (ReqBodyUrlEnc params) jsonResponse mempty
386-- > response <- req POST (https "httpbin.org" /: "post") (ReqBodyUrlEnc params) jsonResponse mempty
378−-- > print (responseBody response :: Value)
387+-- > liftIO $ print (responseBody response :: Value)
379--
388--
380-- Using various optional parameters and URL that is not known in advance:
389-- Using various optional parameters and URL that is not known in advance:
381--
390--
382-- > main :: IO ()
391-- > main :: IO ()
383−-- > main = do
392+-- > main = runReq defaultHttpConfig $ do
384-- > -- This is an example of what to do when URL is given dynamically. Of
393-- > -- This is an example of what to do when URL is given dynamically. Of
385-- > -- course in a real application you may not want to use 'fromJust'.
394-- > -- course in a real application you may not want to use 'fromJust'.
386−-- > let (url, options) = fromJust (parseUrlHttps "https://httpbin.org/get?foo=bar")
395+-- > uri <- URI.mkURI "https://httpbin.org/get?foo=bar"
396+-- > let (url, options) = fromJust (useHttpsURI uri)
387-- > response <- req GET url NoReqBody jsonResponse $
397-- > response <- req GET url NoReqBody jsonResponse $
388-- > "from" =: (15 :: Int) <>
398-- > "from" =: (15 :: Int) <>
389-- > "to" =: (67 :: Int) <>
399-- > "to" =: (67 :: Int) <>
390-- > basicAuth "username" "password" <>
400-- > basicAuth "username" "password" <>
391-- > options <> -- contains the ?foo=bar part
401-- > options <> -- contains the ?foo=bar part
392-- > port 443 -- here you can put any port of course
402-- > port 443 -- here you can put any port of course
393−-- > print (responseBody response :: Value)
403+-- > liftIO $ print (responseBody response :: Value)
394−
395−req
404+req ::
396− :: ( MonadHttp m
405+ ( MonadHttp m,
397− , HttpMethod method
406+ HttpMethod method,
398− , HttpBody body
407+ HttpBody body,
399− , HttpResponse response
408+ HttpResponse response,
400− , HttpBodyAllowed (AllowsBody method) (ProvidesBody body) )
409+ HttpBodyAllowed (AllowsBody method) (ProvidesBody body)
410+ ) =>
401− => method -- ^ HTTP method
411+ -- | HTTP method
412+ method ->
402− -> Url scheme -- ^ 'Url'—location of resource
413+ -- | 'Url'—location of resource
414+ Url scheme ->
403− -> body -- ^ Body of the request
415+ -- | Body of the request
416+ body ->
404− -> Proxy response -- ^ A hint how to interpret response
417+ -- | A hint how to interpret response
418+ Proxy response ->
405− -> Option scheme -- ^ Collection of optional parameters
419+ -- | Collection of optional parameters
420+ Option scheme ->
406− -> m response -- ^ Response
421+ -- | Response
422+ m response
407−req method url body Proxy options = req' method url body options $ \request manager -> do
423+req method url body responseProxy options =
424+ reqCb method url body responseProxy options pure
425+
426+-- | A version of 'req' that does not use one of the predefined instances of
427+-- 'HttpResponse' but instead allows the user to consume @'L.Response'
428+-- 'L.BodyReader'@ manually, in a custom way.
429+--
430+-- @since 1.0.0
431+reqBr ::
432+ ( MonadHttp m,
433+ HttpMethod method,
434+ HttpBody body,
435+ HttpBodyAllowed (AllowsBody method) (ProvidesBody body)
436+ ) =>
437+ -- | HTTP method
438+ method ->
439+ -- | 'Url'—location of resource
440+ Url scheme ->
441+ -- | Body of the request
442+ body ->
443+ -- | Collection of optional parameters
444+ Option scheme ->
445+ -- | How to consume response
446+ (L.Response L.BodyReader -> IO a) ->
447+ -- | Result
448+ m a
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
408− HttpConfig {..} <- getHttpConfig
501+ HttpConfig {..} <- getHttpConfig
409− let wrappingVanilla = handle (throwIO . VanillaHttpException)
502+ let wrapVanilla = handle (throwIO . VanillaHttpException)
410− wrapExc = handle (throwIO . LI.toHttpException request)
503+ wrapExc = handle (throwIO . LI.toHttpException request)
504+ withRRef =
505+ bracket
506+ (newIORef Nothing)
507+ (readIORef >=> mapM_ L.responseClose)
411− (liftIO . try . wrappingVanilla . wrapExc) (do
508+ (liftIO . try . wrapVanilla . wrapExc)
412− response <- retrying httpConfigRetryPolicy httpConfigRetryJudge
509+ ( withRRef $ \rref -> do
413− (const $ getHttpResponse request manager)
510+ let openResponse = mask_ $ do
511+ r <- readIORef rref
512+ mapM_ L.responseClose r
513+ r' <- L.responseOpen request manager
514+ writeIORef rref (Just r')
515+ return r'
516+ exceptionRetryPolicies =
517+ skipAsyncExceptions
518+ ++ [ \retryStatus -> Handler $ \e ->
519+ return $ httpConfigRetryJudgeException retryStatus e
520+ ]
521+ r <-
522+ retrying
523+ httpConfigRetryPolicy
524+ (\retryStatus r -> return $ httpConfigRetryJudge retryStatus r)
525+ ( const
526+ ( recovering
527+ httpConfigRetryPolicy
528+ exceptionRetryPolicies
529+ (const openResponse)
530+ )
531+ )
532+ (preview, r') <- grabPreview httpConfigBodyPreviewLength r
414− httpConfigCheckResponse request response
533+ mapM_ LI.throwHttp (httpConfigCheckResponse request r' preview)
415− return response)
534+ consume r'
535+ )
416 >>= either handleHttpException return
536 >>= either handleHttpException return
417
537
418-- | Mostly like 'req' with respect to its arguments, but accepts a callback
538-- | Mostly like 'req' with respect to its arguments, but accepts a callback
419-- that allows to perform a request in arbitrary fashion.
539-- that allows to perform a request in arbitrary fashion.
420--
540--
421-- This function /does not/ perform handling\/wrapping exceptions, checking
541-- This function /does not/ perform handling\/wrapping exceptions, checking
422−-- response, and retrying. It only prepares 'L.Request' and allows you to
542+-- response (with 'httpConfigCheckResponse'), and retrying. It only prepares
423−-- use it.
543+-- 'L.Request' and allows you to use it.
424--
544--
425-- @since 0.3.0
545-- @since 0.3.0
426−
427−req'
546+req' ::
428− :: forall m method body scheme a.
547+ forall m method body scheme a.
429− ( MonadHttp m
548+ ( MonadHttp m,
430− , HttpMethod method
549+ HttpMethod method,
431− , HttpBody body
550+ HttpBody body,
432− , HttpBodyAllowed (AllowsBody method) (ProvidesBody body) )
551+ HttpBodyAllowed (AllowsBody method) (ProvidesBody body)
552+ ) =>
433− => method -- ^ HTTP method
553+ -- | HTTP method
554+ method ->
434− -> Url scheme -- ^ 'Url'—location of resource
555+ -- | 'Url'—location of resource
556+ Url scheme ->
435− -> body -- ^ Body of the request
557+ -- | Body of the request
558+ body ->
436− -> Option scheme -- ^ Collection of optional parameters
559+ -- | Collection of optional parameters
560+ Option scheme ->
437− -> (L.Request -> L.Manager -> m a) -- ^ How to perform request
561+ -- | How to perform request
562+ (L.Request -> L.Manager -> m a) ->
563+ -- | Result
438− -> m a
564+ m a
439req' method url body options m = do
565req' method url body options m = do
440− config@HttpConfig {..} <- getHttpConfig
566+ config <- getHttpConfig
441− manager <- liftIO (readIORef globalManager)
442 let -- NOTE First appearance of any given header wins. This allows to
567 let -- NOTE First appearance of any given header wins. This allows to
443 -- “overwrite” headers when we construct a request by cons-ing.
568 -- “overwrite” headers when we construct a request by cons-ing.
444 nubHeaders = Endo $ \x ->
569 nubHeaders = Endo $ \x ->
445− x { L.requestHeaders = nubBy ((==) `on` fst) (L.requestHeaders x) }
570+ x {L.requestHeaders = nubBy ((==) `on` fst) (L.requestHeaders x)}
571+ request' =
446− request' = flip appEndo L.defaultRequest $
572+ flip appEndo L.defaultRequest $
447− -- NOTE The order of 'mappend's matters, here method is overwritten
573+ -- NOTE The order of 'mappend's matters, here method is overwritten
448− -- first and 'options' take effect last. In particular, this means
574+ -- first and 'options' take effect last. In particular, this means
449− -- that 'options' can overwrite things set by other request
575+ -- that 'options' can overwrite things set by other request
450− -- components, which is useful for setting port number,
576+ -- components, which is useful for setting port number,
451− -- "Content-Type" header, etc.
577+ -- "Content-Type" header, etc.
452− nubHeaders <>
578+ nubHeaders
453− getRequestMod options <>
579+ <> getRequestMod options
454− getRequestMod config <>
580+ <> getRequestMod config
455− getRequestMod (Womb body :: Womb "body" body) <>
581+ <> getRequestMod (Tagged body :: Tagged "body" body)
456− getRequestMod url <>
582+ <> getRequestMod url
457− getRequestMod (Womb method :: Womb "method" method)
583+ <> getRequestMod (Tagged method :: Tagged "method" method)
458 request <- finalizeRequest options request'
584 request <- finalizeRequest options request'
459− m request manager
585+ withReqManager (m request)
586+
587+-- | Perform an action using the global implicit 'L.Manager' that the rest
588+-- of the library uses. This allows us to reuse connections that the
589+-- 'L.Manager' controls.
590+withReqManager :: (MonadIO m) => (L.Manager -> m a) -> m a
591+withReqManager m = m globalManager
460
592
461−-- | 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
462−-- 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
463−-- when we create a request, instance 'HttpConfig' can affect the default
595+-- all, but when we create a request, instance 'HttpConfig' can affect the
464−-- settings via 'getHttpConfig'.
596+-- default settings via 'getHttpConfig'.
465--
597--
466-- A note about safety, in case 'unsafePerformIO' looks suspicious to you.
598-- A note about safety, in case 'unsafePerformIO' looks suspicious to you.
467-- 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
@@ -469,24 +601,20 @@ req' method url body options m = do
469-- 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
470-- 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
471-- spoil the plan by inlining the definition, hence the @NOINLINE@ pragma.
603-- spoil the plan by inlining the definition, hence the @NOINLINE@ pragma.
472−
473−globalManager :: IORef L.Manager
604+globalManager :: L.Manager
474globalManager = unsafePerformIO $ do
605globalManager = unsafePerformIO $ do
475 context <- NC.initConnectionContext
606 context <- NC.initConnectionContext
607+ let settings =
476− let settings = L.mkManagerSettingsContext (Just context) def Nothing
608+ L.mkManagerSettingsContext
609+ (Just context)
610+ def
611+ Nothing
477 manager <- L.newManager settings
612 manager <- L.newManager settings
478− newIORef manager
613+ return manager
479{-# NOINLINE globalManager #-}
614{-# NOINLINE globalManager #-}
480
615
481−-- | Perform an action using global implicit 'L.Manager' that the rest of
482−-- the library uses. This allows to reuse connections that the 'L.Manager'
483−-- controls.
484−
485−withReqManager :: MonadIO m => (L.Manager -> m a) -> m a
486−withReqManager m = liftIO (readIORef globalManager) >>= m
487−
488----------------------------------------------------------------------------
616----------------------------------------------------------------------------
489−-- Embedding requests into your monad
617+-- Embedding requests in your monad
490
618
491-- $embedding-requests
619-- $embedding-requests
492--
620--
@@ -495,110 +623,165 @@ withReqManager m = liftIO (readIORef globalManager) >>= m
495--
623--
496-- When writing a library, keep your API polymorphic in terms of
624-- When writing a library, keep your API polymorphic in terms of
497-- 'MonadHttp', only define instance of 'MonadHttp' in final application.
625-- 'MonadHttp', only define instance of 'MonadHttp' in final application.
498−-- Another option is to use @newtype@ wrapped monad stack and define
626+-- Another option is to use a @newtype@-wrapped monad stack and define
499−-- 'MonadHttp' for it. As of version /0.4.0/, the 'Req' monad is provided
627+-- 'MonadHttp' for it. As of the version /0.4.0/, the 'Req' monad that
500−-- for this out-of-the-box.
628+-- follows this strategy is provided out-of-the-box (see below).
501
629
502-- | A type class for monads that support performing HTTP requests.
630-- | A type class for monads that support performing HTTP requests.
503-- Typically, you only need to define the 'handleHttpException' method
631-- Typically, you only need to define the 'handleHttpException' method
504-- unless you want to tweak 'HttpConfig'.
632-- unless you want to tweak 'HttpConfig'.
505−
506−class MonadIO m => MonadHttp m where
633+class (MonadIO m) => MonadHttp m where
507−
508 -- | This method describes how to deal with 'HttpException' that was
634 -- | This method describes how to deal with 'HttpException' that was
509 -- 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
510 -- exceptions, but if you prefer working with something like
636 -- exceptions, but if you prefer working with something like
511 -- 'Control.Monad.Except.MonadError', this is the right place to pass it to
637 -- 'Control.Monad.Except.MonadError', this is the right place to pass it to
512 -- 'Control.Monad.Except.throwError'.
638 -- 'Control.Monad.Except.throwError'.
513−
514 handleHttpException :: HttpException -> m a
639 handleHttpException :: HttpException -> m a
515
640
516− -- | Return 'HttpConfig' to be used when performing HTTP requests. Default
641+ -- | Return the 'HttpConfig' to be used when performing HTTP requests.
517− -- implementation returns its 'def' value, which is described in the
642+ -- Default implementation returns its 'def' value, which is described in
518− -- documentation for the type. Common usage pattern with manually defined
643+ -- the documentation for the type. Common usage pattern with manually
519− -- 'getHttpConfig' is to return some hard-coded value, or value extracted
644+ -- defined 'getHttpConfig' is to return some hard-coded value, or a value
520− -- from 'Control.Monad.Reader.MonadReader' if a more flexible approach to
645+ -- extracted from 'Control.Monad.Reader.MonadReader' if a more flexible
521− -- configuration is desirable.
646+ -- approach to configuration is desirable.
522−
523 getHttpConfig :: m HttpConfig
647 getHttpConfig :: m HttpConfig
524− getHttpConfig = return def
648+ getHttpConfig = return defaultHttpConfig
525−
526−-- | 'HttpConfig' contains general and default settings to be used when
527−-- making HTTP requests.
528
649
650+-- | 'HttpConfig' contains settings to be used when making HTTP requests.
529data HttpConfig = HttpConfig
651data HttpConfig = HttpConfig
530− { httpConfigProxy :: Maybe L.Proxy
531− -- ^ 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@
532 -- environment variables are respected, this setting overwrites them.
653 -- environment variables are respected, this setting overwrites them.
533 -- Default value: 'Nothing'.
654 -- Default value: 'Nothing'.
534− , httpConfigRedirectCount :: Int
655+ httpConfigProxy :: Maybe L.Proxy,
535− -- ^ How many redirects to follow when getting a resource. Default
656+ -- | How many redirects to follow when getting a resource. Default
536 -- value: 10.
657 -- value: 10.
537− , httpConfigAltManager :: Maybe L.Manager
658+ httpConfigRedirectCount :: Int,
538− -- ^ Alternative 'L.Manager' to use. 'Nothing' (default value) means
659+ -- | Alternative 'L.Manager' to use. 'Nothing' (default value) means
539− -- that default implicit manager will be used (that's what you want in
660+ -- that the default implicit manager will be used (that's what you want
540− -- 99% of cases).
661+ -- in 99% of cases).
541− , httpConfigCheckResponse :: forall r. HttpResponse r => L.Request -> r -> IO ()
662+ httpConfigAltManager :: Maybe L.Manager,
542− -- ^ Function to check the response immediately after receiving the
663+ -- | Function to check the response immediately after receiving the
664+ -- status and headers, before streaming of response body. The third
665+ -- argument is the beginning of response body (typically first 1024
543− -- status and headers. This is used for throwing exceptions on
666+ -- bytes). This is used for throwing exceptions on non-success status
544− -- non-success status codes by default (set to @\\_ _ -> return ()@ if
667+ -- codes by default (set to @\\_ _ _ -> Nothing@ if this behavior is not
545− -- this behavior is not desirable). Throwing is better then just
668+ -- desirable).
669+ --
670+ -- When the value this function returns is 'Nothing', nothing will
671+ -- happen. When it there is 'L.HttpExceptionContent' inside 'Just', it
672+ -- will be thrown.
673+ --
546− -- returning a request with non-2xx status code because in that case
674+ -- Throwing is better then just returning a request with non-2xx status
547− -- something is wrong and we need a way to short-cut execution. The
675+ -- code because in that case something is wrong and we need a way to
676+ -- short-cut execution (also remember that Req retries automatically on
677+ -- request timeouts and such, so when your request fails, it's certainly
548− -- thrown exception is caught by the library though and is available in
678+ -- something exceptional). The thrown exception is caught by the library
549− -- 'handleHttpException'.
679+ -- though and is available in 'handleHttpException'.
680+ --
681+ -- __Note__: signature of this function was changed in the version
682+ -- /1.0.0/.
550 --
683 --
551 -- @since 0.3.0
684 -- @since 0.3.0
552− , httpConfigRetryPolicy :: RetryPolicyM IO
685+ httpConfigCheckResponse ::
686+ forall b.
687+ L.Request ->
688+ L.Response b ->
689+ ByteString ->
690+ Maybe L.HttpExceptionContent,
553− -- ^ 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
554 -- used (see 'RetryPolicyM').
692 -- used (see 'RetryPolicyM').
555 --
693 --
694+ -- __Note__: signature of this function was changed to disallow 'IO' in
695+ -- version /1.0.0/ and then changed back to its current form in /3.1.0/.
696+ --
556 -- @since 0.3.0
697 -- @since 0.3.0
557− , httpConfigRetryJudge :: forall r. HttpResponse r => RetryStatus -> r -> IO Bool
698+ httpConfigRetryPolicy :: RetryPolicyM IO,
558− -- ^ The function is used to decide whether to retry a request. 'True'
699+ -- | The function is used to decide whether to retry a request. 'True'
559 -- means that the request should be retried.
700 -- means that the request should be retried.
560 --
701 --
702+ -- __Note__: signature of this function was changed in the version
703+ -- /1.0.0/.
704+ --
561 -- @since 0.3.0
705 -- @since 0.3.0
562− } deriving Typeable
706+ httpConfigRetryJudge :: forall b. RetryStatus -> L.Response b -> Bool,
563−
707+ -- | Similar to 'httpConfigRetryJudge', but is used to decide when to
564−instance Default HttpConfig where
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
718+ }
719+
720+-- | The default value of 'HttpConfig'.
721+--
722+-- @since 2.0.0
723+defaultHttpConfig :: HttpConfig
724+defaultHttpConfig =
565− def = HttpConfig
725+ HttpConfig
566− { httpConfigProxy = Nothing
726+ { httpConfigProxy = Nothing,
567− , httpConfigRedirectCount = 10
727+ httpConfigRedirectCount = 10,
568− , httpConfigAltManager = Nothing
728+ httpConfigAltManager = Nothing,
569− , httpConfigCheckResponse = \_ response ->
729+ httpConfigCheckResponse = \_ response preview ->
570− let statusCode = responseStatusCode response in
730+ let scode = statusCode response
571− unless (200 <= statusCode && statusCode < 300) $
731+ in if 200 <= scode && scode < 300
572− let chunk = makeResponseBodyPreview response
732+ then Nothing
573− vresponse = toVanillaResponse response
574− in LI.throwHttp (L.StatusCodeException (void vresponse) chunk)
733+ else Just (L.StatusCodeException (void response) preview),
575− , httpConfigRetryPolicy = def
734+ httpConfigRetryPolicy = retryPolicyDefault,
576− , httpConfigRetryJudge = \_ r -> return $
735+ httpConfigRetryJudge = \_ response ->
577− responseStatusCode r `elem`
736+ statusCode response
578− [ 408 -- Request timeout
737+ `elem` [ 408, -- Request timeout
579− , 504 -- Gateway timeout
738+ 504, -- Gateway timeout
580− , 524 -- A timeout occurred
739+ 524, -- A timeout occurred
581− , 598 -- (Informal convention) Network read timeout error
740+ 598, -- (Informal convention) Network read timeout error
582− , 599 -- (Informal convention) Network connect timeout error
741+ 599 -- (Informal convention) Network connect timeout error
583− ]
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
584 }
752 }
753+ where
754+ statusCode = Y.statusCode . L.responseStatus
585
755
586instance RequestComponent HttpConfig where
756instance RequestComponent HttpConfig where
587 getRequestMod HttpConfig {..} = Endo $ \x ->
757 getRequestMod HttpConfig {..} = Endo $ \x ->
758+ x
588− x { L.proxy = httpConfigProxy
759+ { L.proxy = httpConfigProxy,
589− , L.redirectCount = httpConfigRedirectCount
760+ L.redirectCount = httpConfigRedirectCount,
590− , LI.requestManagerOverride = httpConfigAltManager }
761+ LI.requestManagerOverride = httpConfigAltManager
762+ }
591
763
592−-- | 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
593-- having to define new instances.
765-- having to define new instances.
594--
766--
595-- @since 0.4.0
767-- @since 0.4.0
596−
597newtype Req a = Req (ReaderT HttpConfig IO a)
768newtype Req a = Req (ReaderT HttpConfig IO a)
598− deriving ( Functor
769+ deriving
770+ ( Functor,
599− , Applicative
771+ Applicative,
600− , Monad
772+ Monad,
601− , MonadIO )
773+ MonadIO,
774+ MonadUnliftIO
775+ )
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
602
785
603instance MonadBase IO Req where
786instance MonadBase IO Req where
604 liftBase = liftIO
787 liftBase = liftIO
@@ -607,22 +790,99 @@ instance MonadBaseControl IO Req where
607 type StM Req a = a
790 type StM Req a = a
608 liftBaseWith f = Req . ReaderT $ \r -> f (runReq r)
791 liftBaseWith f = Req . ReaderT $ \r -> f (runReq r)
609 {-# INLINEABLE liftBaseWith #-}
792 {-# INLINEABLE liftBaseWith #-}
610− restoreM = Req . ReaderT . const . return
793+ restoreM = Req . ReaderT . const . return
611 {-# INLINEABLE restoreM #-}
794 {-# INLINEABLE restoreM #-}
612
795
613instance MonadHttp Req where
796instance MonadHttp Req where
614 handleHttpException = Req . lift . throwIO
797 handleHttpException = Req . lift . throwIO
615− getHttpConfig = Req ask
798+ getHttpConfig = Req ask
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
616
874
617-- | 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
618−-- case of exceptional situation an 'HttpException' will be thrown.
876+-- the case of an exceptional situation an 'HttpException' will be thrown.
619--
877--
620-- @since 0.4.0
878-- @since 0.4.0
621−
879+runReq ::
622−runReq :: MonadIO m
880+ (MonadIO m) =>
623− => HttpConfig -- ^ 'HttpConfig' to use
881+ -- | 'HttpConfig' to use
882+ HttpConfig ->
624− -> Req a -- ^ Computation to run
883+ -- | Computation to run
884+ Req a ->
625− -> m a
885+ m a
626runReq config (Req m) = liftIO (runReaderT m config)
886runReq config (Req m) = liftIO (runReaderT m config)
627
887
628----------------------------------------------------------------------------
888----------------------------------------------------------------------------
@@ -638,7 +898,6 @@ runReq config (Req m) = liftIO (runReaderT m config)
638-- define a new method that will works seamlessly, see 'HttpMethod'.
898-- define a new method that will works seamlessly, see 'HttpMethod'.
639
899
640-- | 'GET' method.
900-- | 'GET' method.
641−
642data GET = GET
901data GET = GET
643
902
644instance HttpMethod GET where
903instance HttpMethod GET where
@@ -646,7 +905,6 @@ instance HttpMethod GET where
646 httpMethodName Proxy = Y.methodGet
905 httpMethodName Proxy = Y.methodGet
647
906
648-- | 'POST' method.
907-- | 'POST' method.
649−
650data POST = POST
908data POST = POST
651
909
652instance HttpMethod POST where
910instance HttpMethod POST where
@@ -654,7 +912,6 @@ instance HttpMethod POST where
654 httpMethodName Proxy = Y.methodPost
912 httpMethodName Proxy = Y.methodPost
655
913
656-- | 'HEAD' method.
914-- | 'HEAD' method.
657−
658data HEAD = HEAD
915data HEAD = HEAD
659
916
660instance HttpMethod HEAD where
917instance HttpMethod HEAD where
@@ -662,26 +919,24 @@ instance HttpMethod HEAD where
662 httpMethodName Proxy = Y.methodHead
919 httpMethodName Proxy = Y.methodHead
663
920
664-- | 'PUT' method.
921-- | 'PUT' method.
665−
666data PUT = PUT
922data PUT = PUT
667
923
668instance HttpMethod PUT where
924instance HttpMethod PUT where
669 type AllowsBody PUT = 'CanHaveBody
925 type AllowsBody PUT = 'CanHaveBody
670 httpMethodName Proxy = Y.methodPut
926 httpMethodName Proxy = Y.methodPut
671
927
672−-- | 'DELETE' method. This data type does not allow having request body with
928+-- | 'DELETE' method. RFC 7231 allows a payload in DELETE but without
673−-- 'DELETE' requests, as it should be. However some APIs may expect 'DELETE'
929+-- semantics.
674−-- requests to have bodies, in that case define your own variation of
930+--
675−-- 'DELETE' method and allow it to have a body.
931+-- __Note__: before version /3.4.0/ this method did not allow request
676−
932+-- bodies.
677data DELETE = DELETE
933data DELETE = DELETE
678
934
679instance HttpMethod DELETE where
935instance HttpMethod DELETE where
680− type AllowsBody DELETE = 'NoBody
936+ type AllowsBody DELETE = 'CanHaveBody
681 httpMethodName Proxy = Y.methodDelete
937 httpMethodName Proxy = Y.methodDelete
682
938
683-- | 'TRACE' method.
939-- | 'TRACE' method.
684−
685data TRACE = TRACE
940data TRACE = TRACE
686
941
687instance HttpMethod TRACE where
942instance HttpMethod TRACE where
@@ -689,7 +944,6 @@ instance HttpMethod TRACE where
689 httpMethodName Proxy = Y.methodTrace
944 httpMethodName Proxy = Y.methodTrace
690
945
691-- | 'CONNECT' method.
946-- | 'CONNECT' method.
692−
693data CONNECT = CONNECT
947data CONNECT = CONNECT
694
948
695instance HttpMethod CONNECT where
949instance HttpMethod CONNECT where
@@ -697,7 +951,6 @@ instance HttpMethod CONNECT where
697 httpMethodName Proxy = Y.methodConnect
951 httpMethodName Proxy = Y.methodConnect
698
952
699-- | 'OPTIONS' method.
953-- | 'OPTIONS' method.
700−
701data OPTIONS = OPTIONS
954data OPTIONS = OPTIONS
702
955
703instance HttpMethod OPTIONS where
956instance HttpMethod OPTIONS where
@@ -705,7 +958,6 @@ instance HttpMethod OPTIONS where
705 httpMethodName Proxy = Y.methodOptions
958 httpMethodName Proxy = Y.methodOptions
706
959
707-- | 'PATCH' method.
960-- | 'PATCH' method.
708−
709data PATCH = PATCH
961data PATCH = PATCH
710
962
711instance HttpMethod PATCH where
963instance HttpMethod PATCH where
@@ -713,30 +965,26 @@ instance HttpMethod PATCH where
713 httpMethodName Proxy = Y.methodPatch
965 httpMethodName Proxy = Y.methodPatch
714
966
715-- | A type class for types that can be used as an HTTP method. To define a
967-- | A type class for types that can be used as an HTTP method. To define a
716−-- non-standard method, follow this example that defines COPY:
968+-- non-standard method, follow this example that defines @COPY@:
717--
969--
718-- > data COPY = COPY
970-- > data COPY = COPY
719-- >
971-- >
720-- > instance HttpMethod COPY where
972-- > instance HttpMethod COPY where
721-- > type AllowsBody COPY = 'CanHaveBody
973-- > type AllowsBody COPY = 'CanHaveBody
722-- > httpMethodName Proxy = "COPY"
974-- > httpMethodName Proxy = "COPY"
723−
724class HttpMethod a where
975class HttpMethod a where
725−
726 -- | Type function 'AllowsBody' returns a type of kind 'CanHaveBody' which
976 -- | Type function 'AllowsBody' returns a type of kind 'CanHaveBody' which
727− -- tells the rest of the library whether the method can have a body or
977+ -- tells the rest of the library whether the method can have body or not.
728− -- not. We use the special type 'CanHaveBody' “lifted” to kind level
978+ -- We use the special type 'CanHaveBody' lifted to the kind level instead
729− -- instead of 'Bool' to get more user-friendly compiler messages.
979+ -- of 'Bool' to get more user-friendly compiler messages.
730−
731 type AllowsBody a :: CanHaveBody
980 type AllowsBody a :: CanHaveBody
732
981
733 -- | Return name of the method as a 'ByteString'.
982 -- | Return name of the method as a 'ByteString'.
734−
735 httpMethodName :: Proxy a -> ByteString
983 httpMethodName :: Proxy a -> ByteString
736
984
737−instance HttpMethod method => RequestComponent (Womb "method" method) where
985+instance (HttpMethod method) => RequestComponent (Tagged "method" method) where
738 getRequestMod _ = Endo $ \x ->
986 getRequestMod _ = Endo $ \x ->
739− x { L.method = httpMethodName (Proxy :: Proxy method) }
987+ x {L.method = httpMethodName (Proxy :: Proxy method)}
740
988
741----------------------------------------------------------------------------
989----------------------------------------------------------------------------
742-- Request—URL
990-- Request—URL
@@ -744,7 +992,7 @@ instance HttpMethod method => RequestComponent (Womb "method" method) where
744-- $url
992-- $url
745--
993--
746-- We use 'Url's which are correct by construction, see 'Url'. To build a
994-- We use 'Url's which are correct by construction, see 'Url'. To build a
747−-- 'Url' from a 'ByteString', use 'parseUrlHttp' or 'parseUrlHttps'.
995+-- 'Url' from a 'URI', use 'useHttpURI', 'useHttpsURI', or generic 'useURI'.
748
996
749-- | Request's 'Url'. Start constructing your 'Url' with 'http' or 'https'
997-- | Request's 'Url'. Start constructing your 'Url' with 'http' or 'https'
750-- specifying the scheme and host at the same time. Then use the @('/~')@
998-- specifying the scheme and host at the same time. Then use the @('/~')@
@@ -774,91 +1022,194 @@ instance HttpMethod method => RequestComponent (Womb "method" method) where
774--
1022--
775-- > https "юникод.рф"
1023-- > https "юникод.рф"
776-- > -- 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
777−
778data Url (scheme :: Scheme) = Url Scheme (NonEmpty Text)
1025data Url (scheme :: Scheme) = Url Scheme (NonEmpty Text)
779 -- NOTE The second value is the path segments in reversed order.
1026 -- NOTE The second value is the path segments in reversed order.
780− deriving (Eq, Ord, Show, Data, Typeable, Generic)
1027+ deriving (Eq, Ord, Show, Data, Generic)
781−
1028+
1029+type role Url nominal
1030+
1031+-- With template-haskell >=2.15 and text >=1.2.4 Lift can be derived, however
1032+-- the derived lift forgets the type of the scheme.
1033+instance (Typeable scheme) => TH.Lift (Url scheme) where
1034+ lift url =
1035+ TH.dataToExpQ (fmap liftText . cast) url `TH.sigE` case url of
1036+ Url Http _ -> [t|Url 'Http|]
1037+ Url Https _ -> [t|Url 'Https|]
1038+ where
1039+ liftText t = TH.AppE (TH.VarE 'T.pack) <$> TH.lift (T.unpack t)
1040+ liftTyped = TH.Code . TH.unsafeTExpCoerce . TH.lift
1041+
782−-- | Given host name, produce a 'Url' which have “http” as its scheme and
1042+-- | Given host name, produce a 'Url' which has “http” as its scheme and
783-- empty path. This also sets port to @80@.
1043-- empty path. This also sets port to @80@.
784−
785http :: Text -> Url 'Http
1044http :: Text -> Url 'Http
786http = Url Http . pure
1045http = Url Http . pure
787
1046
788−-- | Given host name, produce a 'Url' which have “https” as its scheme and
1047+-- | Given host name, produce a 'Url' which has “https” as its scheme and
789-- empty path. This also sets port to @443@.
1048-- empty path. This also sets port to @443@.
790−
791https :: Text -> Url 'Https
1049https :: Text -> Url 'Https
792https = Url Https . pure
1050https = Url Https . pure
793
1051
794−-- | 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
795-- 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'.
796−
797infixl 5 /~
1054infixl 5 /~
798−(/~) :: ToHttpApiData a => Url scheme -> a -> Url scheme
799−Url secure path /~ segment = Url secure (NE.cons (toUrlPiece segment) path)
800
1055
801−-- | Type-constrained version of @('/~')@ to remove ambiguity in the cases
1056+(/~) :: (ToHttpApiData a) => Url scheme -> a -> Url scheme
802−-- when next URL piece is a 'Text' literal.
1057+Url secure path /~ segment = Url secure (NE.cons (toUrlPiece segment) path)
803
1058
1059+-- | A type-constrained version of @('/~')@ to remove ambiguity in the cases
1060+-- when next URL piece is a 'Data.Text.Text' literal.
804infixl 5 /:
1061infixl 5 /:
1062+
805(/:) :: Url scheme -> Text -> Url scheme
1063(/:) :: Url scheme -> Text -> Url scheme
806(/:) = (/~)
1064(/:) = (/~)
807
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+
808−-- | The 'parseUrlHttp' function provides an alternative method to get 'Url'
1079+-- | The 'useHttpURI' function provides an alternative method to get 'Url'
809−-- (possibly with some 'Option's) from a 'ByteString'. This is useful when
1080+-- (possibly with some 'Option's) from a 'URI'. This is useful when you are
810−-- you are 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.
811−-- The function parses 'ByteString' because it's the correct type to
1082+--
812−-- represent a URL, as 'Url' cannot contain characters outside of ASCII
1083+-- This function expects the scheme to be “http” and host to be present.
813−-- range, thus we can consider every character a 'Data.Word.Word8' value.
1084+--
814−--
1085+-- @since 3.0.0
815−-- This function only parses 'Url' (scheme, host, path) and optional query
1086+useHttpURI :: URI -> Maybe (Url 'Http, Option scheme)
816−-- parameters that are returned as 'Option'. It does not parse method name
1087+useHttpURI uri = do
817−-- or authentication info from given 'ByteString'.
1088+ guard (URI.uriScheme uri == Just [QQ.scheme|http|])
818−
1089+ urlHead <- http <$> uriHost uri
819−parseUrlHttp :: ByteString -> Maybe (Url 'Http, Option scheme)
1090+ let url = case URI.uriPath uri of
820−parseUrlHttp url' = do
1091+ Nothing -> urlHead
821− url <- B.stripPrefix "http://" url'
1092+ Just uriPath -> uriPathToUrl uriPath urlHead
822− (host :| path, option) <- parseUrlHelper url
1093+ return (url, uriOptions uri)
823− return (foldl (/:) (http host) path, option)
1094+
824−
825−-- | Just like 'parseUrlHttp', but expects “https” scheme.
1095+-- | Just like 'useHttpURI', but expects the “https” scheme.
826−
1096+--
1097+-- @since 3.0.0
827−parseUrlHttps :: ByteString -> Maybe (Url 'Https, Option scheme)
1098+useHttpsURI :: URI -> Maybe (Url 'Https, Option scheme)
828−parseUrlHttps url' = do
1099+useHttpsURI uri = do
829− url <- B.stripPrefix "https://" url'
1100+ guard (URI.uriScheme uri == Just [QQ.scheme|https|])
830− (host :| path, option) <- parseUrlHelper url
1101+ urlHead <- https <$> uriHost uri
831− return (foldl (/:) (https host) path, option)
1102+ let url = case URI.uriPath uri of
832−
1103+ Nothing -> urlHead
833−-- | Get host\/collection of path pieces and possibly query parameters
1104+ Just uriPath -> uriPathToUrl uriPath urlHead
834−-- already converted to 'Option'. This function is not public.
1105+ return (url, uriOptions uri)
835−
1106+
836−parseUrlHelper :: ByteString -> Maybe (NonEmpty Text, Option scheme)
1107+-- | Convert URI path to a 'Url'. Internal.
837−parseUrlHelper url = do
1108+--
838− let (path', query') = B.break (== 0x3f) url
1109+-- @since 3.9.0
839− query = mconcat (uncurry queryParam <$> Y.parseQueryText query')
1110+uriPathToUrl ::
840− p' :| ps <- NE.nonEmpty (Y.decodePathSegments path')
1111+ (Bool, NonEmpty (URI.RText 'URI.PathPiece)) ->
841− (p, port') <-
1112+ Url scheme ->
842− case T.break (== ':') p' of
1113+ Url scheme
843− (x, "") -> return (x, mempty)
1114+uriPathToUrl (trailingSlash, xs) urlHead =
844− (x, prt) ->
1115+ if trailingSlash then path /: T.empty else path
845− case TR.decimal (T.drop 1 prt) of
1116+ where
846− Right (prt',"") -> return (x, port prt')
1117+ path = foldl' (/:) urlHead (URI.unRText <$> NE.toList xs)
847− _ -> Nothing
1118+
848− return (p :| ps, query <> port')
1119+-- | A combination of 'useHttpURI' and 'useHttpsURI' for cases when scheme
1120+-- is not known in advance.
1121+--
1122+-- @since 3.0.0
1123+useURI ::
1124+ URI ->
1125+ Maybe
1126+ ( Either
1127+ (Url 'Http, Option scheme0)
1128+ (Url 'Https, Option scheme1)
1129+ )
1130+useURI uri =
1131+ (Left <$> useHttpURI uri) <|> (Right <$> useHttpsURI uri)
1132+
1133+-- | An internal helper function to extract host from a 'URI'.
1134+uriHost :: URI -> Maybe Text
1135+uriHost uri = case URI.uriAuthority uri of
1136+ Left _ -> Nothing
1137+ Right URI.Authority {..} ->
1138+ Just (URI.unRText authHost)
1139+
1140+-- | A quasiquoter to build an 'Url' and 'Option' tuple. The type of the
1141+-- generated expression is @('Url' scheme0, 'Option' scheme1)@ with
1142+-- @scheme0@ being either 'Http' or 'Https' depending on the input.
1143+--
1144+-- @since 3.2.0
1145+urlQ :: TH.QuasiQuoter
1146+urlQ =
1147+ TH.QuasiQuoter
1148+ { quoteExp = \str ->
1149+ case URI.mkURI (T.pack str) of
1150+ Left err -> fail (displayException err)
1151+ Right uri -> case useURI uri of
1152+ Nothing -> fail "Not a HTTP or HTTPS URL"
1153+ Just eurl ->
1154+ TH.tupE
1155+ [ either (TH.lift . fst) (TH.lift . fst) eurl,
1156+ [|uriOptions uri|]
1157+ ],
1158+ quotePat = error "This usage is not supported",
1159+ quoteType = error "This usage is not supported",
1160+ quoteDec = error "This usage is not supported"
1161+ }
1162+
1163+-- | An internal helper function to extract 'Option's from a 'URI'.
1164+uriOptions :: forall scheme. URI -> Option scheme
1165+uriOptions uri =
1166+ mconcat
1167+ [ auth,
1168+ query,
1169+ port'
1170+ -- , fragment'
1171+ ]
1172+ where
1173+ (auth, port') =
1174+ case URI.uriAuthority uri of
1175+ Left _ -> (mempty, mempty)
1176+ Right URI.Authority {..} ->
1177+ let auth0 = case authUserInfo of
1178+ Nothing -> mempty
1179+ Just URI.UserInfo {..} ->
1180+ let username = T.encodeUtf8 (URI.unRText uiUsername)
1181+ password = maybe "" (T.encodeUtf8 . URI.unRText) uiPassword
1182+ in basicAuthUnsafe username password
1183+ port0 = case authPort of
1184+ Nothing -> mempty
1185+ Just port'' -> port (fromIntegral port'')
1186+ in (auth0, port0)
1187+ query =
1188+ let liftQueryParam = \case
1189+ URI.QueryFlag t -> queryFlag (URI.unRText t)
1190+ URI.QueryParam k v -> URI.unRText k =: URI.unRText v
1191+ in mconcat (liftQueryParam <$> URI.uriQuery uri)
1192+
1193+-- TODO Blocked on upstream: https://github.com/snoyberg/http-client/issues/424
1194+-- fragment' =
1195+-- case URI.uriFragment uri of
1196+-- Nothing -> mempty
1197+-- Just fragment'' -> fragment (URI.unRText fragment'')
849
1198
850instance RequestComponent (Url scheme) where
1199instance RequestComponent (Url scheme) where
851 getRequestMod (Url scheme segments) = Endo $ \x ->
1200 getRequestMod (Url scheme segments) = Endo $ \x ->
852− let (host :| path) = NE.reverse segments in
1201+ let (host :| path) = NE.reverse segments
1202+ in x
853− x { L.secure = case scheme of
1203+ { L.secure = case scheme of
854− Http -> False
1204+ Http -> False
855− Https -> True
1205+ Https -> True,
856− , L.port = case scheme of
1206+ L.port = case scheme of
857− Http -> 80
1207+ Http -> 80
858− Https -> 443
1208+ Https -> 443,
859− , L.host = Y.urlEncode False (T.encodeUtf8 host)
1209+ L.host = Y.urlEncode False (T.encodeUtf8 host),
860− , L.path =
1210+ L.path =
861− (BL.toStrict . BB.toLazyByteString . Y.encodePathSegments) path }
1211+ (BL.toStrict . BB.toLazyByteString . Y.encodePathSegments) path
1212+ }
862
1213
863----------------------------------------------------------------------------
1214----------------------------------------------------------------------------
864-- Request—Body
1215-- Request—Body
@@ -867,39 +1218,37 @@ instance RequestComponent (Url scheme) where
867--
1218--
868-- A number of options for request bodies are available. The @Content-Type@
1219-- A number of options for request bodies are available. The @Content-Type@
869-- header is set for you automatically according to the body option you use
1220-- header is set for you automatically according to the body option you use
870−-- (it's always specified in documentation for a given body option). To add
1221+-- (it's always specified in the documentation for a given body option). To
871−-- your own way to represent request body, see 'HttpBody'.
1222+-- add your own way to represent request body, define an instance of
1223+-- 'HttpBody'.
872
1224
873-- | This data type represents empty body of an HTTP request. This is the
1225-- | This data type represents empty body of an HTTP request. This is the
874-- data type to use with 'HttpMethod's that cannot have a body, as it's the
1226-- data type to use with 'HttpMethod's that cannot have a body, as it's the
875-- only type for which 'ProvidesBody' returns 'NoBody'.
1227-- only type for which 'ProvidesBody' returns 'NoBody'.
876--
1228--
877-- Using of this body option does not set the @Content-Type@ header.
1229-- Using of this body option does not set the @Content-Type@ header.
878−
879data NoReqBody = NoReqBody
1230data NoReqBody = NoReqBody
880
1231
881instance HttpBody NoReqBody where
1232instance HttpBody NoReqBody where
882 getRequestBody NoReqBody = L.RequestBodyBS B.empty
1233 getRequestBody NoReqBody = L.RequestBodyBS B.empty
883
1234
884−-- | 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
885−-- 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
886−-- 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
887−-- JSON and inserted as request body.
1238+-- converted to JSON and inserted as request body.
888--
1239--
889-- This body option sets the @Content-Type@ header to @\"application/json;
1240-- This body option sets the @Content-Type@ header to @\"application/json;
890-- charset=utf-8\"@ value.
1241-- charset=utf-8\"@ value.
891−
892newtype ReqBodyJson a = ReqBodyJson a
1242newtype ReqBodyJson a = ReqBodyJson a
893
1243
894−instance ToJSON a => HttpBody (ReqBodyJson a) where
1244+instance (ToJSON a) => HttpBody (ReqBodyJson a) where
895 getRequestBody (ReqBodyJson a) = L.RequestBodyLBS (A.encode a)
1245 getRequestBody (ReqBodyJson a) = L.RequestBodyLBS (A.encode a)
896 getRequestContentType _ = pure "application/json; charset=utf-8"
1246 getRequestContentType _ = pure "application/json; charset=utf-8"
897
1247
898-- | 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
899−-- the file size does not change during the streaming.
1249+-- the file size does not change during streaming.
900--
1250--
901-- 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.
902−
903newtype ReqBodyFile = ReqBodyFile FilePath
1252newtype ReqBodyFile = ReqBodyFile FilePath
904
1253
905instance HttpBody ReqBodyFile where
1254instance HttpBody ReqBodyFile where
@@ -909,7 +1258,6 @@ instance HttpBody ReqBodyFile where
909-- | HTTP request body represented by a strict 'ByteString'.
1258-- | HTTP request body represented by a strict 'ByteString'.
910--
1259--
911-- Using of this body option does not set the @Content-Type@ header.
1260-- Using of this body option does not set the @Content-Type@ header.
912−
913newtype ReqBodyBs = ReqBodyBs ByteString
1261newtype ReqBodyBs = ReqBodyBs ByteString
914
1262
915instance HttpBody ReqBodyBs where
1263instance HttpBody ReqBodyBs where
@@ -918,22 +1266,19 @@ instance HttpBody ReqBodyBs where
918-- | HTTP request body represented by a lazy 'BL.ByteString'.
1266-- | HTTP request body represented by a lazy 'BL.ByteString'.
919--
1267--
920-- Using of this body option does not set the @Content-Type@ header.
1268-- Using of this body option does not set the @Content-Type@ header.
921−
922newtype ReqBodyLbs = ReqBodyLbs BL.ByteString
1269newtype ReqBodyLbs = ReqBodyLbs BL.ByteString
923
1270
924instance HttpBody ReqBodyLbs where
1271instance HttpBody ReqBodyLbs where
925 getRequestBody (ReqBodyLbs bs) = L.RequestBodyLBS bs
1272 getRequestBody (ReqBodyLbs bs) = L.RequestBodyLBS bs
926
1273
927−-- | 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
928−-- 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
929−-- 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
930−-- 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
931−-- combinators you would use to add query parameters: @('=:')@ and
1278+-- would use to add query parameters: @('=:')@ and 'queryFlag'.
932−-- 'queryFlag'.
933--
1279--
934-- This body option sets the @Content-Type@ header to
1280-- This body option sets the @Content-Type@ header to
935−-- @\"application/x-www-from-urlencoded\"@ value.
1281+-- @\"application/x-www-form-urlencoded\"@ value.
936−
937newtype ReqBodyUrlEnc = ReqBodyUrlEnc FormUrlEncodedParam
1282newtype ReqBodyUrlEnc = ReqBodyUrlEnc FormUrlEncodedParam
938
1283
939instance HttpBody ReqBodyUrlEnc where
1284instance HttpBody ReqBodyUrlEnc where
@@ -943,33 +1288,37 @@ instance HttpBody ReqBodyUrlEnc where
943
1288
944-- | An opaque monoidal value that allows to collect URL-encoded parameters
1289-- | An opaque monoidal value that allows to collect URL-encoded parameters
945-- to be wrapped in 'ReqBodyUrlEnc'.
1290-- to be wrapped in 'ReqBodyUrlEnc'.
946−
947newtype FormUrlEncodedParam = FormUrlEncodedParam [(Text, Maybe Text)]
1291newtype FormUrlEncodedParam = FormUrlEncodedParam [(Text, Maybe Text)]
948 deriving (Semigroup, Monoid)
1292 deriving (Semigroup, Monoid)
949
1293
950instance QueryParam FormUrlEncodedParam where
1294instance QueryParam FormUrlEncodedParam where
951 queryParam name mvalue =
1295 queryParam name mvalue =
952 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
953
1304
954-- | Multipart form data. Please consult the
1305-- | Multipart form data. Please consult the
955-- "Network.HTTP.Client.MultipartFormData" module for how to construct
1306-- "Network.HTTP.Client.MultipartFormData" module for how to construct
956-- parts, then use 'reqBodyMultipart' to create actual request body from the
1307-- parts, then use 'reqBodyMultipart' to create actual request body from the
957−-- parts. 'reqBodyMultipart' is the only way to get a value of type
1308+-- parts. 'reqBodyMultipart' is the only way to get a value of the type
958-- 'ReqBodyMultipart', as its constructor is not exported on purpose.
1309-- 'ReqBodyMultipart', as its constructor is not exported on purpose.
959--
1310--
960-- @since 0.2.0
1311-- @since 0.2.0
961--
1312--
962−-- ==== __Example__
1313+-- ==== __Examples__
963--
1314--
964−-- > import Control.Exception (throwIO)
1315+-- > import Control.Monad.IO.Class
965−-- > import qualified Network.HTTP.Client.MultipartFormData as LM
1316+-- > import Data.Default.Class
966-- > import Network.HTTP.Req
1317-- > import Network.HTTP.Req
967−-- >
1318+-- > import qualified Network.HTTP.Client.MultipartFormData as LM
968−-- > instance MonadHttp IO where
969−-- > handleHttpException = throwIO
970-- >
1319-- >
971-- > main :: IO ()
1320-- > main :: IO ()
972−-- > main = do
1321+-- > main = runReq def $ do
973-- > body <-
1322-- > body <-
974-- > reqBodyMultipart
1323-- > reqBodyMultipart
975-- > [ LM.partBS "title" "My Image"
1324-- > [ LM.partBS "title" "My Image"
@@ -980,8 +1329,7 @@ instance QueryParam FormUrlEncodedParam where
980-- > body
1329-- > body
981-- > bsResponse
1330-- > bsResponse
982-- > mempty
1331-- > mempty
983−-- > print $ responseBody response
1332+-- > liftIO $ print (responseBody response)
984−
985data ReqBodyMultipart = ReqBodyMultipart ByteString LI.RequestBody
1333data ReqBodyMultipart = ReqBodyMultipart ByteString LI.RequestBody
986
1334
987instance HttpBody ReqBodyMultipart where
1335instance HttpBody ReqBodyMultipart where
@@ -992,126 +1340,117 @@ instance HttpBody ReqBodyMultipart where
992-- | Create 'ReqBodyMultipart' request body from a collection of 'LM.Part's.
1340-- | Create 'ReqBodyMultipart' request body from a collection of 'LM.Part's.
993--
1341--
994-- @since 0.2.0
1342-- @since 0.2.0
995−
996−reqBodyMultipart :: MonadIO m => [LM.Part] -> m ReqBodyMultipart
1343+reqBodyMultipart :: (MonadIO m) => [LM.Part] -> m ReqBodyMultipart
997reqBodyMultipart parts = liftIO $ do
1344reqBodyMultipart parts = liftIO $ do
998 boundary <- LM.webkitBoundary
1345 boundary <- LM.webkitBoundary
999− body <- LM.renderParts boundary parts
1346+ body <- LM.renderParts boundary parts
1000 return (ReqBodyMultipart boundary body)
1347 return (ReqBodyMultipart boundary body)
1001
1348
1002-- | A type class for things that can be interpreted as an HTTP
1349-- | A type class for things that can be interpreted as an HTTP
1003-- 'L.RequestBody'.
1350-- 'L.RequestBody'.
1004−
1005class HttpBody body where
1351class HttpBody body where
1006−
1007− {-# MINIMAL getRequestBody #-}
1008−
1009 -- | How to get actual 'L.RequestBody'.
1352 -- | How to get actual 'L.RequestBody'.
1010−
1011 getRequestBody :: body -> L.RequestBody
1353 getRequestBody :: body -> L.RequestBody
1012
1354
1013− -- | This method allows to optionally specify value of @Content-Type@
1355+ -- | This method allows us to optionally specify the value of
1014− -- header that should be used with particular body option. By default it
1356+ -- @Content-Type@ header that should be used with particular body option.
1015− -- returns 'Nothing' and so @Content-Type@ is not set.
1357+ -- By default it returns 'Nothing' and so @Content-Type@ is not set.
1016−
1017 getRequestContentType :: body -> Maybe ByteString
1358 getRequestContentType :: body -> Maybe ByteString
1018 getRequestContentType = const Nothing
1359 getRequestContentType = const Nothing
1019
1360
1020-- | The type function recognizes 'NoReqBody' as having 'NoBody', while any
1361-- | The type function recognizes 'NoReqBody' as having 'NoBody', while any
1021−-- other body option 'CanHaveBody'. This forces user to use 'NoReqBody' with
1362+-- other body option 'CanHaveBody'. This forces the user to use 'NoReqBody'
1022−-- 'GET' method and other methods that should not send a body.
1363+-- with 'GET' method and other methods that should not have body.
1023−
1024type family ProvidesBody body :: CanHaveBody where
1364type family ProvidesBody body :: CanHaveBody where
1025 ProvidesBody NoReqBody = 'NoBody
1365 ProvidesBody NoReqBody = 'NoBody
1026− ProvidesBody body = 'CanHaveBody
1366+ ProvidesBody body = 'CanHaveBody
1027
1367
1028-- | This type function allows any HTTP body if method says it
1368-- | This type function allows any HTTP body if method says it
1029-- 'CanHaveBody'. When the method says it should have 'NoBody', the only
1369-- 'CanHaveBody'. When the method says it should have 'NoBody', the only
1030-- body option to use is 'NoReqBody'.
1370-- body option to use is 'NoReqBody'.
1031−--
1032−-- __Note__: users of GHC 8.0.1 and later will see a slightly more friendly
1033−-- error message when method does not allow a body and body is provided.
1034−
1035−type family HttpBodyAllowed
1371+type family
1372+ HttpBodyAllowed
1036− (allowsBody :: CanHaveBody)
1373+ (allowsBody :: CanHaveBody)
1037− (providesBody :: CanHaveBody) :: Constraint where
1374+ (providesBody :: CanHaveBody) ::
1375+ Constraint
1376+ where
1038− HttpBodyAllowed 'NoBody 'NoBody = ()
1377+ HttpBodyAllowed 'NoBody 'NoBody = ()
1039− HttpBodyAllowed 'CanHaveBody body = ()
1378+ HttpBodyAllowed 'CanHaveBody body = ()
1040−#if MIN_VERSION_base(4,9,0)
1041− HttpBodyAllowed 'NoBody 'CanHaveBody = TypeError
1379+ HttpBodyAllowed 'NoBody 'CanHaveBody =
1380+ TypeError
1042− ('Text "This HTTP method does not allow attaching a request body.")
1381+ ('Text "This HTTP method does not allow attaching a request body.")
1043−#endif
1382+
1044−
1045−instance HttpBody body => RequestComponent (Womb "body" body) where
1383+instance (HttpBody body) => RequestComponent (Tagged "body" body) where
1046− getRequestMod (Womb body) = Endo $ \x ->
1384+ getRequestMod (Tagged body) = Endo $ \x ->
1385+ x
1047− x { L.requestBody = getRequestBody body
1386+ { L.requestBody = getRequestBody body,
1048− , L.requestHeaders =
1387+ L.requestHeaders =
1049− let old = L.requestHeaders x in
1388+ let old = L.requestHeaders x
1050− case getRequestContentType body of
1389+ in case getRequestContentType body of
1051− Nothing -> old
1390+ Nothing -> old
1052− Just contentType ->
1391+ Just contentType ->
1053− (Y.hContentType, contentType) : old }
1392+ (Y.hContentType, contentType) : old
1393+ }
1054
1394
1055----------------------------------------------------------------------------
1395----------------------------------------------------------------------------
1056-- Request—Optional parameters
1396-- Request—Optional parameters
1057
1397
1058-- $optional-parameters
1398-- $optional-parameters
1059--
1399--
1060−-- Optional parameters to a request include things like query parameters,
1400+-- Optional parameters of request include things like query parameters,
1061-- headers, port number, etc. All optional parameters have the type
1401-- headers, port number, etc. All optional parameters have the type
1062-- 'Option', which is a 'Monoid'. This means that you can use 'mempty' as
1402-- 'Option', which is a 'Monoid'. This means that you can use 'mempty' as
1063-- the last argument of 'req' to specify no optional parameters, or combine
1403-- the last argument of 'req' to specify no optional parameters, or combine
1064−-- 'Option's using 'mappend' (or @('<>')@) to have several of them at once.
1404+-- 'Option's using 'mappend' or @('<>')@ to have several of them at once.
1065
1405
1066−-- | Opaque 'Option' type is a 'Monoid' you can use to pack collection of
1406+-- | The opaque 'Option' type is a 'Monoid' you can use to pack collection
1067−-- optional parameters like query parameters and headers. See sections below
1407+-- of optional parameters like query parameters and headers. See sections
1068−-- to learn which 'Option' primitives are available.
1408+-- below to learn which 'Option' primitives are available.
1409+data Option (scheme :: Scheme)
1410+ = Option (Endo (Y.QueryText, L.Request)) (Maybe (L.Request -> IO L.Request))
1069
1411
1070−data Option (scheme :: Scheme) =
1071− Option (Endo (Y.QueryText, L.Request)) (Maybe (L.Request -> IO L.Request))
1072− -- 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
1073− -- Request to avoid appending to existing query string in request every
1413+-- Request to avoid appending to an existing query string in request every
1074− -- time new parameter is added. Additional Maybe (L.Request -> IO
1414+-- time new parameter is added. The additional Maybe (L.Request -> IO
1075− -- 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
1076− -- transformations. This is for authentication methods that sign requests
1416+-- transformations. This is for authentication methods that sign requests
1077− -- based on data in Request.
1417+-- based on data in Request.
1078
1418
1079instance Semigroup (Option scheme) where
1419instance Semigroup (Option scheme) where
1080− Option er0 mr0 <> Option er1 mr1 = Option
1420+ Option er0 mr0 <> Option er1 mr1 =
1421+ Option
1081− (er0 <> er1) (mr0 <|> mr1)
1422+ (er0 <> er1)
1423+ (mr0 <|> mr1)
1082
1424
1083instance Monoid (Option scheme) where
1425instance Monoid (Option scheme) where
1084− mempty = Option mempty Nothing
1426+ mempty = Option mempty Nothing
1085 mappend = (<>)
1427 mappend = (<>)
1086
1428
1429+-- | Use 'formToQuery'.
1430+--
1431+-- @since 3.11.0
1432+instance FromForm (Option scheme) where
1433+ fromForm = Right . formToQuery
1434+
1087-- | 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
1088-- parameters. This helper is not a part of the public API.
1436-- parameters. This helper is not a part of the public API.
1089−
1090withQueryParams :: (Y.QueryText -> Y.QueryText) -> Option scheme
1437withQueryParams :: (Y.QueryText -> Y.QueryText) -> Option scheme
1091withQueryParams f = Option (Endo (first f)) Nothing
1438withQueryParams f = Option (Endo (first f)) Nothing
1092
1439
1093-- | A helper to create an 'Option' that modifies only 'L.Request'. This
1440-- | A helper to create an 'Option' that modifies only 'L.Request'. This
1094-- helper is not a part of public API.
1441-- helper is not a part of public API.
1095−
1096withRequest :: (L.Request -> L.Request) -> Option scheme
1442withRequest :: (L.Request -> L.Request) -> Option scheme
1097withRequest f = Option (Endo (second f)) Nothing
1443withRequest f = Option (Endo (second f)) Nothing
1098
1444
1099−-- | A helper to create an 'Option' that adds a finalizer (request
1100−-- transformation that is applied after all other modifications).
1101−
1102−asFinalizer :: (L.Request -> IO L.Request) -> Option scheme
1103−asFinalizer = Option mempty . pure
1104−
1105instance RequestComponent (Option scheme) where
1445instance RequestComponent (Option scheme) where
1106 getRequestMod (Option f _) = Endo $ \x ->
1446 getRequestMod (Option f _) = Endo $ \x ->
1107 let (qparams, x') = appEndo f ([], x)
1447 let (qparams, x') = appEndo f ([], x)
1108− query = Y.renderQuery True (Y.queryTextToQuery qparams)
1448+ query = Y.renderQuery True (Y.queryTextToQuery qparams)
1109− in x' { L.queryString = query }
1449+ in x' {L.queryString = query}
1110
1450
1111-- | Finalize given 'L.Request' by applying a finalizer from the given
1451-- | Finalize given 'L.Request' by applying a finalizer from the given
1112-- 'Option' (if it has any).
1452-- 'Option' (if it has any).
1113−
1114−finalizeRequest :: MonadIO m => Option scheme -> L.Request -> m L.Request
1453+finalizeRequest :: (MonadIO m) => Option scheme -> L.Request -> m L.Request
1115finalizeRequest (Option _ mfinalizer) = liftIO . fromMaybe pure mfinalizer
1454finalizeRequest (Option _ mfinalizer) = liftIO . fromMaybe pure mfinalizer
1116
1455
1117----------------------------------------------------------------------------
1456----------------------------------------------------------------------------
@@ -1124,47 +1463,67 @@ finalizeRequest (Option _ mfinalizer) = liftIO . fromMaybe pure mfinalizer
1124-- bodies (of the type 'FormUrlEncodedParam').
1463-- bodies (of the type 'FormUrlEncodedParam').
1125
1464
1126-- | This operator builds a query parameter that will be included in URL of
1465-- | This operator builds a query parameter that will be included in URL of
1127−-- your request after question sign @?@. This is the same syntax you use
1466+-- your request after the question sign @?@. This is the same syntax you use
1128-- with form URL encoded request bodies.
1467-- with form URL encoded request bodies.
1129--
1468--
1130-- This operator is defined in terms of 'queryParam':
1469-- This operator is defined in terms of 'queryParam':
1131--
1470--
1132-- > name =: value = queryParam name (pure value)
1471-- > name =: value = queryParam name (pure value)
1133−
1134infix 7 =:
1472infix 7 =:
1473+
1135(=:) :: (QueryParam param, ToHttpApiData a) => Text -> a -> param
1474(=:) :: (QueryParam param, ToHttpApiData a) => Text -> a -> param
1136name =: value = queryParam name (pure value)
1475name =: value = queryParam name (pure value)
1137
1476
1138−-- | Construct a flag, that is, valueless query parameter. For example, in
1477+-- | Construct a flag, that is, a valueless query parameter. For example, in
1139−-- the following URL @a@ is a flag, while @b@ is a query parameter with a
1478+-- the following URL @\"a\"@ is a flag, while @\"b\"@ is a query parameter
1140−-- value:
1479+-- with a value:
1141--
1480--
1142-- > https://httpbin.org/foo/bar?a&b=10
1481-- > https://httpbin.org/foo/bar?a&b=10
1143--
1482--
1144-- This operator is defined in terms of 'queryParam':
1483-- This operator is defined in terms of 'queryParam':
1145--
1484--
1146-- > queryFlag name = queryParam name (Nothing :: Maybe ())
1485-- > queryFlag name = queryParam name (Nothing :: Maybe ())
1147−
1148−queryFlag :: QueryParam param => Text -> param
1486+queryFlag :: (QueryParam param) => Text -> param
1149queryFlag name = queryParam name (Nothing :: Maybe ())
1487queryFlag name = queryParam name (Nothing :: Maybe ())
1150
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+
1151-- | 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
1152-- 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
1153-- 'FormUrlEncodedParam' when constructing form URL encoded request bodies.
1509-- 'FormUrlEncodedParam' when constructing form URL encoded request bodies.
1154-- Having the same syntax for these cases seems natural and user-friendly.
1510-- Having the same syntax for these cases seems natural and user-friendly.
1155−
1156class QueryParam param where
1511class QueryParam param where
1157−
1158 -- | Create a query parameter with given name and value. If value is
1512 -- | Create a query parameter with given name and value. If value is
1159 -- '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
1160 -- way). It's recommended to use @('=:')@ and 'queryFlag' instead of this
1514 -- way). It's recommended to use @('=:')@ and 'queryFlag' instead of this
1161 -- method, because they are easier to read.
1515 -- method, because they are easier to read.
1516+ queryParam :: (ToHttpApiData a) => Text -> Maybe a -> param
1162
1517
1163− queryParam :: ToHttpApiData a => Text -> Maybe a -> param
1518+ -- | Get the query parameter names and values set by 'queryParam'.
1519+ --
1520+ -- @since 3.11.0
1521+ queryParamToList :: param -> [(Text, Maybe Text)]
1164
1522
1165instance QueryParam (Option scheme) where
1523instance QueryParam (Option scheme) where
1166 queryParam name mvalue =
1524 queryParam name mvalue =
1167 withQueryParams ((:) (name, toQueryParam <$> mvalue))
1525 withQueryParams ((:) (name, toQueryParam <$> mvalue))
1526+ queryParamToList (Option f _) = fst $ appEndo f ([], L.defaultRequest)
1168
1527
1169----------------------------------------------------------------------------
1528----------------------------------------------------------------------------
1170-- Request—Optional parameters—Headers
1529-- Request—Optional parameters—Headers
@@ -1173,19 +1532,28 @@ instance QueryParam (Option scheme) where
1173-- headers with the same names the leftmost header will win. This means, in
1532-- headers with the same names the leftmost header will win. This means, in
1174-- particular, that you cannot create a request with several headers of the
1533-- particular, that you cannot create a request with several headers of the
1175-- same name.
1534-- same name.
1176−
1177−header
1535+header ::
1178− :: ByteString -- ^ Header name
1536+ -- | Header name
1537+ ByteString ->
1179− -> ByteString -- ^ Header value
1538+ -- | Header value
1539+ ByteString ->
1180− -> Option scheme
1540+ Option scheme
1181header name value = withRequest (attachHeader name value)
1541header name value = withRequest (attachHeader name value)
1182
1542
1183−-- | A non-public helper that attaches a header with given name and content
1543+-- | Attach a header with given name and content to a 'L.Request'.
1184−-- to a 'L.Request'.
1544+--
1185−
1545+-- @since 1.1.0
1186attachHeader :: ByteString -> ByteString -> L.Request -> L.Request
1546attachHeader :: ByteString -> ByteString -> L.Request -> L.Request
1187attachHeader name value x =
1547attachHeader name value x =
1188− x { L.requestHeaders = (CI.mk name, value) : L.requestHeaders x }
1548+ x {L.requestHeaders = (CI.mk name, value) : L.requestHeaders x}
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}
1189
1557
1190----------------------------------------------------------------------------
1558----------------------------------------------------------------------------
1191-- Request—Optional parameters—Cookies
1559-- Request—Optional parameters—Cookies
@@ -1198,10 +1566,9 @@ attachHeader name value x =
1198
1566
1199-- | Use the given 'L.CookieJar'. A 'L.CookieJar' can be obtained from a
1567-- | Use the given 'L.CookieJar'. A 'L.CookieJar' can be obtained from a
1200-- 'L.Response' record.
1568-- 'L.Response' record.
1201−
1202cookieJar :: L.CookieJar -> Option scheme
1569cookieJar :: L.CookieJar -> Option scheme
1203cookieJar jar = withRequest $ \x ->
1570cookieJar jar = withRequest $ \x ->
1204− x { L.cookieJar = Just jar }
1571+ x {L.cookieJar = Just jar}
1205
1572
1206----------------------------------------------------------------------------
1573----------------------------------------------------------------------------
1207-- Request—Optional parameters—Authentication
1574-- Request—Optional parameters—Authentication
@@ -1219,42 +1586,66 @@ cookieJar jar = withRequest $ \x ->
1219-- | The 'Option' adds basic authentication.
1586-- | The 'Option' adds basic authentication.
1220--
1587--
1221-- See also: <https://en.wikipedia.org/wiki/Basic_access_authentication>.
1588-- See also: <https://en.wikipedia.org/wiki/Basic_access_authentication>.
1222−
1223−basicAuth
1589+basicAuth ::
1224− :: ByteString -- ^ Username
1590+ -- | Username
1225− -> ByteString -- ^ Password
1591+ ByteString ->
1592+ -- | Password
1593+ ByteString ->
1226− -> Option 'Https -- ^ Auth 'Option'
1594+ -- | Auth 'Option'
1595+ Option 'Https
1227basicAuth = basicAuthUnsafe
1596basicAuth = basicAuthUnsafe
1228
1597
1229-- | An alternative to 'basicAuth' which works for any scheme. Note that
1598-- | An alternative to 'basicAuth' which works for any scheme. Note that
1230−-- using basic access authentication without SSL/TLS is vulnerable to
1599+-- using basic access authentication without SSL\/TLS is vulnerable to
1231-- attacks. Use 'basicAuth' instead unless you know what you are doing.
1600-- attacks. Use 'basicAuth' instead unless you know what you are doing.
1232--
1601--
1233-- @since 0.3.1
1602-- @since 0.3.1
1234−
1235−basicAuthUnsafe
1603+basicAuthUnsafe ::
1236− :: ByteString -- ^ Username
1604+ -- | Username
1605+ ByteString ->
1237− -> ByteString -- ^ Password
1606+ -- | Password
1607+ ByteString ->
1238− -> Option scheme -- ^ Auth 'Option'
1608+ -- | Auth 'Option'
1609+ Option scheme
1239−basicAuthUnsafe username password = asFinalizer
1610+basicAuthUnsafe username password =
1611+ customAuth
1240− (pure . L.applyBasicAuth username password)
1612+ (pure . L.applyBasicAuth username password)
1613+
1614+-- | The 'Option' set basic proxy authentication header.
1615+--
1616+-- @since 1.1.0
1617+basicProxyAuth ::
1618+ -- | Username
1619+ ByteString ->
1620+ -- | Password
1621+ ByteString ->
1622+ -- | Auth 'Option'
1623+ Option scheme
1624+basicProxyAuth username password =
1625+ withRequest (L.applyBasicProxyAuth username password)
1241
1626
1242-- | The 'Option' adds OAuth1 authentication.
1627-- | The 'Option' adds OAuth1 authentication.
1243--
1628--
1244-- @since 0.2.0
1629-- @since 0.2.0
1245−
1246−oAuth1
1630+oAuth1 ::
1247− :: ByteString -- ^ Consumer token
1631+ -- | Consumer token
1632+ ByteString ->
1248− -> ByteString -- ^ Consumer secret
1633+ -- | Consumer secret
1249− -> ByteString -- ^ OAuth token
1634+ ByteString ->
1635+ -- | OAuth token
1636+ ByteString ->
1250− -> ByteString -- ^ OAuth token secret
1637+ -- | OAuth token secret
1638+ ByteString ->
1251− -> Option scheme -- ^ Auth 'Option'
1639+ -- | Auth 'Option'
1640+ Option scheme
1252oAuth1 consumerToken consumerSecret token tokenSecret =
1641oAuth1 consumerToken consumerSecret token tokenSecret =
1253− asFinalizer (OAuth.signOAuth app creds)
1642+ customAuth (OAuth.signOAuth app creds)
1254 where
1643 where
1644+ app =
1255− app = OAuth.newOAuth
1645+ OAuth.newOAuth
1256− { OAuth.oauthConsumerKey = consumerToken
1646+ { OAuth.oauthConsumerKey = consumerToken,
1257− , OAuth.oauthConsumerSecret = consumerSecret }
1647+ OAuth.oauthConsumerSecret = consumerSecret
1648+ }
1258 creds = OAuth.newCredential token tokenSecret
1649 creds = OAuth.newCredential token tokenSecret
1259
1650
1260-- | The 'Option' adds an OAuth2 bearer token. This is treated by many
1651-- | The 'Option' adds an OAuth2 bearer token. This is treated by many
@@ -1265,12 +1656,14 @@ oAuth1 consumerToken consumerSecret token tokenSecret =
1265-- > oAuth2Bearer token = header "Authorization" ("Bearer " <> token)
1656-- > oAuth2Bearer token = header "Authorization" ("Bearer " <> token)
1266--
1657--
1267-- See also: <https://en.wikipedia.org/wiki/OAuth>.
1658-- See also: <https://en.wikipedia.org/wiki/OAuth>.
1268−
1269−oAuth2Bearer
1659+oAuth2Bearer ::
1270− :: ByteString -- ^ Token
1660+ -- | Token
1661+ ByteString ->
1271− -> Option 'Https -- ^ Auth 'Option'
1662+ -- | Auth 'Option'
1663+ Option 'Https
1272−oAuth2Bearer token = asFinalizer
1664+oAuth2Bearer token =
1665+ customAuth
1273− (pure . attachHeader "Authorization" ("Bearer " <> token))
1666+ (pure . attachHeader "Authorization" ("Bearer " <> token))
1274
1667
1275-- | The 'Option' adds a not-quite-standard OAuth2 bearer token (that seems
1668-- | The 'Option' adds a not-quite-standard OAuth2 bearer token (that seems
1276-- to be used only by GitHub). This will be treated by whatever services
1669-- to be used only by GitHub). This will be treated by whatever services
@@ -1281,26 +1674,35 @@ oAuth2Bearer token = asFinalizer
1281-- > oAuth2Token token = header "Authorization" ("token" <> token)
1674-- > oAuth2Token token = header "Authorization" ("token" <> token)
1282--
1675--
1283-- See also: <https://developer.github.com/v3/oauth#3-use-the-access-token-to-access-the-api>.
1676-- See also: <https://developer.github.com/v3/oauth#3-use-the-access-token-to-access-the-api>.
1284−
1285−oAuth2Token
1677+oAuth2Token ::
1286− :: ByteString -- ^ Token
1678+ -- | Token
1679+ ByteString ->
1287− -> Option 'Https -- ^ Auth 'Option'
1680+ -- | Auth 'Option'
1681+ Option 'Https
1288−oAuth2Token token = asFinalizer
1682+oAuth2Token token =
1683+ customAuth
1289− (pure . attachHeader "Authorization" ("token " <> token))
1684+ (pure . attachHeader "Authorization" ("token " <> token))
1685+
1686+-- | A helper to create custom authentication 'Option's. The given
1687+-- 'IO'-enabled request transformation is applied after all other
1688+-- modifications when constructing a request. Use wisely.
1689+--
1690+-- @since 1.1.0
1691+customAuth :: (L.Request -> IO L.Request) -> Option scheme
1692+customAuth = Option mempty . pure
1290
1693
1291----------------------------------------------------------------------------
1694----------------------------------------------------------------------------
1292-- Request—Optional parameters—Other
1695-- Request—Optional parameters—Other
1293
1696
1294-- | Specify the port to connect to explicitly. Normally, 'Url' you use
1697-- | Specify the port to connect to explicitly. Normally, 'Url' you use
1295-- 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
1296−-- 'Option' allows to choose arbitrary port overwriting the defaults.
1699+-- 'Option' allows us to choose an arbitrary port overwriting the defaults.
1297−
1298port :: Int -> Option scheme
1700port :: Int -> Option scheme
1299port n = withRequest $ \x ->
1701port n = withRequest $ \x ->
1300− x { L.port = n }
1702+ x {L.port = n}
1301
1703
1302-- | This 'Option' controls whether gzipped data should be decompressed on
1704-- | This 'Option' controls whether gzipped data should be decompressed on
1303−-- the fly. By default everything except for @application\/x-tar@ is
1705+-- the fly. By default everything except for @\"application\/x-tar\"@ is
1304-- decompressed, i.e. we have:
1706-- decompressed, i.e. we have:
1305--
1707--
1306-- > decompress (/= "application/x-tar")
1708-- > decompress (/= "application/x-tar")
@@ -1308,163 +1710,212 @@ port n = withRequest $ \x ->
1308-- You can also choose to decompress everything like this:
1710-- You can also choose to decompress everything like this:
1309--
1711--
1310-- > decompress (const True)
1712-- > decompress (const True)
1311−
1312−decompress
1713+decompress ::
1313− :: (ByteString -> Bool) -- ^ Predicate that is given MIME type, it
1714+ -- | Predicate that is given MIME type, it returns 'True' when content
1314− -- returns 'True' when content should be decompressed on the fly.
1715+ -- should be decompressed on the fly.
1716+ (ByteString -> Bool) ->
1315− -> Option scheme
1717+ Option scheme
1316decompress f = withRequest $ \x ->
1718decompress f = withRequest $ \x ->
1317− x { L.decompress = f }
1719+ x {L.decompress = f}
1318
1720
1319-- | Specify the number of microseconds to wait for response. The default
1721-- | Specify the number of microseconds to wait for response. The default
1320−-- value is 30 seconds.
1722+-- value is 30 seconds (defined in 'L.ManagerSettings' of connection
1321−
1723+-- 'L.Manager').
1322−responseTimeout
1724+responseTimeout ::
1323− :: Int -- ^ Number of microseconds to wait
1725+ -- | Number of microseconds to wait
1726+ Int ->
1324− -> Option scheme
1727+ Option scheme
1325responseTimeout n = withRequest $ \x ->
1728responseTimeout n = withRequest $ \x ->
1326− x { L.responseTimeout = LI.ResponseTimeoutMicro n }
1729+ x {L.responseTimeout = LI.ResponseTimeoutMicro n}
1327
1730
1328-- | HTTP version to send to the server, the default is HTTP 1.1.
1731-- | HTTP version to send to the server, the default is HTTP 1.1.
1329−
1330−httpVersion
1732+httpVersion ::
1331− :: Int -- ^ Major version number
1733+ -- | Major version number
1734+ Int ->
1332− -> Int -- ^ Minor version number
1735+ -- | Minor version number
1736+ Int ->
1333− -> Option scheme
1737+ Option scheme
1334httpVersion major minor = withRequest $ \x ->
1738httpVersion major minor = withRequest $ \x ->
1335− x { L.requestVersion = Y.HttpVersion major minor }
1739+ x {L.requestVersion = Y.HttpVersion major minor}
1336
1740
1337----------------------------------------------------------------------------
1741----------------------------------------------------------------------------
1338-- Response interpretations
1742-- Response interpretations
1339
1743
1340−-- | Make a request and ignore body of response.
1744+-- | Make a request and ignore the body of the response.
1341−
1342−data IgnoreResponse = IgnoreResponse (L.Response ())
1745+newtype IgnoreResponse = IgnoreResponse (L.Response ())
1746+ deriving (Show)
1343
1747
1344instance HttpResponse IgnoreResponse where
1748instance HttpResponse IgnoreResponse where
1345 type HttpResponseBody IgnoreResponse = ()
1749 type HttpResponseBody IgnoreResponse = ()
1346− toVanillaResponse (IgnoreResponse response) = response
1750+ toVanillaResponse (IgnoreResponse r) = r
1347− getHttpResponse request manager =
1751+ getHttpResponse r = return $ IgnoreResponse (void r)
1348− IgnoreResponse <$> liftIO (L.httpNoBody request manager)
1349− makeResponseBodyPreview _ = "<ignored response>"
1350
1752
1351-- | Use this as the fourth argument of 'req' to specify that you want it to
1753-- | Use this as the fourth argument of 'req' to specify that you want it to
1352−-- return the 'IgnoreResponse' interpretation.
1754+-- ignore the response body.
1353−
1354ignoreResponse :: Proxy IgnoreResponse
1755ignoreResponse :: Proxy IgnoreResponse
1355ignoreResponse = Proxy
1756ignoreResponse = Proxy
1356
1757
1357−-- | Make a request and interpret body of response as JSON. The
1758+-- | Make a request and interpret the body of the response as JSON. The
1358-- 'handleHttpException' method of 'MonadHttp' instance corresponding to
1759-- 'handleHttpException' method of 'MonadHttp' instance corresponding to
1359-- monad in which you use 'req' will determine what to do in the case when
1760-- monad in which you use 'req' will determine what to do in the case when
1360-- parsing fails (the 'JsonHttpException' constructor will be used).
1761-- parsing fails (the 'JsonHttpException' constructor will be used).
1762+newtype JsonResponse a = JsonResponse (L.Response a)
1763+ deriving (Show)
1361
1764
1362−data JsonResponse a = JsonResponse (L.Response a) ByteString
1363−
1364−instance FromJSON a => HttpResponse (JsonResponse a) where
1765+instance (FromJSON a) => HttpResponse (JsonResponse a) where
1365 type HttpResponseBody (JsonResponse a) = a
1766 type HttpResponseBody (JsonResponse a) = a
1366− toVanillaResponse (JsonResponse response _) = response
1767+ toVanillaResponse (JsonResponse r) = r
1367− getHttpResponse request manager = do
1768+ getHttpResponse r = do
1368− response <- L.httpLbs request manager
1769+ chunks <- L.brConsume (L.responseBody r)
1369− case A.eitherDecode (L.responseBody response) of
1770+ case A.eitherDecode (BL.fromChunks chunks) of
1370 Left e -> throwIO (JsonHttpException e)
1771 Left e -> throwIO (JsonHttpException e)
1371− Right x -> do
1372− let preview
1373− = BL.toStrict
1374− . BL.take bodyPreviewLength
1375− . L.responseBody
1376− $ response
1377− return $ JsonResponse response { L.responseBody = x } preview
1772+ Right x -> return $ JsonResponse (x <$ r)
1378− makeResponseBodyPreview (JsonResponse _ preview) = preview
1773+ acceptHeader Proxy = Just "application/json"
1379−
1380−-- | Use this as the forth argument of 'req' to specify that you want it to
1381−-- return the 'JsonResponse' interpretation.
1382
1774
1775+-- | Use this as the fourth argument of 'req' to specify that you want it to
1776+-- return the 'JsonResponse' interpretation.
1383jsonResponse :: Proxy (JsonResponse a)
1777jsonResponse :: Proxy (JsonResponse a)
1384jsonResponse = Proxy
1778jsonResponse = Proxy
1385
1779
1386−-- | Make a request and interpret body of response as a strict 'ByteString'.
1780+-- | Make a request and interpret the body of the response as a strict
1387−
1781+-- 'ByteString'.
1388newtype BsResponse = BsResponse (L.Response ByteString)
1782newtype BsResponse = BsResponse (L.Response ByteString)
1783+ deriving (Show)
1389
1784
1390instance HttpResponse BsResponse where
1785instance HttpResponse BsResponse where
1391 type HttpResponseBody BsResponse = ByteString
1786 type HttpResponseBody BsResponse = ByteString
1392− toVanillaResponse (BsResponse response) = response
1787+ toVanillaResponse (BsResponse r) = r
1393− getHttpResponse request manager =
1788+ getHttpResponse r = do
1394− L.withResponse request manager $ \response -> do
1395− chunks <- L.brConsume (L.responseBody response)
1789+ chunks <- L.brConsume (L.responseBody r)
1396− return $ BsResponse response { L.responseBody = B.concat chunks }
1790+ return $ BsResponse (B.concat chunks <$ r)
1397− makeResponseBodyPreview = B.take bodyPreviewLength . responseBody
1398−
1399−-- | Use this as the forth argument of 'req' to specify that you want to
1400−-- interpret response body as a strict 'ByteString'.
1401
1791
1792+-- | Use this as the fourth argument of 'req' to specify that you want to
1793+-- interpret the response body as a strict 'ByteString'.
1402bsResponse :: Proxy BsResponse
1794bsResponse :: Proxy BsResponse
1403bsResponse = Proxy
1795bsResponse = Proxy
1404
1796
1405−-- | Make a request and interpret body of response as a lazy
1797+-- | Make a request and interpret the body of the response as a lazy
1406-- 'BL.ByteString'.
1798-- 'BL.ByteString'.
1407−
1408newtype LbsResponse = LbsResponse (L.Response BL.ByteString)
1799newtype LbsResponse = LbsResponse (L.Response BL.ByteString)
1800+ deriving (Show)
1409
1801
1410instance HttpResponse LbsResponse where
1802instance HttpResponse LbsResponse where
1411 type HttpResponseBody LbsResponse = BL.ByteString
1803 type HttpResponseBody LbsResponse = BL.ByteString
1412− toVanillaResponse (LbsResponse response) = response
1804+ toVanillaResponse (LbsResponse r) = r
1413− getHttpResponse request manager =
1805+ getHttpResponse r = do
1414− LbsResponse <$> L.httpLbs request manager
1806+ chunks <- L.brConsume (L.responseBody r)
1415− makeResponseBodyPreview = BL.toStrict . BL.take 1027 . responseBody
1807+ return $ LbsResponse (BL.fromChunks chunks <$ r)
1416−
1417−-- | Use this as the forth argument of 'req' to specify that you want to
1418−-- interpret response body as a lazy 'BL.ByteString'.
1419
1808
1809+-- | Use this as the fourth argument of 'req' to specify that you want to
1810+-- interpret the response body as a lazy 'BL.ByteString'.
1420lbsResponse :: Proxy LbsResponse
1811lbsResponse :: Proxy LbsResponse
1421lbsResponse = Proxy
1812lbsResponse = Proxy
1422
1813
1814+----------------------------------------------------------------------------
1815+-- Helpers for response interpretations
1816+
1817+-- | Fetch beginning of the response and return it together with a new
1818+-- @'L.Response' 'L.BodyReader'@ that can be passed to 'getHttpResponse' and
1819+-- such.
1820+grabPreview ::
1821+ -- | How many bytes to fetch
1822+ Int ->
1823+ -- | Response with body reader inside
1824+ L.Response L.BodyReader ->
1825+ -- | Preview 'ByteString' and new response with body reader inside
1826+ IO (ByteString, L.Response L.BodyReader)
1827+grabPreview nbytes r = do
1828+ let br = L.responseBody r
1829+ (target, leftover, done) <- brReadN br nbytes
1830+ nref <- newIORef (0 :: Int)
1831+ let br' = do
1832+ n <- readIORef nref
1833+ let incn = modifyIORef' nref (+ 1)
1834+ case n of
1835+ 0 -> do
1836+ incn
1837+ if B.null target
1838+ then br'
1839+ else return target
1840+ 1 -> do
1841+ incn
1842+ if B.null leftover
1843+ then br'
1844+ else return leftover
1845+ _ ->
1846+ if done
1847+ then return B.empty
1848+ else br
1849+ return (target, r {L.responseBody = br'})
1850+
1851+-- | Consume N bytes from 'L.BodyReader', return the target chunk, the
1852+-- leftover (may be empty), and whether we're done consuming the body.
1853+brReadN ::
1854+ -- | Body reader to stream from
1855+ L.BodyReader ->
1856+ -- | How many bytes to consume
1857+ Int ->
1858+ -- | Target chunk, the leftover, whether we're done
1859+ IO (ByteString, ByteString, Bool)
1860+brReadN br n = go 0 id id
1861+ where
1862+ go !tlen t l = do
1863+ chunk <- br
1864+ if B.null chunk
1865+ then return (r t, r l, True)
1866+ else do
1867+ let (target, leftover) = B.splitAt (n - tlen) chunk
1868+ tlen' = B.length target
1869+ t' = t . (target :)
1870+ l' = l . (leftover :)
1871+ if tlen + tlen' < n
1872+ then go (tlen + tlen') t' l'
1873+ else return (r t', r l', False)
1874+ r f = B.concat (f [])
1875+
1423----------------------------------------------------------------------------
1876----------------------------------------------------------------------------
1424-- Inspecting a response
1877-- Inspecting a response
1425
1878
1426-- | Get the response body.
1879-- | Get the response body.
1427−
1428−responseBody
1880+responseBody ::
1429− :: HttpResponse response
1881+ (HttpResponse response) =>
1430− => response
1882+ response ->
1431− -> HttpResponseBody response
1883+ HttpResponseBody response
1432responseBody = L.responseBody . toVanillaResponse
1884responseBody = L.responseBody . toVanillaResponse
1433
1885
1434-- | Get the response status code.
1886-- | Get the response status code.
1435−
1436−responseStatusCode
1887+responseStatusCode ::
1437− :: HttpResponse response
1888+ (HttpResponse response) =>
1438− => response
1889+ response ->
1439− -> Int
1890+ Int
1440responseStatusCode =
1891responseStatusCode =
1441 Y.statusCode . L.responseStatus . toVanillaResponse
1892 Y.statusCode . L.responseStatus . toVanillaResponse
1442
1893
1443-- | Get the response status message.
1894-- | Get the response status message.
1444−
1445−responseStatusMessage
1895+responseStatusMessage ::
1446− :: HttpResponse response
1896+ (HttpResponse response) =>
1447− => response
1897+ response ->
1448− -> ByteString
1898+ ByteString
1449responseStatusMessage =
1899responseStatusMessage =
1450 Y.statusMessage . L.responseStatus . toVanillaResponse
1900 Y.statusMessage . L.responseStatus . toVanillaResponse
1451
1901
1452-- | Lookup a particular header from a response.
1902-- | Lookup a particular header from a response.
1453−
1454−responseHeader
1903+responseHeader ::
1455− :: HttpResponse response
1904+ (HttpResponse response) =>
1456− => response -- ^ Response interpretation
1905+ -- | Response interpretation
1906+ response ->
1457− -> ByteString -- ^ Header to lookup
1907+ -- | Header to lookup
1908+ ByteString ->
1458− -> Maybe ByteString -- ^ Header value if found
1909+ -- | Header value if found
1910+ Maybe ByteString
1459responseHeader r h =
1911responseHeader r h =
1460 (lookup (CI.mk h) . L.responseHeaders . toVanillaResponse) r
1912 (lookup (CI.mk h) . L.responseHeaders . toVanillaResponse) r
1461
1913
1462-- | Get the response 'L.CookieJar'.
1914-- | Get the response 'L.CookieJar'.
1463−
1464−responseCookieJar
1915+responseCookieJar ::
1465− :: HttpResponse response
1916+ (HttpResponse response) =>
1466− => response
1917+ response ->
1467− -> L.CookieJar
1918+ L.CookieJar
1468responseCookieJar = L.responseCookieJar . toVanillaResponse
1919responseCookieJar = L.responseCookieJar . toVanillaResponse
1469
1920
1470----------------------------------------------------------------------------
1921----------------------------------------------------------------------------
@@ -1473,37 +1924,58 @@ responseCookieJar = L.responseCookieJar . toVanillaResponse
1473-- $new-response-interpretation
1924-- $new-response-interpretation
1474--
1925--
1475-- 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
1476−-- type an instance of 'HttpResponse' type class.
1927+-- type an instance of the 'HttpResponse' type class.
1477−
1478−-- | A type class for response interpretations. It allows to fully control
1479−-- how request is made and how its body is parsed.
1480
1928
1929+-- | A type class for response interpretations. It allows us to describe how
1930+-- to consume the response from a @'L.Response' 'L.BodyReader'@ and produce
1931+-- the final result that is to be returned to the user.
1481class HttpResponse response where
1932class HttpResponse response where
1482−
1483 -- | 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
1484 -- instance of 'HttpResponse'.
1934 -- instance of 'HttpResponse'.
1485−
1486− type HttpResponseBody response :: *
1935+ type HttpResponseBody response :: Type
1487
1936
1488 -- | The method describes how to get the underlying 'L.Response' record.
1937 -- | The method describes how to get the underlying 'L.Response' record.
1489−
1490 toVanillaResponse :: response -> L.Response (HttpResponseBody response)
1938 toVanillaResponse :: response -> L.Response (HttpResponseBody response)
1491
1939
1492− -- | This method describes how to make an HTTP request given 'L.Request'
1940+ -- | This method describes how to consume response body and, more
1493− -- (prepared by the library) and 'L.Manager'.
1941+ -- generally, obtain @response@ value from @'L.Response' 'L.BodyReader'@.
1494−
1942+ --
1943+ -- __Note__: 'L.BodyReader' is nothing but @'IO' 'ByteString'@. You should
1944+ -- call this action repeatedly until it yields the empty 'ByteString'. In
1945+ -- that case streaming of response is finished (which apparently leads to
1946+ -- closing of the connection, so don't call the reader after it has
1947+ -- returned the empty 'ByteString' once) and you can concatenate the
1948+ -- chunks to obtain the final result. (Of course you could as well stream
1949+ -- the contents to a file or do whatever you want.)
1950+ --
1951+ -- __Note__: signature of this function was changed in the version
1952+ -- /1.0.0/.
1495− getHttpResponse :: L.Request -> L.Manager -> IO response
1953+ getHttpResponse ::
1496−
1954+ -- | Response with body reader inside
1497− -- | Construct a “preview” of response body. It is recommend to limit the
1955+ L.Response L.BodyReader ->
1498− -- length to 1024 bytes. This is mainly useful for inclusion of response
1956+ -- | The final result
1499− -- body fragments in exceptions.
1957+ IO response
1958+
1959+ -- | The value of @\"Accept\"@ header. This is useful, for example, if a
1960+ -- website supports both @XML@ and @JSON@ responses, and decides what to
1961+ -- reply with based on what @Accept@ headers you have sent.
1500 --
1962 --
1501− -- __Note__: in versions 0.3.0–0.4.0 this function returned @'IO'
1963+ -- __Note__: manually specified 'Options' that set the @\"Accept\"@ header
1502− -- 'ByteString'@.
1964+ -- will take precedence.
1503 --
1965 --
1504− -- @since 0.5.0
1966+ -- @since 2.1.0
1967+ acceptHeader :: Proxy response -> Maybe ByteString
1968+ acceptHeader Proxy = Nothing
1505
1969
1506− makeResponseBodyPreview :: response -> ByteString
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
1507
1979
1508----------------------------------------------------------------------------
1980----------------------------------------------------------------------------
1509-- Other
1981-- Other
@@ -1513,15 +1985,14 @@ class HttpResponse response where
1513-- 'RequestComponent' changing\/overwriting something in it. 'Endo' is a
1985-- 'RequestComponent' changing\/overwriting something in it. 'Endo' is a
1514-- monoid of endomorphisms under composition, it's used to chain different
1986-- monoid of endomorphisms under composition, it's used to chain different
1515-- request components easier using @('<>')@.
1987-- request components easier using @('<>')@.
1516−
1988+--
1989+-- __Note__: this type class is not a part of the public API.
1517class RequestComponent a where
1990class RequestComponent a where
1518−
1519 -- | Get a function that takes a 'L.Request' and changes it somehow
1991 -- | Get a function that takes a 'L.Request' and changes it somehow
1520− -- returning another 'L.Request'. For example HTTP method instance of
1992+ -- returning another 'L.Request'. For example, the 'HttpMethod' instance
1521− -- 'RequestComponent' just overwrites method. The function is wrapped in
1993+ -- of 'RequestComponent' just overwrites method. The function is wrapped
1522− -- 'Endo' so it's easier to chain such “modifying applications” together
1994+ -- in 'Endo' so it's easier to chain such “modifying applications”
1523− -- building bigger and bigger 'RequestComponent's.
1995+ -- together building bigger and bigger 'RequestComponent's.
1524−
1525 getRequestMod :: a -> Endo L.Request
1996 getRequestMod :: a -> Endo L.Request
1526
1997
1527-- | This wrapper is only used to attach a type-level tag to a given type.
1998-- | This wrapper is only used to attach a type-level tag to a given type.
@@ -1531,42 +2002,50 @@ class RequestComponent a where
1531-- method@ and @'HttpBody' body => 'RequestComponent' body@ when it decides
2002-- method@ and @'HttpBody' body => 'RequestComponent' body@ when it decides
1532-- 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,
1533-- when instance is already chosen).
2004-- when instance is already chosen).
1534−
1535−newtype Womb (tag :: Symbol) a = Womb a
2005+newtype Tagged (tag :: Symbol) a = Tagged a
1536
2006
1537-- | Exceptions that this library throws.
2007-- | Exceptions that this library throws.
1538−
1539data HttpException
2008data HttpException
1540− = VanillaHttpException L.HttpException
1541− -- ^ A wrapper with an 'L.HttpException' from "Network.HTTP.Client"
2009+ = -- | A wrapper with an 'L.HttpException' from "Network.HTTP.Client"
1542− | JsonHttpException String
2010+ VanillaHttpException L.HttpException
1543− -- ^ A wrapper with Aeson-produced 'String' describing why decoding failed
2011+ | -- | A wrapper with Aeson-produced 'String' describing why decoding
2012+ -- failed
2013+ JsonHttpException String
1544− deriving (Show, Typeable, Generic)
2014+ deriving (Show, Generic)
1545
2015
1546instance Exception HttpException
2016instance Exception HttpException
1547
2017
1548−-- | A simple 'Bool'-like type we only have for better error messages. We
2018+-- | Return 'Just' if the given 'HttpException' is wrapping a http-client's
1549−-- use it as a kind and its data constructors as type-level tags.
2019+-- 'L.StatusCodeException'. Otherwise, return 'Nothing'.
1550--
2020--
1551−-- See also: 'HttpMethod' and 'HttpBody'.
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
1552
2031
2032+-- | A simple type isomorphic to 'Bool' that we only have for better error
2033+-- messages. We use it as a kind and its data constructors as type-level
2034+-- tags.
2035+--
2036+-- See also: 'HttpMethod' and 'HttpBody'.
1553data CanHaveBody
2037data CanHaveBody
1554− = CanHaveBody -- ^ Indeed can have a body
2038+ = -- | Indeed can have a body
2039+ CanHaveBody
1555− | NoBody -- ^ Should not have a body
2040+ | -- | Should not have a body
2041+ NoBody
1556
2042
1557-- | A type-level tag that specifies URL scheme used (and thus if HTTPS is
2043-- | A type-level tag that specifies URL scheme used (and thus if HTTPS is
1558-- enabled). This is used to force TLS requirement for some authentication
2044-- enabled). This is used to force TLS requirement for some authentication
1559-- 'Option's.
2045-- 'Option's.
1560−
1561data Scheme
2046data Scheme
2047+ = -- | HTTP
1562− = Http -- ^ HTTP
2048+ Http
2049+ | -- | HTTPS
1563− | Https -- ^ HTTPS
2050+ Https
1564− deriving (Eq, Ord, Show, Data, Typeable, Generic)
2051+ deriving (Eq, Ord, Show, Data, Generic, TH.Lift)
1565−
1566−----------------------------------------------------------------------------
1567−-- Constants
1568−
1569−-- | Max length of preview fragment of response body.
1570−
1571−bodyPreviewLength :: Num a => a
1572−bodyPreviewLength = 1024
README.md+56−156
@@ -4,188 +4,84 @@
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−[![Coverage Status](https://coveralls.io/repos/mrkkrp/req/badge.svg?branch=master&service=github)](https://coveralls.io/github/mrkkrp/req?branch=master)
9
8
10−* [Motivation and Req vs other libraries](#motivation-and-req-vs-other-libraries)
11−* [Unsolved problems](#unsolved-problems)
12* [Related packages](#related-packages)
9* [Related packages](#related-packages)
10+* [Blog posts](#blog-posts)
13* [Contribution](#contribution)
11* [Contribution](#contribution)
14* [License](#license)
12* [License](#license)
15
13
16```haskell
14```haskell
17−{-# LANGUAGE OverloadedStrings #-}
15+{-# LANGUAGE OverloadedStrings #-}
18−{-# OPTIONS_GHC -fno-warn-orphans #-}
19
16
20module Main (main) where
17module Main (main) where
21
18
22−import Control.Exception (throwIO)
19+import Control.Monad.IO.Class
23−import Network.HTTP.Req
24import Data.Aeson
20import Data.Aeson
25−
21+import Network.HTTP.Req
26−-- Just make your monad stack an instance of MonadHttp in your application
27−-- and start making requests, enjoy automatic connection sharing.
28−
29−instance MonadHttp IO where
30− handleHttpException = throwIO
31
22
32main :: IO ()
23main :: IO ()
33−main = do
24+-- You can either make your monad an instance of 'MonadHttp', or use
25+-- 'runReq' in any IO-enabled monad without defining new instances.
26+main = runReq defaultHttpConfig $ do
34− let payload = object
27+ let payload =
28+ object
35− [ "foo" .= (10 :: Int)
29+ [ "foo" .= (10 :: Int),
36− , "bar" .= (20 :: Int) ]
30+ "bar" .= (20 :: Int)
31+ ]
37− -- One function, full power and flexibility.
32+ -- One function—full power and flexibility, automatic retrying on timeouts
33+ -- and such, automatic connection sharing.
34+ r <-
35+ req
38− r <- req POST -- method
36+ POST -- method
39− (https "httpbin.org" /: "post") -- safe by construction URL
37+ (https "httpbin.org" /: "post") -- safe by construction URL
40− (ReqBodyJson payload) -- use built-in options or add your own
38+ (ReqBodyJson payload) -- use built-in options or add your own
41− jsonResponse -- specify how to interpret response
39+ jsonResponse -- specify how to interpret response
42− mempty -- query params, headers, explicit port number, etc.
40+ mempty -- query params, headers, explicit port number, etc.
43− 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 library that
44+Req is an HTTP client library that attempts to be easy-to-use, type-safe,
47−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 a 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 a 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 a
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 boilerplate down 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:
92
74
93* [`http-client`](https://hackage.haskell.org/package/http-client)—low level
75* [`http-client`](https://hackage.haskell.org/package/http-client)—low level
94 HTTP client used everywhere in Haskell.
76 HTTP client used everywhere in Haskell.
95−
96* [`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
97 (HTTPS) support for `http-client`.
78 (HTTPS) support for `http-client`.
98
79
99−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
100whole 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
101for 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
102only difference is the API.
83only difference is the API.
103
84
104−## Motivation and Req vs other libraries
105−
106−*This section is my opinion and it contains criticisms of other well-known
107−libraries. If you're user/fan of one of these libraries, please remember not
108−to react aggressively and respect the fact that I may have different views
109−on API design from yours.*
110−
111−I have spent time to write the library because sending HTTP requests is such
112−a common thing and still there is no high-level library for that in Haskell
113−that I could use with pleasure. I'll explain why.
114−
115−First of all there is `http-client` and `http-client-tls`. They just work. I
116−have no issues with the libraries except that they are too low-level for my
117−taste. Indeed, even the docs say that they are low-level and “intended as a
118−base layer for more user-friendly packages”. This is exactly how I use them
119−in Req, as base level. Req is nothing but a different API to `http-client`,
120−so it only works because of the hard work put into `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, set, set instead of *being forced*
131−to declare necessary bits and *being allowed* to declare optional bits in a
132−way that their combination is certainly valid. And you parse request from a
133−string without the protection of TH that otherwise saves the day as in
134−Yesod.
135−
136−Then there is Wreq.
137−`wreq`
138−[doesn't see much development lately](https://github.com/bos/wreq/issues/93).
139−`wreq` is by itself a weird library, IMO. You have functions per method—not
140−very good, as there may be new methods, like PATCH which is not new but
141−still missing (well you have `customMethod`, but what is the point of having
142−per-method functions if you have a more general way to use any method? you
143−should be able to just insert methods in the “argument slot” of
144−`customMethod` and end up with a more general solution). Now every method
145−function has a companion that takes `Options` (like you have `get` and
146−`getWith`). Why the duplication? Where is generality and flexibility? This
147−is not all though, because you cannot really use `get` you see in the main
148−module, because you want to have connection sharing. Wreq's author does not
149−take the gift of automatic connection re-use `Manager` from `http-client`
150−provides, he invents the whole new thing of “sessions”. Only inside a
151−session your connections will be shared and re-used. However with the
152−session stuff you have yet another set of per-method functions like `get`
153−and `getWith`—these are different ones, to be used with sessions! Now if you
154−have a multi-threaded app, here is a surprise for you: you can't share
155−connections between threads as connections are shared only inside
156−`withSession` friend and “session will no longer be valid after that
157−function returns”. There are valid uses for sessions, but the point is that
158−they are just too inconvenient for common tasks.
159−
160−It's funny that one client I worked for had to have his own little wrapper
161−around `http-client` just because he could not possibly use `wreq` and
162−`http-client` and friends were too low-level. The previous paragraph is
163−extracted from a talk with a Haskell developer who works for that client. I
164−thought to myself “something is wrong with HTTP client libraries in Haskell
165−if they had to make a wrapper”.
166−
167−What else? I used `servant-client` a couple of times but amount of
168−boilerplate is too high. If you have several query parameters, and you use
169−just one of them, good luck passing lots of `Nothing`s.
170−
171−## Unsolved problems
172−
173−AWS request signing is problematic because request body can be in the form
174−of an action to execute (and all that “popper” stuff for streaming), not
175−just a `ByteString` and so getting its digest (hash) is not trivial without
176−running the action and consuming body in its entirety before the request in
177−made. In Wreq the author chose to just use `error` when body is not a
178−(strict or lazy) `ByteString`. Maybe it's OK for Wreq, but I don't consider
179−this a proper solution for Req as we support full variety of body options.
180−For example what if I want to upload 1 Gb file to S3? I want to stream it in
181−constant memory but at the same time I need to calculate its hash before I
182−start streaming. One solution to the problem seems to be in taking the hash
183−explicitly (as an argument of the hypothetical `awsAuth`) and making it a
184−responsibility of the user to calculate the hash correctly. I don't like
185−this because it's not user-friendly. So the question stays open, for now
186−there is no AWS signing functionality provided out-of-the-box. The best
187−solution for talking to AWS is the `amazonka` package so far.
188−
189## Related packages
85## Related packages
190
86
191The following packages are designed to be used with Req:
87The following packages are designed to be used with Req:
@@ -196,15 +92,19 @@ The following packages are designed to be used with Req:
196If you happen to have written a package that adds new features to Req,
92If you happen to have written a package that adds new features to Req,
197please submit a PR to include it in this list.
93please submit a PR to include it in this list.
198
94
95+## Blog posts
96+
97+* [Req 1.0.0, HTTP client, and streaming](https://markkarpov.com/post/req-1.0.0-http-client-and-streaming.html)
98+
199## Contribution
99## Contribution
200
100
201Issues, bugs, and questions may be reported in [the GitHub issue tracker for
101Issues, bugs, and questions may be reported in [the GitHub issue tracker for
202this project](https://github.com/mrkkrp/req/issues).
102this project](https://github.com/mrkkrp/req/issues).
203
103
204−Pull requests are also welcome and will be reviewed quickly.
104+Pull requests are also welcome.
205
105
206## License
106## License
207
107
208−Copyright © 2016–2017 Mark Karpov
108+Copyright © 2016–present Mark Karpov
209
109
210Distributed under BSD 3 clause license.
110Distributed under BSD 3 clause license.
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+314−220
@@ -1,47 +1,37 @@
1−{-# LANGUAGE CPP #-}
1+{-# LANGUAGE DataKinds #-}
2−{-# 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 #-}
7
7
8−module Network.HTTP.ReqSpec
8+module Network.HTTP.ReqSpec (spec) where
9− ( spec )
10−where
11
9
12import Control.Exception
10import Control.Exception
13−import Control.Monad.Reader
11+import Control.Monad (forM_)
14import Control.Monad.Trans.Control
12import Control.Monad.Trans.Control
15−import Data.Aeson (Value (..), ToJSON (..), object, (.=))
13+import Data.Aeson (ToJSON (..), Value (..), object, (.=))
16−import Data.Default.Class
14+import Data.Aeson qualified as A
17−import Data.IORef
15+import Data.Aeson.KeyMap qualified as Aeson.KeyMap
18−import Data.Monoid ((<>))
16+import Data.ByteString qualified as B
17+import Data.ByteString.Lazy qualified as BL
18+import Data.Functor.Identity (runIdentity)
19+import Data.Maybe (fromJust)
19import Data.Proxy
20import Data.Proxy
20import Data.Text (Text)
21import Data.Text (Text)
22+import Data.Text qualified as T
23+import Data.Text.Encoding qualified as T
24+import Data.Text.IO qualified as TIO
25+import Network.HTTP.Client qualified as L
26+import Network.HTTP.Client.MultipartFormData qualified as LM
27+import Network.HTTP.Req hiding (req)
21−import Network.HTTP.Req
28+import Network.HTTP.Req qualified as Req
29+import Network.HTTP.Types qualified as Y
22import Test.Hspec
30import Test.Hspec
23import Test.QuickCheck
31import Test.QuickCheck
24−import qualified Control.Retry as R
25−import qualified Data.Aeson as A
26−import qualified Data.ByteString as B
27−import qualified Data.ByteString.Lazy as BL
28−import qualified Data.HashMap.Strict as HM
29−import qualified Data.Text as T
30−import qualified Data.Text.Encoding as T
31−import qualified Data.Text.IO as TIO
32−import qualified Network.HTTP.Client as L
33−import qualified Network.HTTP.Client.MultipartFormData as LM
34−import qualified Network.HTTP.Types as Y
35−
36−#if !MIN_VERSION_base(4,8,0)
37−import Control.Applicative
38−import Data.Monoid (mempty)
39−import Data.Word (Word)
40−#endif
41
32
42spec :: Spec
33spec :: Spec
43spec = do
34spec = do
44−
45 describe "exception throwing on non-2xx status codes" $
35 describe "exception throwing on non-2xx status codes" $
46 it "throws indeed for non-2xx" $
36 it "throws indeed for non-2xx" $
47 req GET (httpbin /: "foo") NoReqBody ignoreResponse mempty
37 req GET (httpbin /: "foo") NoReqBody ignoreResponse mempty
@@ -49,7 +39,7 @@ spec = do
49
39
50 describe "exception throwing on non-2xx status codes (Req monad)" $
40 describe "exception throwing on non-2xx status codes (Req monad)" $
51 it "throws indeed for non-2xx" $
41 it "throws indeed for non-2xx" $
52− asIO . runReq def $
42+ asIO . runReq defaultHttpConfig $
53 liftBaseWith $ \run ->
43 liftBaseWith $ \run ->
54 run (req GET (httpbin /: "foo") NoReqBody ignoreResponse mempty)
44 run (req GET (httpbin /: "foo") NoReqBody ignoreResponse mempty)
55 `shouldThrow` selector404
45 `shouldThrow` selector404
@@ -60,41 +50,61 @@ spec = do
60 blindlyThrowing (req GET httpbin NoReqBody ignoreResponse mempty)
50 blindlyThrowing (req GET httpbin NoReqBody ignoreResponse mempty)
61 `shouldThrow` anyException
51 `shouldThrow` anyException
62
52
53+ describe "isStatusCodeException" $
54+ it "extracts non-2xx response" $
55+ req GET (httpbin /: "foo") NoReqBody ignoreResponse mempty
56+ `shouldThrow` selector404ByStatusCodeException
57+
63 describe "receiving user-agent header back" $
58 describe "receiving user-agent header back" $
64 it "works" $ do
59 it "works" $ do
60+ r <-
61+ req
62+ GET
65− r <- req GET (httpbin /: "user-agent")
63+ (httpbin /: "user-agent")
64+ NoReqBody
65+ jsonResponse
66− NoReqBody jsonResponse (header "user-agent" "Req")
66+ (header "user-agent" "Req")
67+ responseBody r
67− responseBody r `shouldBe` object
68+ `shouldBe` object
68− ["user-agent" .= ("Req" :: Text)]
69+ ["user-agent" .= ("Req" :: Text)]
69− responseStatusCode r `shouldBe` 200
70+ responseStatusCode r `shouldBe` 200
70 responseStatusMessage r `shouldBe` "OK"
71 responseStatusMessage r `shouldBe` "OK"
71
72
72 describe "receiving request headers back" $
73 describe "receiving request headers back" $
73 it "works" $ do
74 it "works" $ do
75+ r <-
76+ req
77+ GET
74− r <- req GET (httpbin /: "headers")
78+ (httpbin /: "headers")
79+ NoReqBody
80+ jsonResponse
75− NoReqBody jsonResponse (header "Foo" "bar" <> header "Baz" "quux")
81+ (header "Foo" "bar" <> header "Baz" "quux")
76− stripFunnyHeaders (responseBody r) `shouldBe` object
82+ stripFunnyHeaders (responseBody r)
83+ `shouldBe` object
77− [ "headers" .= object
84+ [ "headers"
85+ .= object
78− [ "Accept-Encoding" .= ("gzip" :: Text)
86+ [ "Accept-Encoding" .= ("gzip" :: Text),
79− , "Connection" .= ("close" :: Text)
80− , "Foo" .= ("bar" :: Text)
87+ "Foo" .= ("bar" :: Text),
81− , "Baz" .= ("quux" :: Text)
88+ "Baz" .= ("quux" :: Text)
82− , "Host" .= ("httpbin.org" :: Text) ] ]
89+ ]
90+ ]
83− responseStatusCode r `shouldBe` 200
91+ responseStatusCode r `shouldBe` 200
84 responseStatusMessage r `shouldBe` "OK"
92 responseStatusMessage r `shouldBe` "OK"
85
93
86 describe "receiving GET data back" $
94 describe "receiving GET data back" $
87 it "works" $ do
95 it "works" $ do
88 r <- req GET (httpbin /: "get") NoReqBody jsonResponse mempty
96 r <- req GET (httpbin /: "get") NoReqBody jsonResponse mempty
89− (stripFunnyHeaders . stripOrigin) (responseBody r) `shouldBe` object
97+ (stripFunnyHeaders . stripOrigin) (responseBody r)
98+ `shouldBe` object
90− [ "args" .= emptyObject
99+ [ "args" .= emptyObject,
91− , "url" .= ("https://httpbin.org/get" :: Text)
100+ "url" .= ("http://localhost:1234/get" :: Text),
92− , "headers" .= object
101+ "headers"
102+ .= object
93− [ "Accept-Encoding" .= ("gzip" :: Text)
103+ [ "Accept-Encoding" .= ("gzip" :: Text)
94− , "Connection" .= ("close" :: Text)
104+ ]
95− , "Host" .= ("httpbin.org" :: Text) ] ]
105+ ]
96 responseHeader r "Content-Type" `shouldBe` return "application/json"
106 responseHeader r "Content-Type" `shouldBe` return "application/json"
97− responseStatusCode r `shouldBe` 200
107+ responseStatusCode r `shouldBe` 200
98 responseStatusMessage r `shouldBe` "OK"
108 responseStatusMessage r `shouldBe` "OK"
99
109
100 describe "receiving POST JSON data back" $
110 describe "receiving POST JSON data back" $
@@ -102,49 +112,55 @@ spec = do
102 let text = "foo" :: Text
112 let text = "foo" :: Text
103 reflected = reflectJSON text
113 reflected = reflectJSON text
104 r <- req POST (httpbin /: "post") (ReqBodyJson text) jsonResponse mempty
114 r <- req POST (httpbin /: "post") (ReqBodyJson text) jsonResponse mempty
105− (stripFunnyHeaders . stripOrigin) (responseBody r) `shouldBe` object
115+ (stripFunnyHeaders . stripOrigin) (responseBody r)
116+ `shouldBe` object
106− [ "args" .= emptyObject
117+ [ "args" .= emptyObject,
107− , "json" .= text
118+ "json" .= text,
108− , "data" .= reflected
119+ "data" .= reflected,
109− , "url" .= ("https://httpbin.org/post" :: Text)
120+ "url" .= ("http://localhost:1234/post" :: Text),
110− , "headers" .= object
121+ "headers"
122+ .= object
111− [ "Content-Type" .= ("application/json; charset=utf-8" :: Text)
123+ [ "Content-Type" .= ("application/json; charset=utf-8" :: Text),
112− , "Accept-Encoding" .= ("gzip" :: Text)
124+ "Accept-Encoding" .= ("gzip" :: Text),
113− , "Connection" .= ("close" :: Text)
114− , "Host" .= ("httpbin.org" :: Text)
115− , "Content-Length" .= show (T.length reflected) ]
125+ "Content-Length" .= show (T.length reflected)
126+ ],
116− , "files" .= emptyObject
127+ "files" .= emptyObject,
117− , "form" .= emptyObject ]
128+ "form" .= emptyObject
129+ ]
118 responseHeader r "Content-Type" `shouldBe` return "application/json"
130 responseHeader r "Content-Type" `shouldBe` return "application/json"
119− responseStatusCode r `shouldBe` 200
131+ responseStatusCode r `shouldBe` 200
120 responseStatusMessage r `shouldBe` "OK"
132 responseStatusMessage r `shouldBe` "OK"
121
133
122 describe "receiving POST data back (multipart form data)" $
134 describe "receiving POST data back (multipart form data)" $
123 it "works" $ do
135 it "works" $ do
136+ body <-
124− body <- reqBodyMultipart
137+ reqBodyMultipart
125− [ LM.partBS "foo" "foo data!"
138+ [ LM.partBS "foo" "foo data!",
126− , LM.partBS "bar" "bar data!" ]
139+ LM.partBS "bar" "bar data!"
140+ ]
127 r <- req POST (httpbin /: "post") body jsonResponse mempty
141 r <- req POST (httpbin /: "post") body jsonResponse mempty
128− let Just contentType = getRequestContentType body
142+ let contentType = fromJust (getRequestContentType body)
129− (stripFunnyHeaders . stripOrigin) (responseBody r) `shouldBe` object
143+ (stripFunnyHeaders . stripOrigin) (responseBody r)
144+ `shouldBe` object
130− [ "args" .= emptyObject
145+ [ "args" .= emptyObject,
131− , "json" .= Null
146+ "json" .= Null,
132− , "data" .= ("" :: Text)
147+ "data" .= ("" :: Text),
133− , "url" .= ("https://httpbin.org/post" :: Text)
148+ "url" .= ("http://localhost:1234/post" :: Text),
134− , "headers" .= object
149+ "headers"
150+ .= object
135− [ "Content-Type" .= T.decodeUtf8 contentType
151+ [ "Content-Type" .= T.decodeUtf8 contentType,
136− , "Accept-Encoding" .= ("gzip" :: Text)
152+ "Accept-Encoding" .= ("gzip" :: Text),
137− , "Connection" .= ("close" :: Text)
138− , "Host" .= ("httpbin.org" :: Text)
139− , "Content-Length" .= ("242" :: Text)
153+ "Content-Length" .= ("242" :: Text)
154+ ],
155+ "files" .= emptyObject,
156+ "form"
157+ .= object
158+ [ "foo" .= ("foo data!" :: Text),
159+ "bar" .= ("bar data!" :: Text)
160+ ]
140 ]
161 ]
141− , "files" .= emptyObject
142− , "form" .= object
143− [ "foo" .= ("foo data!" :: Text)
144− , "bar" .= ("bar data!" :: Text) ]
145− ]
146 responseHeader r "Content-Type" `shouldBe` return "application/json"
162 responseHeader r "Content-Type" `shouldBe` return "application/json"
147− responseStatusCode r `shouldBe` 200
163+ responseStatusCode r `shouldBe` 200
148 responseStatusMessage r `shouldBe` "OK"
164 responseStatusMessage r `shouldBe` "OK"
149
165
150 describe "receiving PATCHed file back" $
166 describe "receiving PATCHed file back" $
@@ -153,57 +169,69 @@ spec = do
153 file = "httpbin-data/robots.txt"
169 file = "httpbin-data/robots.txt"
154 contents <- TIO.readFile file
170 contents <- TIO.readFile file
155 r <- req PATCH (httpbin /: "patch") (ReqBodyFile file) jsonResponse mempty
171 r <- req PATCH (httpbin /: "patch") (ReqBodyFile file) jsonResponse mempty
156− (stripFunnyHeaders . stripOrigin) (responseBody r) `shouldBe` object
172+ (stripFunnyHeaders . stripOrigin) (responseBody r)
173+ `shouldBe` object
157− [ "args" .= emptyObject
174+ [ "args" .= emptyObject,
158− , "json" .= Null
175+ "json" .= Null,
159− , "data" .= contents
176+ "data" .= contents,
160− , "url" .= ("https://httpbin.org/patch" :: Text)
177+ "url" .= ("http://localhost:1234/patch" :: Text),
161− , "headers" .= object
178+ "headers"
179+ .= object
162− [ "Accept-Encoding" .= ("gzip" :: Text)
180+ [ "Accept-Encoding" .= ("gzip" :: Text),
163− , "Connection" .= ("close" :: Text)
164− , "Host" .= ("httpbin.org" :: Text)
165− , "Content-Length" .= show (T.length contents) ]
181+ "Content-Length" .= show (T.length contents)
182+ ],
166− , "files" .= emptyObject
183+ "files" .= emptyObject,
167− , "form" .= emptyObject ]
184+ "form" .= emptyObject
185+ ]
168 responseHeader r "Content-Type" `shouldBe` return "application/json"
186 responseHeader r "Content-Type" `shouldBe` return "application/json"
169− responseStatusCode r `shouldBe` 200
187+ responseStatusCode r `shouldBe` 200
170 responseStatusMessage r `shouldBe` "OK"
188 responseStatusMessage r `shouldBe` "OK"
171
189
172 describe "receiving PUT form URL-encoded data back" $
190 describe "receiving PUT form URL-encoded data back" $
173 it "works" $ do
191 it "works" $ do
192+ let params =
174− let params = "foo" =: ("bar" :: Text) <>
193+ "foo" =: ("bar" :: Text)
175− "baz" =: (5 :: Int) <>
194+ <> "baz" =: (5 :: Int)
176− queryFlag "quux"
195+ <> queryFlag "quux"
177 r <- req PUT (httpbin /: "put") (ReqBodyUrlEnc params) jsonResponse mempty
196 r <- req PUT (httpbin /: "put") (ReqBodyUrlEnc params) jsonResponse mempty
178− (stripFunnyHeaders . stripOrigin) (responseBody r) `shouldBe` object
197+ (stripFunnyHeaders . stripOrigin) (responseBody r)
198+ `shouldBe` object
179− [ "args" .= emptyObject
199+ [ "args" .= emptyObject,
180− , "json" .= Null
200+ "json" .= Null,
181− , "data" .= ("" :: Text)
201+ "data" .= ("" :: Text),
182− , "url" .= ("https://httpbin.org/put" :: Text)
202+ "url" .= ("http://localhost:1234/put" :: Text),
183− , "headers" .= object
203+ "headers"
204+ .= object
184− [ "Content-Type" .= ("application/x-www-form-urlencoded" :: Text)
205+ [ "Content-Type" .= ("application/x-www-form-urlencoded" :: Text),
185− , "Accept-Encoding" .= ("gzip" :: Text)
206+ "Accept-Encoding" .= ("gzip" :: Text),
186− , "Connection" .= ("close" :: Text)
187− , "Host" .= ("httpbin.org" :: Text)
188− , "Content-Length" .= ("18" :: Text) ]
207+ "Content-Length" .= ("18" :: Text)
208+ ],
189− , "files" .= emptyObject
209+ "files" .= emptyObject,
210+ "form"
190− , "form" .= object
211+ .= object
191− [ "foo" .= ("bar" :: Text)
212+ [ "foo" .= ("bar" :: Text),
192− , "baz" .= ("5" :: Text)
213+ "baz" .= ("5" :: Text),
193− , "quux" .= ("" :: Text) ] ]
214+ "quux" .= ("" :: Text)
215+ ]
216+ ]
194 responseHeader r "Content-Type" `shouldBe` return "application/json"
217 responseHeader r "Content-Type" `shouldBe` return "application/json"
195− responseStatusCode r `shouldBe` 200
218+ responseStatusCode r `shouldBe` 200
196 responseStatusMessage r `shouldBe` "OK"
219 responseStatusMessage r `shouldBe` "OK"
197
220
198 -- TODO /delete
221 -- TODO /delete
199
222
200 describe "receiving UTF-8 encoded Unicode data" $
223 describe "receiving UTF-8 encoded Unicode data" $
201 it "works" $ do
224 it "works" $ do
225+ r <-
226+ req
227+ GET
202− r <- req GET (httpbin /: "encoding" /: "utf8")
228+ (httpbin /: "encoding" /: "utf8")
229+ NoReqBody
203− NoReqBody bsResponse mempty
230+ bsResponse
231+ mempty
204 utf8data <- B.readFile "httpbin-data/utf8.html"
232 utf8data <- B.readFile "httpbin-data/utf8.html"
205− responseBody r `shouldBe` utf8data
233+ responseBody r `shouldBe` utf8data
206− responseStatusCode r `shouldBe` 200
234+ responseStatusCode r `shouldBe` 200
207 responseStatusMessage r `shouldBe` "OK"
235 responseStatusMessage r `shouldBe` "OK"
208
236
209 -- TODO /gzip
237 -- TODO /gzip
@@ -211,27 +239,38 @@ spec = do
211
239
212 describe "retrying" $
240 describe "retrying" $
213 it "retries as many times as specified" $ do
241 it "retries as many times as specified" $ do
242+ -- FIXME We no longer can count retries because all the functions
243+ -- responsible for controlling retrying are pure now.
214 let status = 408 :: Int
244 let status = 408 :: Int
215− nref <- newIORef (0 :: Int)
245+ r <-
246+ prepareForShit $
247+ req
248+ GET
216− r <- countingRetries nref $ req GET (httpbin /: "status" /~ status)
249+ (httpbin /: "status" /~ status)
250+ NoReqBody
217− NoReqBody ignoreResponse mempty
251+ ignoreResponse
252+ mempty
218 responseStatusCode r `shouldBe` status
253 responseStatusCode r `shouldBe` status
219− readIORef nref `shouldReturn` 6 -- number of retries plus 1
220
254
221− forM_ [101..102] checkStatusCode
255+ -- forM_ [101..102] checkStatusCode
222− forM_ [200..208] checkStatusCode
256+ forM_ [200 .. 208] checkStatusCode
223 -- forM_ [300..308] checkStatusCode
257 -- forM_ [300..308] checkStatusCode
224− forM_ [400..431] checkStatusCode
258+ forM_ [400 .. 431] checkStatusCode
225− forM_ [500..511] checkStatusCode
259+ forM_ [500 .. 511] checkStatusCode
226
260
227 -- TODO /response-headers
261 -- TODO /response-headers
228 -- TODO /redirect
262 -- TODO /redirect
229
263
230 describe "redirects" $
264 describe "redirects" $
231 it "follows redirects" $ do
265 it "follows redirects" $ do
266+ r <-
267+ req
268+ GET
232− r <- req GET (httpbin /: "redirect-to") NoReqBody ignoreResponse
269+ (httpbin /: "redirect-to")
270+ NoReqBody
271+ ignoreResponse
233− ("url" =: ("https://httpbin.org" :: Text))
272+ ("url" =: ("https://httpbin.org" :: Text))
234− responseStatusCode r `shouldBe` 200
273+ responseStatusCode r `shouldBe` 200
235 responseStatusMessage r `shouldBe` "OK"
274 responseStatusMessage r `shouldBe` "OK"
236
275
237 -- TODO /relative-redicet
276 -- TODO /relative-redicet
@@ -240,21 +279,30 @@ spec = do
240
279
241 describe "basicAuth" $ do
280 describe "basicAuth" $ do
242 let user, password :: Text
281 let user, password :: Text
243− user = "Scooby"
282+ user = "Scooby"
244 password = "Doo"
283 password = "Doo"
245 context "when we do not send appropriate basic auth data" $
284 context "when we do not send appropriate basic auth data" $
246 it "fails with 401" $ do
285 it "fails with 401" $ do
286+ r <-
247− r <- prepareForShit $ req GET
287+ prepareForShit $
288+ req
289+ GET
248− (httpbin /: "basic-auth" /~ user /~ password)
290+ (httpbin /: "basic-auth" /~ user /~ password)
291+ NoReqBody
249− NoReqBody ignoreResponse mempty
292+ ignoreResponse
293+ mempty
250− responseStatusCode r `shouldBe` 401
294+ responseStatusCode r `shouldBe` 401
251 responseStatusMessage r `shouldBe` "UNAUTHORIZED"
295 responseStatusMessage r `shouldBe` "UNAUTHORIZED"
252 context "when we provide appropriate basic auth data" $
296 context "when we provide appropriate basic auth data" $
253 it "succeeds" $ do
297 it "succeeds" $ do
298+ r <-
299+ req
300+ GET
254− r <- req GET (httpbin /: "basic-auth" /~ user /~ password)
301+ (httpbin /: "basic-auth" /~ user /~ password)
302+ NoReqBody
255− NoReqBody ignoreResponse
303+ ignoreResponse
256− (basicAuth (T.encodeUtf8 user) (T.encodeUtf8 password))
304+ (basicAuthUnsafe (T.encodeUtf8 user) (T.encodeUtf8 password))
257− responseStatusCode r `shouldBe` 200
305+ responseStatusCode r `shouldBe` 200
258 responseStatusMessage r `shouldBe` "OK"
306 responseStatusMessage r `shouldBe` "OK"
259
307
260 -- TODO /hidden-basic-auth
308 -- TODO /hidden-basic-auth
@@ -269,8 +317,8 @@ spec = do
269 it "works" $ do
317 it "works" $ do
270 r <- req GET (httpbin /: "robots.txt") NoReqBody bsResponse mempty
318 r <- req GET (httpbin /: "robots.txt") NoReqBody bsResponse mempty
271 robots <- B.readFile "httpbin-data/robots.txt"
319 robots <- B.readFile "httpbin-data/robots.txt"
272− responseBody r `shouldBe` robots
320+ responseBody r `shouldBe` robots
273− responseStatusCode r `shouldBe` 200
321+ responseStatusCode r `shouldBe` 200
274 responseStatusMessage r `shouldBe` "OK"
322 responseStatusMessage r `shouldBe` "OK"
275
323
276 -- TODO /deny
324 -- TODO /deny
@@ -281,10 +329,15 @@ spec = do
281 property $ \n' -> do
329 property $ \n' -> do
282 let n :: Word
330 let n :: Word
283 n = getSmall n'
331 n = getSmall n'
332+ r <-
333+ req
334+ GET
284− r <- req GET (httpbin /: "bytes" /~ n)
335+ (httpbin /: "bytes" /~ n)
336+ NoReqBody
285− NoReqBody lbsResponse mempty
337+ lbsResponse
338+ mempty
286 responseBody r `shouldSatisfy` ((== n) . fromIntegral . BL.length)
339 responseBody r `shouldSatisfy` ((== n) . fromIntegral . BL.length)
287− responseStatusCode r `shouldBe` 200
340+ responseStatusCode r `shouldBe` 200
288 responseStatusMessage r `shouldBe` "OK"
341 responseStatusMessage r `shouldBe` "OK"
289 context "when we try to interpret 1000 random bytes as JSON" $
342 context "when we try to interpret 1000 random bytes as JSON" $
290 it "throws correct exception" $ do
343 it "throws correct exception" $ do
@@ -293,29 +346,38 @@ spec = do
293 selector _ = False
346 selector _ = False
294 n :: Int
347 n :: Int
295 n = 1000
348 n = 1000
349+ req
350+ GET
296− req GET (httpbin /: "bytes" /~ n) NoReqBody
351+ (httpbin /: "bytes" /~ n)
352+ NoReqBody
297− (Proxy :: Proxy (JsonResponse Value)) mempty
353+ (Proxy :: Proxy (JsonResponse Value))
354+ mempty
298− `shouldThrow` selector
355+ `shouldThrow` selector
299
356
300 describe "streaming random bytes" $
357 describe "streaming random bytes" $
301 it "works" $
358 it "works" $
302 property $ \n' -> do
359 property $ \n' -> do
303 let n :: Word
360 let n :: Word
304 n = getSmall n'
361 n = getSmall n'
362+ r <-
363+ req
364+ GET
305− r <- req GET (httpbin /: "stream-bytes" /~ n)
365+ (httpbin /: "stream-bytes" /~ n)
366+ NoReqBody
306− NoReqBody bsResponse mempty
367+ bsResponse
368+ mempty
307 responseBody r `shouldSatisfy` ((== n) . fromIntegral . B.length)
369 responseBody r `shouldSatisfy` ((== n) . fromIntegral . B.length)
308− responseStatusCode r `shouldBe` 200
370+ responseStatusCode r `shouldBe` 200
309 responseStatusMessage r `shouldBe` "OK"
371 responseStatusMessage r `shouldBe` "OK"
310
372
311− -- TODO /links
373+-- TODO /links
312− -- TODO /image
374+-- TODO /image
313− -- TODO /image/png
375+-- TODO /image/png
314− -- TODO /image/jpeg
376+-- TODO /image/jpeg
315− -- TODO /image/webp
377+-- TODO /image/webp
316− -- TODO /image/svg
378+-- TODO /image/svg
317− -- TODO /forms/post
379+-- TODO /forms/post
318− -- TODO /xml
380+-- TODO /xml
319
381
320----------------------------------------------------------------------------
382----------------------------------------------------------------------------
321-- Instances
383-- Instances
@@ -326,93 +388,125 @@ instance MonadHttp IO where
326----------------------------------------------------------------------------
388----------------------------------------------------------------------------
327-- Helpers
389-- Helpers
328
390
329−-- | 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.
330−-- response status codes.
331−
332prepareForShit :: Req a -> IO a
392prepareForShit :: Req a -> IO a
333−prepareForShit = runReq def { httpConfigCheckResponse = noNoise }
393+prepareForShit = runReq defaultHttpConfig {httpConfigCheckResponse = noNoise}
334 where
394 where
335− noNoise _ _ = return ()
395+ noNoise _ _ _ = Nothing
336−
337−-- | Run request with such settings that it throws on any response.
338
396
397+-- | Run a request with such settings that it throws on any response.
339blindlyThrowing :: Req a -> IO a
398blindlyThrowing :: Req a -> IO a
340−blindlyThrowing = runReq def { httpConfigCheckResponse = doit }
399+blindlyThrowing = runReq defaultHttpConfig {httpConfigCheckResponse = doit}
341 where
400 where
342 doit _ _ = error "Oops!"
401 doit _ _ = error "Oops!"
343
402
344−-- | Run request with such settings that every retry increments the given
345−-- @'IORef' 'Int'@.
346−
347−countingRetries :: IORef Int -> Req a -> IO a
348−countingRetries nref = runReq def
349− { httpConfigCheckResponse = noNoise
350− , httpConfigRetryPolicy = R.constantDelay 50000 <> R.limitRetries 5
351− , httpConfigRetryJudge = judge }
352− where
353− noNoise _ _ = return ()
354− judge _ _ = True <$ modifyIORef nref (+ 1)
355−
356-- | 'Url' representing <https://httpbin.org>.
403-- | 'Url' representing <https://httpbin.org>.
357−
358−httpbin :: Url 'Https
404+httpbin :: Url 'Http
359−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
360
426
361-- | 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
362-- to depend on that.
428-- to depend on that.
363−
364stripOrigin :: Value -> Value
429stripOrigin :: Value -> Value
365−stripOrigin (Object m) = Object (HM.delete "origin" m)
430+stripOrigin (Object m) = Object (Aeson.KeyMap.delete "origin" m)
366−stripOrigin value = value
431+stripOrigin value = value
367
432
368-- | Remove funny headers that might break the tests.
433-- | Remove funny headers that might break the tests.
369−
370stripFunnyHeaders :: Value -> Value
434stripFunnyHeaders :: Value -> Value
371−stripFunnyHeaders (Object m) =
435+stripFunnyHeaders = \case
372− 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
373− f value = value
456+ value -> value
457+ where
458+ whitelistedHeaders =
374− hs = [ "Content-Type"
459+ [ "Content-Type",
375− , "Accept-Encoding"
460+ "Accept-Encoding",
376− , "Connection"
377− , "Host"
378− , "Content-Length"
461+ "Content-Length",
379− , "Foo"
462+ "Foo",
380− , "Baz" ]
463+ "Baz"
381− in Object (HM.adjust f "headers" m)
464+ ]
382−stripFunnyHeaders value = value
383
465
384-- | 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
385-- get various response status codes.
467-- get various response status codes.
386−
387checkStatusCode :: Int -> SpecWith ()
468checkStatusCode :: Int -> SpecWith ()
388checkStatusCode code =
469checkStatusCode code =
389 describe ("receiving status code " ++ show code) $
470 describe ("receiving status code " ++ show code) $
390 it "works" $ do
471 it "works" $ do
472+ r <-
473+ prepareForShit $
474+ req
475+ GET
391− r <- prepareForShit $ req GET (httpbin /: "status" /~ code)
476+ (httpbin /: "status" /~ code)
477+ NoReqBody
392− NoReqBody ignoreResponse mempty
478+ ignoreResponse
479+ mempty
393 responseStatusCode r `shouldBe` code
480 responseStatusCode r `shouldBe` code
394
481
395-- | Exception selector that selects only 404 “Not found” exceptions.
482-- | Exception selector that selects only 404 “Not found” exceptions.
396−
397selector404 :: HttpException -> Bool
483selector404 :: HttpException -> Bool
484+selector404
398−selector404 (VanillaHttpException
485+ ( VanillaHttpException
399− (L.HttpExceptionRequest _
486+ ( L.HttpExceptionRequest
487+ _
400− (L.StatusCodeException response chunk))) =
488+ (L.StatusCodeException response chunk)
489+ )
490+ ) =
401− L.responseStatus response == Y.status404 && not (B.null chunk)
491+ L.responseStatus response == Y.status404 && not (B.null chunk)
402selector404 _ = False
492selector404 _ = False
403
493
404−-- | Empty JSON 'Object'.
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
405
500
501+-- | The empty JSON 'Object'.
406emptyObject :: Value
502emptyObject :: Value
407−emptyObject = Object HM.empty
503+emptyObject = Object Aeson.KeyMap.empty
408
504
409−-- | Get rendered JSON value as 'Text'.
505+-- | Get a rendered JSON value as 'Text'.
410−
411−reflectJSON :: ToJSON a => a -> Text
506+reflectJSON :: (ToJSON a) => a -> Text
412reflectJSON = T.decodeUtf8 . BL.toStrict . A.encode
507reflectJSON = T.decodeUtf8 . BL.toStrict . A.encode
413
508
414-- | 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'
415-- monad.
510-- monad.
416−
417asIO :: IO a -> IO a
511asIO :: IO a -> IO a
418asIO = id
512asIO = id
pure-tests/Network/HTTP/ReqSpec.hs+363−189
@@ -1,69 +1,71 @@
1−{-# LANGUAGE CPP #-}
1+{-# LANGUAGE DataKinds #-}
2−{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE DeriveGeneric #-}
2+{-# LANGUAGE DeriveGeneric #-}
4−{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE FlexibleInstances #-}
4+{-# LANGUAGE LambdaCase #-}
5−{-# LANGUAGE OverloadedStrings #-}
5+{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE QuasiQuotes #-}
6−{-# LANGUAGE RankNTypes #-}
7+{-# LANGUAGE RankNTypes #-}
7−{-# LANGUAGE RecordWildCards #-}
8+{-# LANGUAGE RecordWildCards #-}
8−{-# LANGUAGE ScopedTypeVariables #-}
9+{-# LANGUAGE ScopedTypeVariables #-}
10+{-# LANGUAGE TypeApplications #-}
11+{-# LANGUAGE NoMonomorphismRestriction #-}
9{-# OPTIONS_GHC -fno-warn-orphans #-}
12{-# OPTIONS_GHC -fno-warn-orphans #-}
10
13
11−#if __GLASGOW_HASKELL__ < 710
12−{-# LANGUAGE ConstraintKinds #-}
13−#endif
14−
15−module Network.HTTP.ReqSpec
14+module Network.HTTP.ReqSpec (spec) where
16− ( spec )
17−where
18
15
16+import Blaze.ByteString.Builder qualified as BB
19import Control.Exception (throwIO)
17import Control.Exception (throwIO)
18+import Control.Monad
19+import Control.Retry
20import Data.Aeson (ToJSON (..))
20import Data.Aeson (ToJSON (..))
21+import Data.Aeson qualified as A
21import Data.ByteString (ByteString)
22import Data.ByteString (ByteString)
22−import Data.Default.Class
23+import Data.ByteString qualified as B
24+import Data.ByteString.Char8 qualified as B8
25+import Data.ByteString.Lazy qualified as BL
26+import Data.CaseInsensitive qualified as CI
27+import Data.Either (isRight)
28+import Data.List.NonEmpty qualified as NE
23−import Data.Maybe (isNothing, fromJust)
29+import Data.Maybe (fromJust, fromMaybe, isJust, isNothing)
24−import Data.Monoid ((<>))
25import Data.Proxy
30import Data.Proxy
26import Data.Text (Text)
31import Data.Text (Text)
32+import Data.Text qualified as T
33+import Data.Text.Encoding qualified as T
27import Data.Time
34import Data.Time
35+import Data.Typeable (Typeable, eqT)
36+import GHC.Exts (IsList (..))
28import GHC.Generics
37import GHC.Generics
38+import Language.Haskell.TH qualified as TH
39+import Language.Haskell.TH.Quote qualified as TH
40+import Network.HTTP.Client qualified as L
29import Network.HTTP.Req
41import Network.HTTP.Req
42+import Network.HTTP.Types qualified as Y
43+import Network.HTTP.Types.Header qualified as Y
30import Test.Hspec
44import Test.Hspec
31import Test.Hspec.Core.Spec (SpecM)
45import Test.Hspec.Core.Spec (SpecM)
32import Test.QuickCheck
46import Test.QuickCheck
33−import qualified Blaze.ByteString.Builder as BB
47+import Text.URI (URI)
34−import qualified Data.Aeson as A
48+import Text.URI qualified as URI
35−import qualified Data.ByteString as B
49+import Text.URI.QQ qualified as QQ
36−import qualified Data.ByteString.Char8 as B8
50+import Web.FormUrlEncoded qualified as F
37−import qualified Data.ByteString.Lazy as BL
38−import qualified Data.CaseInsensitive as CI
39−import qualified Data.Text as T
40−import qualified Data.Text.Encoding as T
41−import qualified Network.HTTP.Client as L
42−import qualified Network.HTTP.Types as Y
43−
44−#if !MIN_VERSION_base(4,8,0)
45−import Control.Applicative
46−import Data.Foldable (foldMap)
47−import Data.Monoid (mempty)
48−#endif
49
51
50spec :: Spec
52spec :: Spec
51spec = do
53spec = do
52−
53 describe "config" $
54 describe "config" $
54 it "getHttpConfig has effect on resulting request" $
55 it "getHttpConfig has effect on resulting request" $
55 property $ \config -> do
56 property $ \config -> do
56 request <- runReq config (req_ GET url NoReqBody mempty)
57 request <- runReq config (req_ GET url NoReqBody mempty)
57− L.proxy request `shouldBe` httpConfigProxy config
58+ L.proxy request `shouldBe` httpConfigProxy config
58 L.redirectCount request `shouldBe` httpConfigRedirectCount config
59 L.redirectCount request `shouldBe` httpConfigRedirectCount config
59
60
60 describe "methods" $ do
61 describe "methods" $ do
61− let mnth
62+ let mnth ::
62− :: forall method.
63+ forall method.
63− ( HttpMethod method
64+ ( HttpMethod method,
64− , HttpBodyAllowed (AllowsBody method) 'NoBody )
65+ HttpBodyAllowed (AllowsBody method) 'NoBody
65− => method
66+ ) =>
67+ method ->
66− -> SpecM () ()
68+ SpecM () ()
67 mnth method = do
69 mnth method = do
68 let name = httpMethodName (Proxy :: Proxy method)
70 let name = httpMethodName (Proxy :: Proxy method)
69 describe (B8.unpack name) $
71 describe (B8.unpack name) $
@@ -86,15 +88,15 @@ spec = do
86 property $ \host -> do
88 property $ \host -> do
87 request <- req_ GET (http host) NoReqBody mempty
89 request <- req_ GET (http host) NoReqBody mempty
88 L.secure request `shouldBe` False
90 L.secure request `shouldBe` False
89− L.port request `shouldBe` 80
91+ L.port request `shouldBe` 80
90− L.host request `shouldBe` urlEncode host
92+ L.host request `shouldBe` urlEncode host
91 describe "https" $
93 describe "https" $
92 it "sets all the params correctly" $
94 it "sets all the params correctly" $
93 property $ \host -> do
95 property $ \host -> do
94 request <- req_ GET (https host) NoReqBody mempty
96 request <- req_ GET (https host) NoReqBody mempty
95 L.secure request `shouldBe` True
97 L.secure request `shouldBe` True
96− L.port request `shouldBe` 443
98+ L.port request `shouldBe` 443
97− L.host request `shouldBe` urlEncode host
99+ L.host request `shouldBe` urlEncode host
98 describe "(/~)" $
100 describe "(/~)" $
99 it "attaches a path piece that is URL-encoded" $
101 it "attaches a path piece that is URL-encoded" $
100 property $ \host pieces -> do
102 property $ \host pieces -> do
@@ -109,40 +111,95 @@ spec = do
109 request <- req_ GET url' NoReqBody mempty
111 request <- req_ GET url' NoReqBody mempty
110 L.host request `shouldBe` urlEncode host
112 L.host request `shouldBe` urlEncode host
111 L.path request `shouldBe` encodePathPieces pieces
113 L.path request `shouldBe` encodePathPieces pieces
114+
112− describe "parseUrlHttp" $ do
115+ describe "useHttpURI" $ do
113 it "does not recognize non-http schemes" $
116 it "does not recognize non-http schemes" $
117+ property $ \uri ->
118+ when (URI.uriScheme uri /= Just [QQ.scheme|http|]) $
114− parseUrlHttp "https://httpbin.org" `shouldSatisfy` isNothing
119+ useHttpURI uri `shouldSatisfy` isNothing
115− it "parses correct URLs" $
120+ it "accepts correct URLs" $
116− property $ \host mport' pieces queryParams -> do
121+ property $ \uri' -> do
117− let (url', path, queryString) =
122+ unless (isRight (URI.uriAuthority uri')) discard
118− assembleUrl Http host mport' pieces queryParams
123+ let uri = uri' {URI.uriScheme = Just [QQ.scheme|http|]}
119− (url'', options) = fromJust (parseUrlHttp url')
124+ (url', options) = fromJust (useHttpURI uri)
120− request <- req_ GET url'' NoReqBody options
125+ request <- req_ GET url' NoReqBody options
121− L.host request `shouldBe` urlEncode (unHost host)
126+ L.host request `shouldBe` uriHost uri
122− L.port request `shouldBe` maybe 80 getNonNegative mport'
127+ L.port request `shouldBe` uriPort 80 uri
123− L.path request `shouldBe` path
128+ L.path request `shouldBe` uriPath uri
124− L.queryString request `shouldBe` queryString
129+ L.queryString request `shouldBe` uriQuery uri
125− it "rejects gibberish in port component" $ do
130+ lookup "Authorization" (L.requestHeaders request)
126− parseUrlHttp "http://my-site.com:bob/far" `shouldSatisfy` isNothing
131+ `shouldBe` uriBasicAuth uri
127− parseUrlHttp "http://my-site.com:7001uh/api" `shouldSatisfy` isNothing
128− parseUrlHttp "http://my-site.com:/bar" `shouldSatisfy` isNothing
129− describe "parseUrlHttps" $ do
132+ describe "useHttpsURI" $ do
130 it "does not recognize non-https schemes" $
133 it "does not recognize non-https schemes" $
134+ property $ \uri ->
135+ when (URI.uriScheme uri /= Just [QQ.scheme|https|]) $
131− parseUrlHttps "http://httpbin.org" `shouldSatisfy` isNothing
136+ useHttpsURI uri `shouldSatisfy` isNothing
132 it "parses correct URLs" $
137 it "parses correct URLs" $
133− property $ \host mport' pieces queryParams -> do
138+ property $ \uri' -> do
134− let (url', path, queryString) =
139+ unless (isRight (URI.uriAuthority uri')) discard
135− assembleUrl Https host mport' pieces queryParams
140+ let uri = uri' {URI.uriScheme = Just [QQ.scheme|https|]}
136− (url'', options) = fromJust (parseUrlHttps url')
141+ (url', options) = fromJust (useHttpsURI uri)
137− request <- req_ GET url'' NoReqBody options
142+ request <- req_ GET url' NoReqBody options
138− L.host request `shouldBe` urlEncode (unHost host)
143+ L.host request `shouldBe` uriHost uri
139− L.port request `shouldBe` maybe 443 getNonNegative mport'
144+ L.port request `shouldBe` uriPort 443 uri
140− L.path request `shouldBe` path
145+ L.path request `shouldBe` uriPath uri
141− L.queryString request `shouldBe` queryString
146+ L.queryString request `shouldBe` uriQuery uri
142− it "rejects gibberish in port component" $ do
147+ lookup "Authorization" (L.requestHeaders request)
143− parseUrlHttp "https://my-site.com:bob/far" `shouldSatisfy` isNothing
148+ `shouldBe` uriBasicAuth uri
144− parseUrlHttp "https://my-site.com:7001uh/api" `shouldSatisfy` isNothing
149+ describe "useURI" $ do
150+ it "does not recognize non-http and non-https schemes" $
151+ property $ \uri ->
152+ when
153+ ( ( URI.uriScheme uri /= Just [QQ.scheme|http|]
154+ && (URI.uriScheme uri /= Just [QQ.scheme|https|])
155+ )
156+ )
145− parseUrlHttp "https://my-site.com:/bar" `shouldSatisfy` isNothing
157+ $ useURI uri `shouldSatisfy` isNothing
158+ it "parses correct URLs" $
159+ property $ \uri' -> do
160+ unless (isRight (URI.uriAuthority uri')) discard
161+ let uriHttp = uri' {URI.uriScheme = Just [QQ.scheme|http|]}
162+ uriHttps = uri' {URI.uriScheme = Just [QQ.scheme|https|]}
163+ requestHttp <-
164+ case fromJust (useURI uriHttp) of
165+ Left (url', options) -> req_ GET url' NoReqBody options
166+ _ -> error "(useURI uriHttp) should have returned Left"
167+ requestHttps <-
168+ case fromJust (useURI uriHttps) of
169+ Right (url', options) -> req_ GET url' NoReqBody options
170+ _ -> error "(useURI uriHttps) should have returned Right"
171+ L.host requestHttp `shouldBe` uriHost uriHttp
172+ L.host requestHttps `shouldBe` uriHost uriHttps
173+ L.port requestHttp `shouldBe` uriPort 80 uriHttp
174+ L.port requestHttps `shouldBe` uriPort 443 uriHttps
175+ L.path requestHttp `shouldBe` uriPath uriHttp
176+ L.path requestHttps `shouldBe` uriPath uriHttps
177+ L.queryString requestHttp `shouldBe` uriQuery uriHttp
178+ L.queryString requestHttps `shouldBe` uriQuery uriHttps
179+ lookup "Authorization" (L.requestHeaders requestHttp)
180+ `shouldBe` uriBasicAuth uriHttp
181+ lookup "Authorization" (L.requestHeaders requestHttps)
182+ `shouldBe` uriBasicAuth uriHttps
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"
146
203
147 describe "bodies" $ do
204 describe "bodies" $ do
148 describe "NoReqBody" $
205 describe "NoReqBody" $
@@ -166,7 +223,7 @@ spec = do
166 L.RequestBodyBS x -> x `shouldBe` bs
223 L.RequestBodyBS x -> x `shouldBe` bs
167 _ -> expectationFailure "Wrong request body constructor."
224 _ -> expectationFailure "Wrong request body constructor."
168 describe "ReqBodyLbs" $
225 describe "ReqBodyLbs" $
169− it "sets body to specified lazy byte string" $
226+ it "sets body to specified lazy byte string" $
170 property $ \lbs -> do
227 property $ \lbs -> do
171 request <- req_ POST url (ReqBodyLbs lbs) mempty
228 request <- req_ POST url (ReqBodyLbs lbs) mempty
172 case L.requestBody request of
229 case L.requestBody request of
@@ -180,6 +237,19 @@ spec = do
180 L.RequestBodyLBS x -> x `shouldBe` renderQuery params
237 L.RequestBodyLBS x -> x `shouldBe` renderQuery params
181 _ -> expectationFailure "Wrong request body constructor."
238 _ -> expectationFailure "Wrong request body constructor."
182
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))
252+
183 describe "optional parameters" $ do
253 describe "optional parameters" $ do
184 describe "header" $ do
254 describe "header" $ do
185 it "sets specified header value" $
255 it "sets specified header value" $
@@ -188,88 +258,136 @@ spec = do
188 lookup (CI.mk name) (L.requestHeaders request) `shouldBe` pure value
258 lookup (CI.mk name) (L.requestHeaders request) `shouldBe` pure value
189 it "left header wins" $
259 it "left header wins" $
190 property $ \name value0 value1 -> do
260 property $ \name value0 value1 -> do
261+ request <-
262+ req_
263+ GET
264+ url
191− request <- req_ GET url NoReqBody
265+ NoReqBody
192− (header name value0 <> header name value1)
266+ (header name value0 <> header name value1)
193 lookup (CI.mk name) (L.requestHeaders request) `shouldBe` pure value0
267 lookup (CI.mk name) (L.requestHeaders request) `shouldBe` pure value0
194 it "overwrites headers set by other parts of the lib" $
268 it "overwrites headers set by other parts of the lib" $
195 property $ \value -> do
269 property $ \value -> do
270+ request <-
271+ req_
272+ POST
273+ url
196− request <- req_ POST url (ReqBodyUrlEnc mempty)
274+ (ReqBodyUrlEnc mempty)
197− (header "Content-Type" value)
275+ (header "Content-Type" value)
198 lookup "Content-Type" (L.requestHeaders request) `shouldBe` pure value
276 lookup "Content-Type" (L.requestHeaders request) `shouldBe` pure value
199 describe "cookieJar" $
277 describe "cookieJar" $
200 it "cookie jar is set without modifications" $
278 it "cookie jar is set without modifications" $
201 property $ \cjar -> do
279 property $ \cjar -> do
202 request <- req_ GET url NoReqBody (cookieJar cjar)
280 request <- req_ GET url NoReqBody (cookieJar cjar)
203− L.cookieJar request `shouldBe` pure cjar
281+ L.cookieJar request `shouldSatisfy` (maybe False (L.equalCookieJar cjar))
204 describe "basicAuth" $ do
282 describe "basicAuth" $ do
205 it "sets Authorization header to correct value" $
283 it "sets Authorization header to correct value" $
206 property $ \username password -> do
284 property $ \username password -> do
207 request <- req_ GET url NoReqBody (basicAuth username password)
285 request <- req_ GET url NoReqBody (basicAuth username password)
208− lookup "Authorization" (L.requestHeaders request) `shouldBe`
286+ lookup "Authorization" (L.requestHeaders request)
209− pure (basicAuthHeader username password)
287+ `shouldBe` Just (basicAuthHeader username password)
210 it "overwrites manual setting of header" $
288 it "overwrites manual setting of header" $
211 property $ \username password value -> do
289 property $ \username password value -> do
290+ request0 <-
291+ req_
292+ GET
293+ url
212− request0 <- req_ GET url NoReqBody
294+ NoReqBody
213− (basicAuth username password <> header "Authorization" value)
295+ (basicAuth username password <> header "Authorization" value)
296+ request1 <-
297+ req_
298+ GET
299+ url
214− request1 <- req_ GET url NoReqBody
300+ NoReqBody
215− (header "Authorization" value <> basicAuth username password)
301+ (header "Authorization" value <> basicAuth username password)
216− let result = basicAuthHeader username password
302+ let result = Just (basicAuthHeader username password)
217− lookup "Authorization" (L.requestHeaders request0) `shouldBe`
303+ lookup "Authorization" (L.requestHeaders request0) `shouldBe` result
218− pure result
219− lookup "Authorization" (L.requestHeaders request1) `shouldBe`
304+ lookup "Authorization" (L.requestHeaders request1) `shouldBe` result
220− pure result
221 it "left auth option wins" $
305 it "left auth option wins" $
222 property $ \username0 password0 username1 password1 -> do
306 property $ \username0 password0 username1 password1 -> do
307+ request <-
308+ req_
309+ GET
310+ url
223− request <- req_ GET url NoReqBody
311+ NoReqBody
224− (basicAuth username0 password0 <> basicAuth username1 password1)
312+ (basicAuth username0 password0 <> basicAuth username1 password1)
225− lookup "Authorization" (L.requestHeaders request) `shouldBe`
313+ lookup "Authorization" (L.requestHeaders request)
226− pure (basicAuthHeader username0 password0)
314+ `shouldBe` Just (basicAuthHeader username0 password0)
227 describe "oAuth2Bearer" $ do
315 describe "oAuth2Bearer" $ do
228 it "sets Authorization header to correct value" $
316 it "sets Authorization header to correct value" $
229 property $ \token -> do
317 property $ \token -> do
230 request <- req_ GET url NoReqBody (oAuth2Bearer token)
318 request <- req_ GET url NoReqBody (oAuth2Bearer token)
231− lookup "Authorization" (L.requestHeaders request) `shouldBe`
319+ lookup "Authorization" (L.requestHeaders request)
232− pure ("Bearer " <> token)
320+ `shouldBe` pure ("Bearer " <> token)
233 it "overwrites manual setting of header" $
321 it "overwrites manual setting of header" $
234 property $ \token value -> do
322 property $ \token value -> do
323+ request0 <-
324+ req_
325+ GET
326+ url
235− request0 <- req_ GET url NoReqBody
327+ NoReqBody
236− (oAuth2Bearer token <> header "Authorization" value)
328+ (oAuth2Bearer token <> header "Authorization" value)
329+ request1 <-
330+ req_
331+ GET
332+ url
237− request1 <- req_ GET url NoReqBody
333+ NoReqBody
238− (header "Authorization" value <> oAuth2Bearer token)
334+ (header "Authorization" value <> oAuth2Bearer token)
239 let result = "Bearer " <> token
335 let result = "Bearer " <> token
240− lookup "Authorization" (L.requestHeaders request0) `shouldBe`
336+ lookup "Authorization" (L.requestHeaders request0)
241− pure result
337+ `shouldBe` pure result
242− lookup "Authorization" (L.requestHeaders request1) `shouldBe`
338+ lookup "Authorization" (L.requestHeaders request1)
243− pure result
339+ `shouldBe` pure result
244 it "left auth option wins" $
340 it "left auth option wins" $
245 property $ \token0 token1 -> do
341 property $ \token0 token1 -> do
342+ request <-
343+ req_
344+ GET
345+ url
246− request <- req_ GET url NoReqBody
346+ NoReqBody
247− (oAuth2Bearer token0 <> oAuth2Bearer token1)
347+ (oAuth2Bearer token0 <> oAuth2Bearer token1)
248− lookup "Authorization" (L.requestHeaders request) `shouldBe`
348+ lookup "Authorization" (L.requestHeaders request)
249− pure ("Bearer " <> token0)
349+ `shouldBe` pure ("Bearer " <> token0)
350+ describe "ProxyAuthorization" $
351+ it "sets Authorization header to correct value" $
352+ property $ \username password -> do
353+ request <- req_ GET url NoReqBody (basicProxyAuth username password)
354+ lookup "Proxy-Authorization" (L.requestHeaders request)
355+ `shouldBe` pure (basicProxyAuthHeader username password)
250 describe "oAuth2Token" $ do
356 describe "oAuth2Token" $ do
251 it "sets Authorization header to correct value" $
357 it "sets Authorization header to correct value" $
252 property $ \token -> do
358 property $ \token -> do
253 request <- req_ GET url NoReqBody (oAuth2Token token)
359 request <- req_ GET url NoReqBody (oAuth2Token token)
254− lookup "Authorization" (L.requestHeaders request) `shouldBe`
360+ lookup "Authorization" (L.requestHeaders request)
255− pure ("token " <> token)
361+ `shouldBe` pure ("token " <> token)
256 it "overwrites manual setting of header" $
362 it "overwrites manual setting of header" $
257 property $ \token value -> do
363 property $ \token value -> do
364+ request0 <-
365+ req_
366+ GET
367+ url
258− request0 <- req_ GET url NoReqBody
368+ NoReqBody
259− (oAuth2Token token <> header "Authorization" value)
369+ (oAuth2Token token <> header "Authorization" value)
370+ request1 <-
371+ req_
372+ GET
373+ url
260− request1 <- req_ GET url NoReqBody
374+ NoReqBody
261− (header "Authorization" value <> oAuth2Token token)
375+ (header "Authorization" value <> oAuth2Token token)
262 let result = "token " <> token
376 let result = "token " <> token
263− lookup "Authorization" (L.requestHeaders request0) `shouldBe`
377+ lookup "Authorization" (L.requestHeaders request0)
264− pure result
378+ `shouldBe` pure result
265− lookup "Authorization" (L.requestHeaders request1) `shouldBe`
379+ lookup "Authorization" (L.requestHeaders request1)
266− pure result
380+ `shouldBe` pure result
267 it "left auth option wins" $
381 it "left auth option wins" $
268 property $ \token0 token1 -> do
382 property $ \token0 token1 -> do
383+ request <-
384+ req_
385+ GET
386+ url
269− request <- req_ GET url NoReqBody
387+ NoReqBody
270− (oAuth2Token token0 <> oAuth2Token token1)
388+ (oAuth2Token token0 <> oAuth2Token token1)
271− lookup "Authorization" (L.requestHeaders request) `shouldBe`
389+ lookup "Authorization" (L.requestHeaders request)
272− pure ("token " <> token0)
390+ `shouldBe` pure ("token " <> token0)
273 describe "port" $
391 describe "port" $
274 it "sets port overwriting the defaults" $
392 it "sets port overwriting the defaults" $
275 property $ \n -> do
393 property $ \n -> do
@@ -289,6 +407,19 @@ spec = do
289 request <- req_ GET url NoReqBody (httpVersion major minor)
407 request <- req_ GET url NoReqBody (httpVersion major minor)
290 L.requestVersion request `shouldBe` Y.HttpVersion major minor
408 L.requestVersion request `shouldBe` Y.HttpVersion major minor
291
409
410+ describe "quasiquoter" $ do
411+ it "works for valid urls" $
412+ -- Doing it this way instead of just checking if [urlQ|...|] :: (Url
413+ -- 'Https, Option _) type checks, so we can catch if the type of scheme
414+ -- is unspecified.
415+ let testTypeOfQuoterResult ::
416+ forall a s. (Typeable a) => (a, Option s) -> Bool
417+ testTypeOfQuoterResult _ = isJust $ eqT @a @(Url 'Https)
418+ in property $ testTypeOfQuoterResult [urlQ|https://example.org/|]
419+ it "doesn't work for invalid urls" $
420+ property $
421+ TH.runQ (TH.quoteExp urlQ "not a url") `shouldThrow` anyIOException
422+
292----------------------------------------------------------------------------
423----------------------------------------------------------------------------
293-- Instances
424-- Instances
294
425
@@ -297,21 +428,27 @@ instance MonadHttp IO where
297
428
298instance Arbitrary HttpConfig where
429instance Arbitrary HttpConfig where
299 arbitrary = do
430 arbitrary = do
300− httpConfigProxy <- arbitrary
431+ httpConfigProxy <- arbitrary
301 httpConfigRedirectCount <- arbitrary
432 httpConfigRedirectCount <- arbitrary
302 let httpConfigAltManager = Nothing
433 let httpConfigAltManager = Nothing
303− httpConfigCheckResponse _ _ = return ()
434+ httpConfigCheckResponse _ _ _ = Nothing
304− httpConfigRetryPolicy = def
435+ httpConfigRetryPolicy = retryPolicyDefault
305− httpConfigRetryJudge _ _ = return False
436+ httpConfigRetryJudge _ _ = False
437+ httpConfigRetryJudgeException _ _ = False
438+ httpConfigBodyPreviewLength = 1024
306 return HttpConfig {..}
439 return HttpConfig {..}
307
440
308instance Show HttpConfig where
441instance Show HttpConfig where
309 show HttpConfig {..} =
442 show HttpConfig {..} =
310− "HttpConfig\n" ++
443+ "HttpConfig\n"
444+ ++ "{ httpConfigProxy="
311− "{ httpConfigProxy=" ++ show httpConfigProxy ++ "\n" ++
445+ ++ show httpConfigProxy
446+ ++ "\n"
447+ ++ ", httpRedirectCount="
312− ", httpRedirectCount=" ++ show httpConfigRedirectCount ++ "\n" ++
448+ ++ show httpConfigRedirectCount
449+ ++ "\n"
313− ", httpConfigAltManager=<cannot be shown>\n" ++
450+ ++ ", httpConfigAltManager=<cannot be shown>\n"
314− ", httpConfigCheckResponse=<cannot be shown>}\n"
451+ ++ ", httpConfigCheckResponse=<cannot be shown>}\n"
315
452
316instance Arbitrary L.Proxy where
453instance Arbitrary L.Proxy where
317 arbitrary = L.Proxy <$> arbitrary <*> arbitrary
454 arbitrary = L.Proxy <$> arbitrary <*> arbitrary
@@ -329,9 +466,10 @@ instance Show (Option scheme) where
329 show _ = "<cannot be shown>"
466 show _ = "<cannot be shown>"
330
467
331data Thing = Thing
468data Thing = Thing
332− { _thingBool :: Bool
469+ { _thingBool :: Bool,
333− , _thingText :: Text
470+ _thingText :: Text
471+ }
334− } deriving (Eq, Show, Generic)
472+ deriving (Eq, Show, Generic)
335
473
336instance ToJSON Thing
474instance ToJSON Thing
337
475
@@ -343,17 +481,17 @@ instance Arbitrary L.CookieJar where
343
481
344instance Arbitrary L.Cookie where
482instance Arbitrary L.Cookie where
345 arbitrary = do
483 arbitrary = do
346− cookie_name <- arbitrary
484+ cookie_name <- arbitrary
347− cookie_value <- arbitrary
485+ cookie_value <- arbitrary
348− cookie_expiry_time <- arbitrary
486+ cookie_expiry_time <- arbitrary
349− cookie_domain <- arbitrary
487+ cookie_domain <- arbitrary
350− cookie_path <- arbitrary
488+ cookie_path <- arbitrary
351 cookie_creation_time <- arbitrary
489 cookie_creation_time <- arbitrary
352 cookie_last_access_time <- arbitrary
490 cookie_last_access_time <- arbitrary
353− cookie_persistent <- arbitrary
491+ cookie_persistent <- arbitrary
354− cookie_host_only <- arbitrary
492+ cookie_host_only <- arbitrary
355− cookie_secure_only <- arbitrary
493+ cookie_secure_only <- arbitrary
356− cookie_http_only <- arbitrary
494+ cookie_http_only <- arbitrary
357 return L.Cookie {..}
495 return L.Cookie {..}
358
496
359instance Arbitrary UTCTime where
497instance Arbitrary UTCTime where
@@ -365,19 +503,20 @@ instance Arbitrary Day where
365instance Arbitrary DiffTime where
503instance Arbitrary DiffTime where
366 arbitrary = secondsToDiffTime <$> arbitrary
504 arbitrary = secondsToDiffTime <$> arbitrary
367
505
506+instance Arbitrary F.Form where
507+ arbitrary = (F.Form . fromList) <$> arbitrary
508+
368----------------------------------------------------------------------------
509----------------------------------------------------------------------------
369-- Helper types
510-- Helper types
370
511
371-- | A wrapper to generate correct hosts.
512-- | A wrapper to generate correct hosts.
372−
373−newtype Host = Host { unHost :: Text }
513+newtype Host = Host {unHost :: Text}
374 deriving (Eq, Show)
514 deriving (Eq, Show)
375
515
376instance Arbitrary Host where
516instance Arbitrary Host where
377 arbitrary = Host . T.pack <$> listOf1 (arbitrary `suchThat` (/= ':'))
517 arbitrary = Host . T.pack <$> listOf1 (arbitrary `suchThat` (/= ':'))
378
518
379-- | A wrapper to generate correct query parameters.
519-- | A wrapper to generate correct query parameters.
380−
381newtype QueryParams = QueryParams [(Text, Maybe Text)]
520newtype QueryParams = QueryParams [(Text, Maybe Text)]
382 deriving (Eq, Show)
521 deriving (Eq, Show)
383
522
@@ -390,69 +529,104 @@ instance Arbitrary QueryParams where
390-- Helpers
529-- Helpers
391
530
392-- | 'req' that just returns the prepared 'L.Request'.
531-- | 'req' that just returns the prepared 'L.Request'.
393−
394−req_
532+req_ ::
395− :: ( MonadHttp m
533+ ( MonadHttp m,
396− , HttpMethod method
534+ HttpMethod method,
397− , HttpBody body
535+ HttpBody body,
398− , HttpBodyAllowed (AllowsBody method) (ProvidesBody body) )
536+ HttpBodyAllowed (AllowsBody method) (ProvidesBody body)
537+ ) =>
399− => method -- ^ HTTP method
538+ -- | HTTP method
539+ method ->
400− -> Url scheme -- ^ 'Url' — location of resource
540+ -- | 'Url' — location of resource
541+ Url scheme ->
401− -> body -- ^ Body of the request
542+ -- | Body of the request
543+ body ->
402− -> Option scheme -- ^ Collection of optional parameters
544+ -- | Collection of optional parameters
545+ Option scheme ->
546+ -- | Vanilla request
403− -> m L.Request -- ^ Vanilla request
547+ m L.Request
404req_ method url' body options = req' method url' body options $
548req_ method url' body options = req' method url' body options $
405 \request _ -> return request
549 \request _ -> return request
406
550
407-- | A dummy 'Url'.
551-- | A dummy 'Url'.
408−
409url :: Url 'Https
552url :: Url 'Https
410url = https "httpbin.org"
553url = https "httpbin.org"
411
554
412-- | Percent encode given 'Text'.
555-- | Percent encode given 'Text'.
413−
414urlEncode :: Text -> ByteString
556urlEncode :: Text -> ByteString
415urlEncode = Y.urlEncode False . T.encodeUtf8
557urlEncode = Y.urlEncode False . T.encodeUtf8
416
558
417-- | Build URL path from given path pieces.
559-- | Build URL path from given path pieces.
418−
419encodePathPieces :: [Text] -> ByteString
560encodePathPieces :: [Text] -> ByteString
420encodePathPieces = BL.toStrict . BB.toLazyByteString . Y.encodePathSegments
561encodePathPieces = BL.toStrict . BB.toLazyByteString . Y.encodePathSegments
421
562
422−-- | Assemble a URL.
563+-- | Get host from a 'URI'. This function is not total.
423−
564+uriHost :: URI -> ByteString
424−assembleUrl
565+uriHost uri =
425− :: Scheme -- ^ Scheme
566+ fromJust $
426− -> Host -- ^ Host
567+ urlEncode . URI.unRText . URI.authHost
427− -> Maybe (NonNegative Int) -- ^ Port
568+ <$> either (const Nothing) Just (URI.uriAuthority uri)
428− -> [Text] -- ^ Path pieces
569+
429− -> QueryParams -- ^ Query parameters
570+-- | Get part from a 'URI' defaulting to the provided value.
430− -> (ByteString, ByteString, ByteString) -- ^ URL, path, query string
571+uriPort :: Int -> URI -> Int
431−assembleUrl scheme' (Host host') mport' pathPieces (QueryParams queryParams) =
572+uriPort def uri =
432− (scheme <> host <> port' <> path <> queryString, path, queryString)
573+ maybe def (fromIntegral) $
433− where
574+ either (const Nothing) Just (URI.uriAuthority uri) >>= URI.authPort
434− scheme = case scheme' of
575+
435− Http -> "http://"
576+-- | Get the path from a 'URI'.
436− Https -> "https://"
577+uriPath :: URI -> ByteString
437− host = urlEncode host'
578+uriPath uri = fromMaybe "" $ do
438− port' =
579+ (trailingSlash, xs) <- URI.uriPath uri
439− case mport' of
440− Nothing -> ""
441− Just (NonNegative x) -> ":" <> B8.pack (show x)
442− path = encodePathPieces pathPieces
580+ let pref = (encodePathPieces . fmap URI.unRText . NE.toList) xs
581+ return $ if trailingSlash then pref <> "/" else pref
582+
583+-- | Get the query string from a 'URI'.
584+uriQuery :: URI -> ByteString
585+uriQuery uri = do
586+ let liftQueryParam = \case
587+ URI.QueryFlag t -> (URI.unRText t, Nothing)
588+ URI.QueryParam k v -> (URI.unRText k, Just (URI.unRText v))
443− queryString = Y.renderQuery True (Y.queryTextToQuery queryParams)
589+ Y.renderQuery True (Y.queryTextToQuery (liftQueryParam <$> (URI.uriQuery uri)))
444−
590+
591+-- | Predict the headrs that should be set if the given 'URI' has username
592+-- and password in it.
593+uriBasicAuth :: URI -> Maybe ByteString
594+uriBasicAuth uri = do
595+ auth <- either (const Nothing) Just (URI.uriAuthority uri)
596+ URI.UserInfo {..} <- URI.authUserInfo auth
597+ let username = T.encodeUtf8 (URI.unRText uiUsername)
598+ password = maybe "" (T.encodeUtf8 . URI.unRText) uiPassword
599+ return (basicAuthHeader username password)
600+
601+-- | Render a query as lazy 'BL.ByteString'.
445renderQuery :: [(Text, Maybe Text)] -> BL.ByteString
602renderQuery :: [(Text, Maybe Text)] -> BL.ByteString
446renderQuery = BL.fromStrict . Y.renderQuery False . Y.queryTextToQuery
603renderQuery = BL.fromStrict . Y.renderQuery False . Y.queryTextToQuery
447
604
448-- | Convert collection of query parameters to 'FormUrlEncodedParam' thing.
605-- | Convert collection of query parameters to 'FormUrlEncodedParam' thing.
449−
450formUrlEnc :: [(Text, Maybe Text)] -> FormUrlEncodedParam
606formUrlEnc :: [(Text, Maybe Text)] -> FormUrlEncodedParam
451formUrlEnc = foldMap (uncurry queryParam)
607formUrlEnc = foldMap (uncurry queryParam)
452
608
453-- | Get “Authorization” basic auth header given username and password.
609-- | Get “Authorization” basic auth header given username and password.
454−
455basicAuthHeader :: ByteString -> ByteString -> ByteString
610basicAuthHeader :: ByteString -> ByteString -> ByteString
456basicAuthHeader username password =
611basicAuthHeader username password =
457 fromJust . lookup Y.hAuthorization . L.requestHeaders $
612 fromJust . lookup Y.hAuthorization . L.requestHeaders $
458 L.applyBasicAuth username password L.defaultRequest
613 L.applyBasicAuth username password L.defaultRequest
614+
615+-- | Get "Proxy-Authorization" basic proxy auth header given username and
616+-- password.
617+basicProxyAuthHeader :: ByteString -> ByteString -> ByteString
618+basicProxyAuthHeader username password =
619+ fromJust . lookup Y.hProxyAuthorization . L.requestHeaders $
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+117−97
@@ -1,107 +1,127 @@
1−name: req
2−version: 0.4.0
3−cabal-version: >= 1.18
1+cabal-version: 2.4
2+name: req
3+version: 3.13.4
4+license: BSD-3-Clause
5+license-file: LICENSE.md
6+maintainer: Mark Karpov <markkarpov92@gmail.com>
7+author: Mark Karpov <markkarpov92@gmail.com>
4−tested-with: GHC==7.8.4, GHC==7.10.3, GHC==8.0.2, GHC==8.2.1
8+tested-with: ghc ==9.6.3 ghc ==9.8.2 ghc ==9.10.1
5−license: BSD3
6−license-file: LICENSE.md
7−author: Mark Karpov <markkarpov92@gmail.com>
8−maintainer: Mark Karpov <markkarpov92@gmail.com>
9−homepage: https://github.com/mrkkrp/req
9+homepage: https://github.com/mrkkrp/req
10−bug-reports: https://github.com/mrkkrp/req/issues
10+bug-reports: https://github.com/mrkkrp/req/issues
11+synopsis: HTTP client library
12+description: HTTP client library.
11−category: Network, Web
13+category: Network, Web
12−synopsis: Easy-to-use, type-safe, expandable, high-level HTTP library
13−build-type: Simple
14+build-type: Simple
14−description: Easy-to-use, type-safe, expandable, high-level HTTP library.
15+data-files:
16+ httpbin-data/utf8.html
17+ httpbin-data/robots.txt
18+
15−extra-doc-files: CHANGELOG.md
19+extra-doc-files:
20+ CHANGELOG.md
16− , README.md
21+ README.md
17−data-files: httpbin-data/utf8.html
18− , httpbin-data/robots.txt
19
22
20source-repository head
23source-repository head
21− type: git
24+ type: git
22− location: https://github.com/mrkkrp/req.git
25+ location: https://github.com/mrkkrp/req.git
23
26
24flag dev
27flag dev
25− description: Turn on development settings.
28+ description: Turn on development settings.
26− manual: True
27− default: False
29+ default: False
30+ manual: True
28
31
29library
32library
33+ exposed-modules: Network.HTTP.Req
34+ default-language: GHC2021
30− build-depends: aeson >= 0.9 && < 1.3
35+ build-depends:
36+ aeson >=0.9 && <3,
31− , authenticate-oauth >= 1.5 && < 1.7
37+ authenticate-oauth >=1.5 && <1.8,
32− , base >= 4.7 && < 5.0
38+ base >=4.15 && <5,
33− , blaze-builder >= 0.3 && < 0.5
39+ blaze-builder >=0.3 && <0.5,
34− , bytestring >= 0.10.8 && < 0.11
40+ bytestring >=0.10.8 && <0.13,
35− , case-insensitive >= 0.2 && < 1.3
41+ case-insensitive >=0.2 && <1.3,
42+ containers >=0.5 && <0.8,
36− , connection >= 0.2.2 && < 0.3
43+ crypton-connection >=0.4.2 && <0.5,
37− , data-default-class
44+ data-default-class >=0.2.0.0,
45+ exceptions >=0.6 && <0.11,
38− , http-api-data >= 0.2 && < 0.4
46+ http-api-data >=0.2 && <0.7,
39− , http-client >= 0.5 && < 0.6
47+ http-client >=0.7.13.1 && <0.8,
40− , http-client-tls >= 0.3.2 && < 0.4
48+ http-client-tls >=0.3.2 && <0.4,
41− , http-types >= 0.8 && < 10.0
49+ http-types >=0.8 && <10,
50+ modern-uri >=0.3 && <0.4,
42− , monad-control >= 1.0 && < 1.1
51+ monad-control >=1.0 && <1.1,
43− , mtl >= 2.0 && < 3.0
52+ mtl >=2.0 && <3.0,
44− , retry >= 0.7 && < 0.8
53+ retry >=0.8 && <0.10,
54+ template-haskell >=2.19 && <2.23,
45− , text >= 0.2 && < 1.3
55+ text >=0.2 && <2.2,
46− , time >= 1.2 && < 1.9
47− , transformers >= 0.4 && < 0.6
56+ transformers >=0.5.3.0 && <0.7,
48− , transformers-base
57+ transformers-base,
49− if !impl(ghc >= 8.0)
58+ unliftio-core >=0.1.1 && <0.3
50− build-depends: semigroups == 0.18.*
59+
51− exposed-modules: Network.HTTP.Req
52− if flag(dev)
60+ if flag(dev)
53− ghc-options: -O0 -Wall -Werror
61+ ghc-options:
62+ -Wall -Werror -Wpartial-fields -Wunused-packages
63+ -Wno-unused-imports
64+
54− else
65+ else
55− ghc-options: -O2 -Wall
66+ ghc-options: -O2 -Wall
56− default-language: Haskell2010
57
67
58test-suite pure-tests
68test-suite pure-tests
69+ type: exitcode-stdio-1.0
59− main-is: Spec.hs
70+ main-is: Spec.hs
60− other-modules: Network.HTTP.ReqSpec
71+ build-tool-depends: hspec-discover:hspec-discover >=2 && <3
61− hs-source-dirs: pure-tests
72+ hs-source-dirs: pure-tests
62− type: exitcode-stdio-1.0
73+ other-modules: Network.HTTP.ReqSpec
74+ default-language: GHC2021
75+ build-depends:
63− build-depends: QuickCheck >= 2.7 && < 3.0
76+ QuickCheck >=2.7 && <3,
64− , aeson >= 0.9 && < 1.3
77+ aeson >=0.9 && <3,
65− , base >= 4.7 && < 5.0
78+ base >=4.15 && <5.0,
66− , blaze-builder >= 0.3 && < 0.5
79+ blaze-builder >=0.3 && <0.5,
67− , bytestring >= 0.10.8 && < 0.11
80+ bytestring >=0.10.8 && <0.13,
68− , case-insensitive >= 0.2 && < 1.3
81+ case-insensitive >=0.2 && <1.3,
69− , data-default-class
70− , hspec >= 2.0 && < 3.0
82+ hspec >=2.0 && <3,
71− , hspec-core >= 2.0 && < 3.0
83+ hspec-core >=2.0 && <3,
84+ http-api-data >=0.2 && <0.7,
72− , http-client >= 0.5 && < 0.6
85+ http-client >=0.7 && <0.8,
73− , http-types >= 0.8 && < 10.0
86+ http-types >=0.8 && <10,
74− , mtl >= 2.0 && < 3.0
87+ modern-uri >=0.3 && <0.4,
75− , req
88+ req,
89+ retry >=0.8 && <0.10,
90+ template-haskell >=2.19 && <2.23,
76− , text >= 0.2 && < 1.3
91+ text >=0.2 && <2.2,
77− , time >= 1.2 && < 1.9
92+ time >=1.2 && <1.15
93+
78− if flag(dev)
94+ if flag(dev)
79− ghc-options: -O0 -Wall -Werror
95+ ghc-options:
96+ -Wall -Werror -Wredundant-constraints -Wpartial-fields
97+ -Wunused-packages
98+
80− else
99+ else
81− ghc-options: -O2 -Wall
100+ ghc-options: -O2 -Wall
82− default-language: Haskell2010
83
101
84test-suite httpbin-tests
102test-suite httpbin-tests
103+ type: exitcode-stdio-1.0
85− main-is: Spec.hs
104+ main-is: Spec.hs
105+ build-tool-depends: hspec-discover:hspec-discover >=2 && <3
106+ hs-source-dirs: httpbin-tests
86− other-modules: Network.HTTP.ReqSpec
107+ other-modules: Network.HTTP.ReqSpec
87− hs-source-dirs: httpbin-tests
108+ default-language: GHC2021
88− type: exitcode-stdio-1.0
109+ build-depends:
89− build-depends: QuickCheck >= 2.7 && < 3.0
110+ QuickCheck >=2.7 && <3,
90− , aeson >= 0.9 && < 1.3
111+ aeson >=2 && <3,
91− , base >= 4.7 && < 5.0
112+ base >=4.15 && <5,
92− , bytestring >= 0.10.8 && < 0.11
113+ bytestring >=0.10.8 && <0.13,
93− , data-default-class
94− , hspec >= 2.0 && < 3.0
114+ hspec >=2.0 && <3.0,
95− , http-client >= 0.5 && < 0.6
115+ http-client >=0.7 && <0.8,
96− , http-types >= 0.8 && < 10.0
116+ http-types >=0.8 && <10,
97− , monad-control >= 1.0 && < 1.1
117+ monad-control >=1.0 && <1.1,
98− , mtl >= 2.0 && < 3.0
99− , req
118+ req,
100− , retry >= 0.7 && < 0.8
101− , text >= 0.2 && < 1.3
119+ text >=0.2 && <2.2
102− , unordered-containers >= 0.2.5 && < 0.2.9
120+
103− if flag(dev)
121+ if flag(dev)
104− ghc-options: -O0 -Wall -Werror
122+ ghc-options:
123+ -Wall -Werror -Wredundant-constraints -Wpartial-fields
124+ -Wunused-packages
125+
105− else
126+ else
106− ghc-options: -O2 -Wall
127+ buildable: False
107− default-language: Haskell2010
stack.yamldeleted−3
@@ -1,3 +0,0 @@
1−resolver: lts-8.15
2−packages:
3−- '.'