Puis-je définir max_retries pour requests.request?

192

Le module de requêtes Python est simple et élégant mais une chose me dérange. Il est possible d'obtenir une request.exception.ConnectionError avec un message comme:

Max retries exceeded with url: ...

Cela implique que les demandes peuvent tenter d'accéder aux données plusieurs fois. Mais il n'y a aucune mention de cette possibilité nulle part dans la documentation. En regardant le code source, je n'ai trouvé aucun endroit où je pourrais modifier la valeur par défaut (probablement 0).

Est-il donc possible de définir en quelque sorte le nombre maximum de tentatives pour les demandes?

Kirill Zaitsev
la source
9
Une mise à jour à ce sujet avec des demandes à 2.x? J'adorerais une implémentation de requests.get (url, max_retries = num_max_retries)).
paragbaxi
13
@paragbaxi: et encore mieux arequests.get(url, max_retries=num_max_retries, dely_between_retries=3))
WoJ
2
@WoJ J'ai pris vos exemples et en ai fait une réalité;) dans just.getet just.postdans github.com/kootenpv/just
PascalVKooten
2
Article utile sur les tentatives avec demandes: peterbe.com/plog/best-practice-with-retries-with-requests
Gokul

Réponses:

168

C'est la urllib3bibliothèque sous-jacente qui effectue la nouvelle tentative. Pour définir un nombre maximal de tentatives différent, utilisez d' autres adaptateurs de transport :

from requests.adapters import HTTPAdapter

s = requests.Session()
s.mount('http://stackoverflow.com', HTTPAdapter(max_retries=5))

L' max_retriesargument prend un entier ou un Retry()objet ; ce dernier vous donne un contrôle précis sur les types d'échecs réessayés (une valeur entière est transformée en une Retry()instance qui ne gère que les échecs de connexion; les erreurs après qu'une connexion est établie ne sont par défaut pas gérées car elles pourraient entraîner des effets secondaires) .


Ancienne réponse, antérieure à la publication des demandes 1.2.1 :

La requestsbibliothèque ne rend pas vraiment cela configurable, ni n'en a l'intention (voir cette pull request ). Actuellement (demandes 1.1), le nombre de tentatives est défini sur 0. Si vous voulez vraiment le définir sur une valeur plus élevée, vous devrez le définir globalement:

import requests

requests.adapters.DEFAULT_RETRIES = 5

Cette constante n'est pas documentée; utilisez-le à vos risques et périls car les futures versions pourraient changer la façon dont cela est géré.

Mise à jour : et cela a changé; dans la version 1.2.1, l'option pour définir le max_retriesparamètre sur la HTTPAdapter()classe a été ajoutée, de sorte que vous devez maintenant utiliser des adaptateurs de transport alternatifs, voir ci-dessus. L'approche monkey-patch ne fonctionne plus, à moins que vous ne corrigiez également les HTTPAdapter.__init__()valeurs par défaut (ce n'est vraiment pas recommandé).

Martijn Pieters
la source
9
Vous n'êtes pas obligé de le spécifier pour chaque site si cela n'est pas nécessaire. Vous pouvez simplement faire session.mount('http://', HTTPAdapter(max_retries=10))cela fonctionnera pour toutes les connexions http. La même chose avec https fonctionnera alors pour toutes les connexions https.
user136036
1
@ user136036: oui, les adaptateurs sont recherchés par la plus longue correspondance de préfixe; si vous voulez que cela s'applique à toutes les URL http://et que ce https://sont les préfixes minimaux à utiliser, consultez la documentation vers laquelle la réponse renvoie.
Martijn Pieters
1
Notez que HTTPAdapter(max_retries=5)cela ne fonctionnera que pour certains scénarios. À partir de la documentation des demandes , Note, this applies only to failed DNS lookups, socket connections and connection timeouts, never to requests where data has made it to the server. By default, Requests does not retry failed connections.pour forcer une nouvelle tentative pour tout code d'état, consultez la réponse de @ datashaman ci-dessous.
Steven Xu
@StevenXu: oui, vous pouvez configurer Retry()pour modifier les scénarios d'échec réessayés.
Martijn Pieters
239

Cela changera non seulement les max_retries, mais activera également une stratégie d'interruption qui fait que les requêtes à toutes les adresses http: // dorment pendant un certain temps avant de réessayer (jusqu'à un total de 5 fois):

import requests
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter

s = requests.Session()

retries = Retry(total=5,
                backoff_factor=0.1,
                status_forcelist=[ 500, 502, 503, 504 ])

s.mount('http://', HTTPAdapter(max_retries=retries))

s.get('http://httpstat.us/500')

Selon la documentation pourRetry : si le backoff_factor est 0.1 , alors sleep () dormira pendant [0.1s, 0.2s, 0.4s, ...] entre les tentatives. Cela forcera également une nouvelle tentative si le code d'état renvoyé est 500 , 502 , 503 ou 504 .

Diverses autres options pour Retrypermettre un contrôle plus granulaire:

  • total - Nombre total de tentatives à autoriser.
  • connect - Combien d'erreurs liées à la connexion à réessayer.
  • read - Combien de fois réessayer en cas d'erreurs de lecture.
  • redirect - Combien de redirections à effectuer.
  • method_whitelist - Ensemble de verbes de méthode HTTP en majuscules sur lesquels nous devons réessayer.
  • status_forcelist - Un ensemble de codes d'état HTTP sur lesquels nous devrions forcer une nouvelle tentative.
  • backoff_factor - Un facteur d' interruption à appliquer entre les tentatives.
  • rise_on_redirect - Indique si, si le nombre de redirections est épuisé, pour lever un MaxRetryError, ou pour renvoyer une réponse avec un code de réponse dans la plage 3xx .
  • rise_on_status - Sens similaire à rais_on_redirect : si nous devons lever une exception ou renvoyer une réponse, si le statut tombe dans la plage status_forcelist et que les tentatives ont été épuisées.

NB : rise_on_status est relativement nouveau, et n'en a pas encore fait une version d'urllib3 ou de requêtes. L'argument de mot-clé rise_on_status semble avoir été intégré dans la bibliothèque standard tout au plus dans la version 3.6 de python.

Pour faire des requêtes réessayer sur des codes d'état HTTP spécifiques, utilisez status_forcelist . Par exemple, status_forcelist = [503] réessayera sur le code d'état 503 (service non disponible).

Par défaut, la nouvelle tentative se déclenche uniquement pour ces conditions:

  • Impossible d'obtenir une connexion depuis le pool.
  • TimeoutError
  • HTTPExceptionsoulevé (à partir de http.client dans Python 3 sinon httplib ). Cela semble être des exceptions HTTP de bas niveau, comme une URL ou un protocole mal formé.
  • SocketError
  • ProtocolError

Notez que ce sont toutes des exceptions qui empêchent la réception d'une réponse HTTP régulière. Si une réponse régulière est générée, aucune nouvelle tentative n'est effectuée. Sans utiliser le status_forcelist , même une réponse avec le statut 500 ne sera pas retentée.

Pour qu'il se comporte d'une manière plus intuitive pour travailler avec une API ou un serveur Web distant, j'utiliserais l'extrait de code ci-dessus, qui force les tentatives sur les statuts 500 , 502 , 503 et 504 , qui ne sont pas rares sur le web et (éventuellement) récupérable étant donné une période d'attente suffisamment longue.

EDITED : Importez la Retryclasse directement depuis urllib3 .

datashaman
la source
1
J'essaye d'implémenter votre logique, mais je ne sais pas si cela fonctionne parce que le journal montre juste une demande même le statut de res est 503. Comment puis-je savoir si la nouvelle tentative fonctionne? Voir le code: pastebin.com/rty4bKTw
Danilo Oliveira
1
Le code joint fonctionne comme prévu. L'astuce est le paramètre status_forcelist . Cela indique au package urllib3 de réessayer des codes d'état spécifiques. Code: pastebin.com/k2bFbH7Z
datashaman
1
urllib3 ne pense pas (et ne devrait pas) penser que le statut 503 est une exception (par défaut).
datashaman
1
@Connor non, l'adaptateur est attaché à la session.
datashaman
1
urlib3.Retry ne fait plus partie des requêtes. cela doit être importé directement.
Modification
59

Attention, la réponse de Martijn Pieters n'est pas adaptée à la version 1.2.1+. Vous ne pouvez pas le définir globalement sans patcher la bibliothèque.

Vous pouvez le faire à la place:

import requests
from requests.adapters import HTTPAdapter

s = requests.Session()
s.mount('http://www.github.com', HTTPAdapter(max_retries=5))
s.mount('https://www.github.com', HTTPAdapter(max_retries=5))
Gizmondo
la source
22
Belle solution mais notez qu'il n'y a pas de délai entre les tentatives. Si vous voulez dormir entre les tentatives, vous devrez lancer le vôtre.
nofinator
21

Après avoir lutté un peu avec certaines des réponses ici, j'ai trouvé une bibliothèque appelée backoff qui fonctionnait mieux pour ma situation. Un exemple basique:

import backoff

@backoff.on_exception(
    backoff.expo,
    requests.exceptions.RequestException,
    max_tries=5,
    giveup=lambda e: e.response is not None and e.response.status_code < 500
)
def publish(self, data):
    r = requests.post(url, timeout=10, json=data)
    r.raise_for_status()

Je recommanderais toujours de donner une chance à la fonctionnalité native de la bibliothèque, mais si vous rencontrez des problèmes ou avez besoin d'un contrôle plus large, la réduction est une option.

Brad Koch
la source
2
super bibliothèque, merci! J'avais besoin de cette fonctionnalité pour autre chose que requests, donc cela fonctionne parfaitement!
Dennis Golomazov
3

Un moyen plus propre d'obtenir un contrôle plus élevé pourrait être de regrouper les éléments de nouvelle tentative dans une fonction et de rendre cette fonction récupérable à l'aide d'un décorateur et de mettre les exceptions en liste blanche.

J'ai créé la même chose ici: http://www.praddy.in/retry-decorator-whitelisted-exceptions/

Reproduire le code dans ce lien:

def retry(exceptions, delay=0, times=2):
"""
A decorator for retrying a function call with a specified delay in case of a set of exceptions

Parameter List
-------------
:param exceptions:  A tuple of all exceptions that need to be caught for retry
                                    e.g. retry(exception_list = (Timeout, Readtimeout))
:param delay: Amount of delay (seconds) needed between successive retries.
:param times: no of times the function should be retried


"""
def outer_wrapper(function):
    @functools.wraps(function)
    def inner_wrapper(*args, **kwargs):
        final_excep = None  
        for counter in xrange(times):
            if counter > 0:
                time.sleep(delay)
            final_excep = None
            try:
                value = function(*args, **kwargs)
                return value
            except (exceptions) as e:
                final_excep = e
                pass #or log it

        if final_excep is not None:
            raise final_excep
    return inner_wrapper

return outer_wrapper

@retry(exceptions=(TimeoutError, ConnectTimeoutError), delay=0, times=3)
def call_api():
praddy
la source