Czy mogę ustawić max_retries dla requests.request?

181

Moduł zapytań w Pythonie jest prosty i elegancki, ale jedno mnie wkurza. Możliwe jest uzyskanie requests.exception.ConnectionError z komunikatem:

Max retries exceeded with url: ...

Oznacza to, że żądania mogą próbować uzyskać dostęp do danych kilka razy. Ale nigdzie w dokumentacji nie ma ani jednej wzmianki o tej możliwości. Patrząc na kod źródłowy, nie znalazłem żadnego miejsca, w którym mógłbym zmienić domyślną (prawdopodobnie 0) wartość.

Czy można w jakiś sposób ustawić maksymalną liczbę ponownych żądań?

Kirill Zaitsev
źródło
9
Wszelkie aktualizacje tego z prośbami w 2.x? Chciałbym implementację requests.get (url, max_retries = num_max_retries)).
paragbaxi
11
@paragbaxi: a jeszcze lepiejrequests.get(url, max_retries=num_max_retries, dely_between_retries=3))
WoJ
1
@WoJ Wziąłem swoje przykłady i sprawiał, że rzeczywistość;) w just.geti just.postw github.com/kootenpv/just
PascalVKooten
2
Przydatny artykuł na temat ponownych prób z prośbami
Gokul

Odpowiedzi:

161

Jest to urllib3biblioteka podstawowa , która próbuje ponownie. Aby ustawić inną maksymalną liczbę ponownych prób, użyj alternatywnych adapterów transportowych :

from requests.adapters import HTTPAdapter

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

max_retriesArgumentu zajmuje całkowitą albo Retry()obiekt ; ten ostatni zapewnia dokładną kontrolę nad tym, jakie rodzaje błędów są ponawiane (wartość całkowita jest przekształcana w Retry()instancję, która obsługuje tylko awarie połączenia; błędy po nawiązaniu połączenia nie są domyślnie obsługiwane, ponieważ mogą one prowadzić do skutków ubocznych) .


Stara odpowiedź, wcześniejsza niż wydanie żądań 1.2.1 :

requestsBiblioteka nie naprawdę to konfigurowalne, ani nie zamierza (patrz ten wniosek ciągnącego ). Obecnie (żądania 1.1) liczba ponownych prób jest ustawiona na 0. Jeśli naprawdę chcesz ustawić wyższą wartość, musisz ustawić to globalnie:

import requests

requests.adapters.DEFAULT_RETRIES = 5

Ta stała nie jest udokumentowana; używaj go na własne ryzyko, ponieważ przyszłe wydania mogą zmienić sposób, w jaki jest to obsługiwane.

Aktualizacja : i to się zmieniło; w wersji 1.2.1 dodano opcję ustawienia max_retriesparametru dla HTTPAdapter()klasy , dzięki czemu teraz musisz użyć alternatywnych adapterów transportowych, patrz wyżej. Metoda małpiej łatki już nie działa, chyba że poprawisz także HTTPAdapter.__init__()ustawienia domyślne (bardzo niezalecane).

Martijn Pieters
źródło
9
Nie musisz tego określać dla każdej witryny, jeśli nie jest to potrzebne. Możesz to zrobić, session.mount('http://', HTTPAdapter(max_retries=10))będzie działać dla wszystkich połączeń HTTP. To samo z https będzie działać dla wszystkich połączeń https.
user136036,
1
@ user136036: tak, adaptery są wyszukiwane według najdłuższego dopasowania przedrostka; jeśli chcesz, aby dotyczyło to wszystkich adresów URL http://i https://są minimalnymi prefiksami do użycia, zapoznaj się z dokumentacją, do której prowadzą łącza do odpowiedzi.
Martijn Pieters
1
Pamiętaj, że HTTPAdapter(max_retries=5)będzie działać tylko w przypadku niektórych scenariuszy. Z dokumentu żądania , 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.aby wymusić ponowienie próby dla kodów statusu, patrz odpowiedź @ datashaman poniżej.
Steven Xu,
@StevenXu: tak, możesz skonfigurować, Retry()aby zmienić, które scenariusze awarii są ponawiane.
Martijn Pieters
225

Spowoduje to nie tylko zmianę max_retries, ale także włączenie strategii wycofywania, która powoduje, że żądania do wszystkich adresów http: //uśpione przez pewien czas przed ponowną próbą (w sumie 5 razy):

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')

Zgodnie z dokumentacją dlaRetry : jeśli współczynnik_offoff wynosi 0,1 , wówczas sleep () będzie spał przez [0,1s, 0,2s, 0,4s, ...] pomiędzy ponownymi próbami. Wymusi również ponowienie próby, jeśli zwrócony kod stanu to 500 , 502 , 503 lub 504 .

Różne inne opcje Retryumożliwiające bardziej szczegółową kontrolę:

  • ogółem - całkowita liczba ponownych prób dozwolonych.
  • connect - ile błędów związanych z połączeniem należy ponowić.
  • przeczytane - ile razy należy ponowić próbę w przypadku błędów odczytu.
  • przekierowanie - Ile przekierowań wykonać.
  • method_whitelist - Zestaw czasowników metod HTTP pisanych wielkimi literami, które powinniśmy spróbować ponownie.
  • status_forcelist - zestaw kodów statusu HTTP, na które powinniśmy wymusić ponowienie.
  • backoff_factor - Współczynnik wycofania stosowany między próbami.
  • raise_on_redirect - czy, jeśli liczba przekierowań jest wyczerpana, aby podnieść MaxRetryErrorlub zwrócić odpowiedź z kodem odpowiedzi w 3xx .
  • raise_on_status - Podobne znaczenie do raise_on_redirect : czy powinniśmy zgłosić wyjątek, czy zwrócić odpowiedź, jeśli status spadnie do zakresu status_forcelist i próby zostaną wyczerpane.

Uwaga : raise_on_status jest stosunkowo nowy i nie wydał jeszcze wersji urllib3 ani żądań. raise_on_status argumentem kluczowe wydaje się, że znalazły się w standardowej biblioteki Pythona co najwyżej w wersji 3.6.

Aby ponawiać żądania dla określonych kodów stanu HTTP, użyj status_forcelist . Na przykład status_forcelist = [503] spróbuje ponownie o kodzie stanu 503 (usługa niedostępna).

Domyślnie ponowna próba jest uruchamiana tylko dla następujących warunków:

  • Nie można uzyskać połączenia z puli.
  • TimeoutError
  • HTTPExceptionpodniesiony (z http.client w Pythonie 3 jeszcze httplib ). Wydaje się, że są to wyjątki HTTP niskiego poziomu, takie jak nieprawidłowy adres URL lub protokół.
  • SocketError
  • ProtocolError

Zauważ, że są to wszystkie wyjątki, które uniemożliwiają regularną odpowiedź HTTP. Jeśli zostanie wygenerowana jakakolwiek regularna odpowiedź, ponowna próba nie zostanie wykonana. Bez użycia status_forcelist nawet odpowiedź ze statusem 500 nie będzie ponawiana.

Aby działał w sposób bardziej intuicyjny w pracy ze zdalnym interfejsem API lub serwerem WWW, użyłbym powyższego fragmentu kodu, który wymusza ponawianie prób w stanach 500 , 502 , 503 i 504 , z których wszystkie nie są rzadkie w sieć i (ewentualnie) możliwe do odzyskania, biorąc pod uwagę wystarczająco duży okres wycofania.

EDYCJA : Importuj Retryklasę bezpośrednio z urllib3 .

Datashaman
źródło
1
Próbuję zaimplementować logikę, ale nie wiem, czy to działa, ponieważ dziennik pokazuje tylko jedno żądanie, nawet status res to 503. Skąd mogę wiedzieć, czy ponowna próba działa? Zobacz kod: pastebin.com/rty4bKTw
Danilo Oliveira
1
Załączony kod działa zgodnie z oczekiwaniami. Trik jest parametrem status_forcelist . To mówi pakietowi urllib3, aby ponowił określone kody stanu. Kod: pastebin.com/k2bFbH7Z
datashaman
1
urllib3 nie uważa (i nie powinien) myśleć, że status 503 jest wyjątkiem (domyślnie).
datashaman
1
@ Łącznik nie, adapter jest dołączony do sesji.
Dataashaman
1
urlib3.Retry nie jest już częścią żądań. to musi zostać zaimportowane bezpośrednio. Sugerowana edycja
użytkownik2390183
59

Uważaj, odpowiedź Martijna Pietersa nie jest odpowiednia dla wersji 1.2.1+. Nie można ustawić go globalnie bez łatania biblioteki.

Możesz to zrobić zamiast tego:

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
źródło
22
Fajne rozwiązanie, ale pamiętaj, że nie ma opóźnienia między kolejnymi próbami. Jeśli chcesz spać między próbami, musisz rzucić własną.
nofinator
18

Po trudnych chwilach z niektórymi odpowiedziami tutaj znalazłem bibliotekę o nazwie backoff, która działała lepiej w mojej sytuacji. Podstawowy przykład:

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()

Nadal zalecałbym przetestowanie natywnej funkcjonalności biblioteki, ale jeśli napotkasz jakiekolwiek problemy lub potrzebujesz szerszej kontroli, wycofanie jest opcją.

Brad Koch
źródło
1
świetna biblioteka, dziękuję! Potrzebowałem tej funkcji do czegoś innego requests, więc działa idealnie!
Dennis Golomazov
3

Bardziej czystym sposobem na uzyskanie większej kontroli może być spakowanie elementów ponownej próby do funkcji i uczynienie tej funkcji możliwą do odzyskania za pomocą dekoratora i umieszczenie wyjątków na białej liście.

Stworzyłem to samo tutaj: http://www.praddy.in/retry-decorator-whitelisted-exceptions/

Odtwarzanie kodu w tym linku:

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
źródło