on master help sign in
Integration with retry (#22)

commit0ddc000140397be9b2bc912b35a15df6c9a43385 authorMark Karpov <markkarpov92@gmail.com> dateTue 20 Jun 2017, 13:17 (9 years ago) parentb234fd4
5 files changed+157−57
CHANGELOG.md+18 Network/HTTP/Req.hs+98−51 httpbin-tests/Network/HTTP/ReqSpec.hs+33−4 pure-tests/Network/HTTP/ReqSpec.hs+5−2 req.cabal+3
CHANGELOG.md+18
@@ -3,6 +3,24 @@
33* Made URL parsing functions `parseUrlHttp` and `parseUrlHttps` recognize
44 port numbers.
55
6+* Added `req'` function that allows to perform requests via a callback that
7+ receives pre-constructed request and manager.
8+
9+* Removed the `ReturnRequest` HTTP response implementation as it was not
10+ quite safe and was not going to work with retrying. Use `req'` instead for
11+ “pure” testing.
12+
13+* Changed the type of `httpConfigCheckResponse`, so the second argument can
14+ be any instance of `HttpResponse`.
15+
16+* Added built-in automatic retrying. See `httpConfigRetryPolicy` and
17+ `httpConfigRetryJudge` in `HttpConfig`. The default configuration retries
18+ 5 times on request timeouts.
19+
20+* Added the `makeResponseBodyPreview` method to the `HttpResponse` type
21+ class that allows to specify how to build a “preview” of response body for
22+ inclusion into exceptions.
23+
624* Improved wording in the documentation and `README.md`.
725
826## Req 0.2.0
Network/HTTP/Req.hs+98−51
@@ -96,6 +96,7 @@
9696{-# LANGUAGE GeneralizedNewtypeDeriving #-}
9797{-# LANGUAGE KindSignatures #-}
9898{-# LANGUAGE OverloadedStrings #-}
99+{-# LANGUAGE RankNTypes #-}
99100{-# LANGUAGE RecordWildCards #-}
100101{-# LANGUAGE ScopedTypeVariables #-}
101102{-# LANGUAGE TypeFamilies #-}
@@ -116,6 +117,7 @@ module Network.HTTP.Req
116117 ( -- * Making a request
117118 -- $making-a-request
118119 req
120+ , req'
119121 , withReqManager
120122 -- * Embedding requests into your monad
121123 -- $embedding-requests
@@ -191,15 +193,12 @@ module Network.HTTP.Req
191193 , bsResponse
192194 , LbsResponse
193195 , lbsResponse
194− , ReturnRequest
195− , returnRequest
196196 -- ** Inspecting a response
197197 , responseBody
198198 , responseStatusCode
199199 , responseStatusMessage
200200 , responseHeader
201201 , responseCookieJar
202− , responseRequest
203202 -- ** Defining your own interpretation
204203 -- $new-response-interpretation
205204 , HttpResponse (..)
@@ -211,9 +210,10 @@ where
211210
212211import Control.Applicative
213212import Control.Arrow (first, second)
214−import Control.Exception (Exception, try, catch, throwIO)
213+import Control.Exception (Exception, try, handle, throwIO)
215214import Control.Monad
216215import Control.Monad.IO.Class
216+import Control.Retry
217217import Data.Aeson (ToJSON (..), FromJSON (..))
218218import Data.ByteString (ByteString)
219219import Data.Data (Data)
@@ -285,10 +285,8 @@ import GHC.Exts (Constraint)
285285-- * 'jsonResponse'
286286-- * 'bsResponse' (to get a strict 'ByteString')
287287-- * 'lbsResponse' (to get a lazy 'BL.ByteString')
288−-- * 'returnRequest' (makes no request, but returns the constructed
289−-- request itself, used for testing).
290288--
291−-- Finally @options@ is a 'Monoid' that holds a composite 'Option' for all
289+-- Finally, @options@ is a 'Monoid' that holds a composite 'Option' for all
292290-- other optional settings like query parameters, headers, non-standard port
293291-- number, etc. There are quite a few things you can put there, see the
294292-- corresponding section in the documentation. If you don't need anything at
@@ -388,8 +386,7 @@ import GHC.Exts (Constraint)
388386-- > print (responseBody response :: Value)
389387
390388req
391− :: forall m method body response scheme.
392− ( MonadHttp m
389+ :: ( MonadHttp m
393390 , HttpMethod method
394391 , HttpBody body
395392 , HttpResponse response
@@ -400,8 +397,40 @@ req
400397 -> Proxy response -- ^ A hint how to interpret response
401398 -> Option scheme -- ^ Collection of optional parameters
402399 -> m response -- ^ Response
403−req method url body Proxy options = do
404− config <- getHttpConfig
400+req method url body Proxy options = req' method url body options $ \request manager -> do
401+ HttpConfig {..} <- getHttpConfig
402+ let wrappingVanilla = handle (throwIO . VanillaHttpException)
403+ wrapExc = handle (throwIO . LI.toHttpException request)
404+ (liftIO . try . wrappingVanilla . wrapExc) (do
405+ response <- retrying httpConfigRetryPolicy httpConfigRetryJudge
406+ (const $ getHttpResponse request manager)
407+ httpConfigCheckResponse request response
408+ return response)
409+ >>= either handleHttpException return
410+
411+-- | Mostly like 'req' with respect to its arguments, but accepts a callback
412+-- that allows to perform a request in arbitrary fashion.
413+--
414+-- This function /does not/ perform handling\/wrapping exceptions, checking
415+-- response, and retrying. It only prepares 'L.Request' and allows you to
416+-- use it.
417+--
418+-- @since 0.3.0
419+
420+req'
421+ :: forall m method body scheme a.
422+ ( MonadHttp m
423+ , HttpMethod method
424+ , HttpBody body
425+ , HttpBodyAllowed (AllowsBody method) (ProvidesBody body) )
426+ => method -- ^ HTTP method
427+ -> Url scheme -- ^ 'Url'—location of resource
428+ -> body -- ^ Body of the request
429+ -> Option scheme -- ^ Collection of optional parameters
430+ -> (L.Request -> L.Manager -> m a) -- ^ How to perform request
431+ -> m a
432+req' method url body options m = do
433+ config@HttpConfig {..} <- getHttpConfig
405434 manager <- liftIO (readIORef globalManager)
406435 let -- NOTE First appearance of any given header wins. This allows to
407436 -- “overwrite” headers when we construct a request by cons-ing.
@@ -419,10 +448,8 @@ req method url body Proxy options = do
419448 getRequestMod (Womb body :: Womb "body" body) <>
420449 getRequestMod url <>
421450 getRequestMod (Womb method :: Womb "method" method)
422− wrappingVanilla m = catch m (throwIO . VanillaHttpException)
423451 request <- finalizeRequest options request'
424− (liftIO . try . wrappingVanilla) (getHttpResponse request manager)
425− >>= either handleHttpException return
452+ m request manager
426453
427454-- | Global 'L.Manager' that 'req' uses. Here we just go with the default
428455-- settings, so users don't need to deal with this manager stuff at all, but
@@ -470,8 +497,6 @@ withReqManager m = liftIO (readIORef globalManager) >>= m
470497
471498class MonadIO m => MonadHttp m where
472499
473− {-# MINIMAL handleHttpException #-}
474−
475500 -- | This method describes how to deal with 'HttpException' that was
476501 -- caught by the library. One option is to re-throw it if you are OK with
477502 -- exceptions, but if you prefer working with something like
@@ -505,7 +530,7 @@ data HttpConfig = HttpConfig
505530 -- ^ Alternative 'L.Manager' to use. 'Nothing' (default value) means
506531 -- that default implicit manager will be used (that's what you want in
507532 -- 99% of cases).
508− , httpConfigCheckResponse :: L.Request -> L.Response L.BodyReader -> IO ()
533+ , httpConfigCheckResponse :: forall r. HttpResponse r => L.Request -> r -> IO ()
509534 -- ^ Function to check the response immediately after receiving the
510535 -- status and headers. This is used for throwing exceptions on
511536 -- non-success status codes by default (set to @\\_ _ -> return ()@ if
@@ -514,6 +539,18 @@ data HttpConfig = HttpConfig
514539 -- something is wrong and we need a way to short-cut execution. The
515540 -- thrown exception is caught by the library though and is available in
516541 -- 'handleHttpException'.
542+ --
543+ -- @since 0.3.0
544+ , httpConfigRetryPolicy :: RetryPolicyM IO
545+ -- ^ The retry policy to use for request retrying. By default 'def' is
546+ -- used (see 'RetryPolicyM').
547+ --
548+ -- @since 0.3.0
549+ , httpConfigRetryJudge :: forall r. HttpResponse r => RetryStatus -> r -> IO Bool
550+ -- ^ The function is used to decide whether to retry a request. 'True'
551+ -- means that the request should be retried.
552+ --
553+ -- @since 0.3.0
517554 } deriving Typeable
518555
519556instance Default HttpConfig where
@@ -522,17 +559,27 @@ instance Default HttpConfig where
522559 , httpConfigRedirectCount = 10
523560 , httpConfigAltManager = Nothing
524561 , httpConfigCheckResponse = \_ response ->
525− let Y.Status statusCode _ = L.responseStatus response in
562+ let statusCode = responseStatusCode response in
526563 unless (200 <= statusCode && statusCode < 300) $ do
527− chunk <- BL.toStrict <$> L.brReadSome (L.responseBody response) 1024
528− LI.throwHttp (L.StatusCodeException (void response) chunk) }
564+ chunk <- makeResponseBodyPreview response
565+ let vresponse = toVanillaResponse response
566+ LI.throwHttp (L.StatusCodeException (void vresponse) chunk)
567+ , httpConfigRetryPolicy = def
568+ , httpConfigRetryJudge = \_ r -> return $
569+ responseStatusCode r `elem`
570+ [ 408 -- Request timeout
571+ , 504 -- Gateway timeout
572+ , 524 -- A timeout occurred
573+ , 598 -- (Informal convention) Network read timeout error
574+ , 599 -- (Informal convention) Network connect timeout error
575+ ]
576+ }
529577
530578instance RequestComponent HttpConfig where
531579 getRequestMod HttpConfig {..} = Endo $ \x ->
532580 x { L.proxy = httpConfigProxy
533581 , L.redirectCount = httpConfigRedirectCount
534− , LI.requestManagerOverride = httpConfigAltManager
535− , LI.checkResponse = httpConfigCheckResponse }
582+ , LI.requestManagerOverride = httpConfigAltManager }
536583
537584----------------------------------------------------------------------------
538585-- Request—Method
@@ -1243,6 +1290,7 @@ instance HttpResponse IgnoreResponse where
12431290 toVanillaResponse (IgnoreResponse response) = response
12441291 getHttpResponse request manager =
12451292 IgnoreResponse <$> liftIO (L.httpNoBody request manager)
1293+ makeResponseBodyPreview _ = return "<ignored response>"
12461294
12471295-- | Use this as the fourth argument of 'req' to specify that you want it to
12481296-- return the 'IgnoreResponse' interpretation.
@@ -1255,16 +1303,23 @@ ignoreResponse = Proxy
12551303-- monad in which you use 'req' will determine what to do in the case when
12561304-- parsing fails (the 'JsonHttpException' constructor will be used).
12571305
1258−newtype JsonResponse a = JsonResponse (L.Response a)
1306+data JsonResponse a = JsonResponse (L.Response a) ByteString
12591307
12601308instance FromJSON a => HttpResponse (JsonResponse a) where
12611309 type HttpResponseBody (JsonResponse a) = a
1262− toVanillaResponse (JsonResponse response) = response
1310+ toVanillaResponse (JsonResponse response _) = response
12631311 getHttpResponse request manager = do
12641312 response <- L.httpLbs request manager
12651313 case A.eitherDecode (L.responseBody response) of
12661314 Left e -> throwIO (JsonHttpException e)
1267− Right x -> return $ JsonResponse response { L.responseBody = x }
1315+ Right x -> do
1316+ let preview
1317+ = BL.toStrict
1318+ . BL.take bodyPreviewLength
1319+ . L.responseBody
1320+ $ response
1321+ return $ JsonResponse response { L.responseBody = x } preview
1322+ makeResponseBodyPreview (JsonResponse _ preview) = return preview
12681323
12691324-- | Use this as the forth argument of 'req' to specify that you want it to
12701325-- return the 'JsonResponse' interpretation.
@@ -1283,6 +1338,7 @@ instance HttpResponse BsResponse where
12831338 L.withResponse request manager $ \response -> do
12841339 chunks <- L.brConsume (L.responseBody response)
12851340 return $ BsResponse response { L.responseBody = B.concat chunks }
1341+ makeResponseBodyPreview = return . B.take bodyPreviewLength . responseBody
12861342
12871343-- | Use this as the forth argument of 'req' to specify that you want to
12881344-- interpret response body as a strict 'ByteString'.
@@ -1300,6 +1356,7 @@ instance HttpResponse LbsResponse where
13001356 toVanillaResponse (LbsResponse response) = response
13011357 getHttpResponse request manager =
13021358 LbsResponse <$> L.httpLbs request manager
1359+ makeResponseBodyPreview = return . BL.toStrict . BL.take 1027 . responseBody
13031360
13041361-- | Use this as the forth argument of 'req' to specify that you want to
13051362-- interpret response body as a lazy 'BL.ByteString'.
@@ -1307,27 +1364,6 @@ instance HttpResponse LbsResponse where
13071364lbsResponse :: Proxy LbsResponse
13081365lbsResponse = Proxy
13091366
1310−-- | This interpretation does not result in any request at all, but you can
1311−-- use the 'responseRequest' function to extract 'L.Request' that 'req' has
1312−-- prepared. This is useful primarily for testing.
1313−--
1314−-- Note that when you use this interpretation inspecting response will
1315−-- diverge (i.e. it'll blow up with an error, don't do that).
1316−
1317−newtype ReturnRequest = ReturnRequest L.Request
1318−
1319−instance HttpResponse ReturnRequest where
1320− type HttpResponseBody ReturnRequest = ()
1321− toVanillaResponse (ReturnRequest _) = error
1322− "Network.HTTP.Req.ReturnRequest interpretation does not make requests"
1323− getHttpResponse request _ = return (ReturnRequest request)
1324−
1325−-- | Use this as the forth argument of 'req' to specify that you want it to
1326−-- just return the request it consturcted without making any requests.
1327−
1328−returnRequest :: Proxy ReturnRequest
1329−returnRequest = Proxy
1330−
13311367----------------------------------------------------------------------------
13321368-- Inspecting a response
13331369
@@ -1375,11 +1411,6 @@ responseCookieJar
13751411 -> L.CookieJar
13761412responseCookieJar = L.responseCookieJar . toVanillaResponse
13771413
1378−-- | Get the original request from 'ReturnRequest' response interpretation.
1379−
1380−responseRequest :: ReturnRequest -> L.Request
1381−responseRequest (ReturnRequest request) = request
1382−
13831414----------------------------------------------------------------------------
13841415-- Response—Defining your own interpretation
13851416
@@ -1407,6 +1438,14 @@ class HttpResponse response where
14071438
14081439 getHttpResponse :: L.Request -> L.Manager -> IO response
14091440
1441+ -- | Construct a “preview” of response body. It is recommend to limit the
1442+ -- length to 1024 bytes. This is mainly useful for inclusion of response
1443+ -- body fragments in exceptions.
1444+ --
1445+ -- @since 0.3.0
1446+
1447+ makeResponseBodyPreview :: response -> IO ByteString
1448+
14101449----------------------------------------------------------------------------
14111450-- Other
14121451
@@ -1464,3 +1503,11 @@ data Scheme
14641503 = Http -- ^ HTTP
14651504 | Https -- ^ HTTPS
14661505 deriving (Eq, Ord, Show, Data, Typeable, Generic)
1506+
1507+----------------------------------------------------------------------------
1508+-- Constants
1509+
1510+-- | Max length of preview fragment of response body.
1511+
1512+bodyPreviewLength :: Num a => a
1513+bodyPreviewLength = 1024
httpbin-tests/Network/HTTP/ReqSpec.hs+33−4
@@ -13,12 +13,14 @@ import Control.Exception (throwIO)
1313import Control.Monad.Reader
1414import Data.Aeson (Value (..), ToJSON (..), object, (.=))
1515import Data.Default.Class
16+import Data.IORef
1617import Data.Monoid ((<>))
1718import Data.Proxy
1819import Data.Text (Text)
1920import Network.HTTP.Req
2021import Test.Hspec
2122import Test.QuickCheck
23+import qualified Control.Retry as R
2224import qualified Data.Aeson as A
2325import qualified Data.ByteString as B
2426import qualified Data.ByteString.Lazy as BL
@@ -31,6 +33,7 @@ import qualified Network.HTTP.Client.MultipartFormData as LM
3133import qualified Network.HTTP.Types as Y
3234
3335#if !MIN_VERSION_base(4,8,0)
36+import Control.Applicative
3437import Data.Monoid (mempty)
3538import Data.Word (Word)
3639#endif
@@ -204,6 +207,15 @@ spec = do
204207 -- TODO /gzip
205208 -- TODO /deflate
206209
210+ describe "retrying" $
211+ it "retries as many times as specified" $ do
212+ let status = 408 :: Int
213+ nref <- newIORef (0 :: Int)
214+ r <- countingRetries nref $ req GET (httpbin /: "status" /~ status)
215+ NoReqBody ignoreResponse mempty
216+ responseStatusCode r `shouldBe` status
217+ readIORef nref `shouldReturn` 6 -- number of retries plus 1
218+
207219 forM_ [101..102] checkStatusCode
208220 forM_ [200..208] checkStatusCode
209221 -- forM_ [300..308] checkStatusCode
@@ -320,18 +332,35 @@ instance MonadHttp (ReaderT HttpConfig IO) where
320332-- response status codes.
321333
322334prepareForShit
323− :: (forall m. MonadHttp m => m a)
335+ :: ReaderT HttpConfig IO a
324336 -> IO a
325337prepareForShit m = runReaderT m def { httpConfigCheckResponse = noNoise }
326− where noNoise _ _ = return ()
338+ where
339+ noNoise _ _ = return ()
327340
328341-- | Run request with such settings that it throws on any response.
329342
330343blindlyThrowing
331− :: (forall m. MonadHttp m => m a)
344+ :: ReaderT HttpConfig IO a
332345 -> IO a
333346blindlyThrowing m = runReaderT m def { httpConfigCheckResponse = doit }
334− where doit _ _ = error "Oops!"
347+ where
348+ doit _ _ = error "Oops!"
349+
350+-- | Run request with such settings that every retry increments the given
351+-- @'IORef' 'Int'@.
352+
353+countingRetries
354+ :: IORef Int
355+ -> ReaderT HttpConfig IO a
356+ -> IO a
357+countingRetries nref m = runReaderT m def
358+ { httpConfigCheckResponse = noNoise
359+ , httpConfigRetryPolicy = R.constantDelay 50000 <> R.limitRetries 5
360+ , httpConfigRetryJudge = judge }
361+ where
362+ noNoise _ _ = return ()
363+ judge _ _ = True <$ modifyIORef nref (+ 1)
335364
336365-- | 'Url' representing <https://httpbin.org>.
337366
pure-tests/Network/HTTP/ReqSpec.hs+5−2
@@ -20,6 +20,7 @@ import Control.Exception (throwIO)
2020import Control.Monad.Reader
2121import Data.Aeson (ToJSON (..))
2222import Data.ByteString (ByteString)
23+import Data.Default.Class
2324import Data.Maybe (isNothing, fromJust)
2425import Data.Monoid ((<>))
2526import Data.Proxy
@@ -305,6 +306,8 @@ instance Arbitrary HttpConfig where
305306 httpConfigRedirectCount <- arbitrary
306307 let httpConfigAltManager = Nothing
307308 httpConfigCheckResponse _ _ = return ()
309+ httpConfigRetryPolicy = def
310+ httpConfigRetryJudge _ _ = return False
308311 return HttpConfig {..}
309312
310313instance Show HttpConfig where
@@ -403,8 +406,8 @@ req_
403406 -> body -- ^ Body of the request
404407 -> Option scheme -- ^ Collection of optional parameters
405408 -> m L.Request -- ^ Vanilla request
406−req_ method url' body options =
407− responseRequest `liftM` req method url' body returnRequest options
409+req_ method url' body options = req' method url' body options $
410+ \request _ -> return request
408411
409412-- | A dummy 'Url'.
410413
req.cabal+3
@@ -40,6 +40,7 @@ library
4040 , http-client-tls >= 0.3.2 && < 0.4
4141 , http-types >= 0.8 && < 10.0
4242 , mtl >= 2.0 && < 3.0
43+ , retry >= 0.7 && < 0.8
4344 , text >= 0.2 && < 1.3
4445 , time >= 1.2 && < 1.9
4546 , transformers >= 0.4 && < 0.6
@@ -63,6 +64,7 @@ test-suite pure-tests
6364 , blaze-builder >= 0.3 && < 0.5
6465 , bytestring >= 0.10.8 && < 0.11
6566 , case-insensitive >= 0.2 && < 1.3
67+ , data-default-class
6668 , hspec >= 2.0 && < 3.0
6769 , hspec-core >= 2.0 && < 3.0
6870 , http-client >= 0.5 && < 0.6
@@ -92,6 +94,7 @@ test-suite httpbin-tests
9294 , http-types >= 0.8 && < 10.0
9395 , mtl >= 2.0 && < 3.0
9496 , req
97+ , retry >= 0.7 && < 0.8
9598 , text >= 0.2 && < 1.3
9699 , unordered-containers >= 0.2.5 && < 0.2.9
97100 if flag(dev)