¿Puedo configurar max_retries para request.request?

182

El módulo de solicitudes de Python es simple y elegante, pero una cosa me molesta. Es posible obtener un request.exception.ConnectionError con un mensaje como:

Max retries exceeded with url: ...

Esto implica que las solicitudes pueden intentar acceder a los datos varias veces. Pero no hay una sola mención de esta posibilidad en ninguna parte de los documentos. Mirando el código fuente, no encontré ningún lugar donde pudiera alterar el valor predeterminado (presumiblemente 0).

Entonces, ¿es posible establecer de alguna manera el número máximo de reintentos para las solicitudes?

Kirill Zaitsev
fuente
9
¿Alguna actualización sobre esto con solicitudes en 2.x? Me encantaría la implementación de request.get (url, max_retries = num_max_retries).
paragbaxi
11
@paragbaxi: y aún mejor arequests.get(url, max_retries=num_max_retries, dely_between_retries=3))
WoJ
1
@WoJ Tomé tus ejemplos y lo hice realidad;) en just.gety just.posten github.com/kootenpv/just
PascalVKooten
2
Artículo útil sobre reintentos con solicitudes: peterbe.com/plog/best-practice-with-retries-with-requests
Gokul

Respuestas:

161

Es la urllib3biblioteca subyacente la que vuelve a intentarlo. Para establecer un recuento máximo de reintentos diferente, use adaptadores de transporte alternativos :

from requests.adapters import HTTPAdapter

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

El max_retriesargumento toma un entero o un Retry()objeto ; este último le brinda un control detallado sobre qué tipos de fallas se reintentan (un valor entero se convierte en una Retry()instancia que solo maneja fallas de conexión; los errores después de que se realiza una conexión no se manejan de manera predeterminada, ya que podrían provocar efectos secundarios) .


Respuesta anterior, anterior a la publicación de solicitudes 1.2.1 :

La requestsbiblioteca realmente no hace que esto sea configurable, ni tiene la intención de hacerlo (consulte esta solicitud de extracción ). Actualmente (solicitudes 1.1), el recuento de reintentos está establecido en 0. Si realmente desea establecerlo en un valor más alto, deberá configurarlo globalmente:

import requests

requests.adapters.DEFAULT_RETRIES = 5

Esta constante no está documentada; úselo bajo su propio riesgo ya que las futuras versiones podrían cambiar la forma en que se maneja esto.

Actualización : y esto hizo el cambio; en la versión 1.2.1 se agregó la opción para establecer el max_retriesparámetro en la HTTPAdapter()clase , de modo que ahora tiene que usar adaptadores de transporte alternativos, consulte más arriba. El enfoque de parche de mono ya no funciona, a menos que también parchee los HTTPAdapter.__init__()valores predeterminados (no se recomienda mucho).

Martijn Pieters
fuente
9
No tiene que especificar esto para cada sitio si no es necesario. Simplemente puede hacer session.mount('http://', HTTPAdapter(max_retries=10))esto funcionará para todas las conexiones http. Lo mismo con https funcionará para todas las conexiones https.
user136036
1
@ user136036: sí, los adaptadores se buscan por la coincidencia de prefijo más larga; si desea que esto se aplique a todas las URL http://y https://son los prefijos mínimos para usar, consulte la documentación a la que se vincula la respuesta.
Martijn Pieters
1
Tenga en cuenta que HTTPAdapter(max_retries=5)solo funcionará para cierto escenario. Desde el documento de solicitudes , 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.para forzar el reintento de cualquier código de estado, consulte la respuesta de @datashaman a continuación.
Steven Xu
@StevenXu: sí, puede configurar Retry()para alterar qué escenarios de falla se vuelven a intentar.
Martijn Pieters
226

Esto no solo cambiará max_retries sino que también habilitará una estrategia de retroceso que hace que las solicitudes a todas las direcciones http: // se suspendan durante un período de tiempo antes de volver a intentarlo (hasta un total de 5 veces):

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

De acuerdo con la documentación paraRetry : si el backoff_factor es 0.1 , sleep () dormirá durante [0.1s, 0.2s, 0.4s, ...] entre reintentos. También forzará un nuevo intento si el código de estado devuelto es 500 , 502 , 503 o 504 .

Varias otras opciones para Retrypermitir un control más granular:

  • total : número total de reintentos para permitir.
  • connect : cuántos errores relacionados con la conexión hay que volver a intentar.
  • leer : cuántas veces volver a intentar errores de lectura.
  • redirigir : cuántos redireccionamientos realizar.
  • method_whitelist - Conjunto de verbos de métodos HTTP en mayúsculas en los que deberíamos volver a intentar.
  • status_forcelist : un conjunto de códigos de estado HTTP en los que deberíamos forzar un reintento.
  • backoff_factor : un factor de retroceso para aplicar entre intentos.
  • raise_on_redirect : si, si se agota el número de redireccionamientos, para elevar MaxRetryErroro devolver una respuesta con un código de respuesta en el rango 3xx .
  • raise_on_status: significado similar a raise_on_redirect : si debemos generar una excepción o devolver una respuesta, si el estado cae en el rango status_forcelist y los intentos se han agotado.

NB : raise_on_status es relativamente nuevo y aún no se ha convertido en una versión de urllib3 o solicitudes. Elargumento de la palabra clave raise_on_status parece haber llegado a la biblioteca estándar como máximo en python versión 3.6.

Para realizar solicitudes, vuelva a intentarlo con códigos de estado HTTP específicos, use status_forcelist . Por ejemplo, status_forcelist = [503] volverá a intentar el código de estado 503 (servicio no disponible).

De forma predeterminada, el reintento solo se activa para estas condiciones:

  • No se pudo obtener una conexión desde el grupo.
  • TimeoutError
  • HTTPExceptionelevado (desde http.client en Python 3 más httplib ). Esto parece ser excepciones HTTP de bajo nivel, como URL o protocolo no formados correctamente.
  • SocketError
  • ProtocolError

Tenga en cuenta que estas son todas las excepciones que impiden recibir una respuesta HTTP normal. Si cualquiera se genera la respuesta normal, sin reintento está hecho. Sin usar el status_forcelist , incluso una respuesta con el estado 500 no se volverá a intentar.

Para que se comporte de una manera más intuitiva para trabajar con una API remota o un servidor web, usaría el fragmento de código anterior, que obliga a los reintentos en los estados 500 , 502 , 503 y 504 , todos los cuales no son infrecuentes en el web y (posiblemente) recuperable dado un período de retroceso lo suficientemente grande.

EDITADO : Importar Retryclase directamente desde urllib3 .

datashaman
fuente
1
Estoy tratando de implementar su lógica, pero no sé si está funcionando porque el registro solo muestra una solicitud, incluso el estado res es 503. ¿Cómo puedo saber si el reintento está funcionando? Vea el código: pastebin.com/rty4bKTw
Danilo Oliveira
1
El código adjunto funciona como se esperaba. El truco es el parámetro status_forcelist . Esto le dice al paquete urllib3 que vuelva a intentar códigos de estado específicos. Código: pastebin.com/k2bFbH7Z
datashaman
1
urllib3 no (y no debería) pensar que el estado 503 es una excepción (por defecto).
datashaman
1
@Connor no, el adaptador está conectado a la sesión.
datashaman
1
urlib3.Retry ya no forma parte de las solicitudes. Esto tiene que importarse directamente. Edición sugerida
usuario2390183
59

Tenga cuidado, la respuesta de Martijn Pieters no es adecuada para la versión 1.2.1+. No puede configurarlo globalmente sin parchear la biblioteca.

Puedes hacer esto en su lugar:

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
fuente
22
Buena solución, pero tenga en cuenta que no hay demora entre reintentos. Si quieres dormir entre intentos, deberás rodar el tuyo.
nofinator
18

Después de luchar un poco con algunas de las respuestas aquí, encontré una biblioteca llamada backoff que funcionaba mejor para mi situación. Un ejemplo básico:

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

Todavía recomiendo darle una oportunidad a la funcionalidad nativa de la biblioteca, pero si tiene algún problema o necesita un control más amplio, el retroceso es una opción.

Brad Koch
fuente
1
gran biblioteca, gracias! Necesitaba esta funcionalidad para algo más que requests, ¡así que funciona perfectamente!
Dennis Golomazov
3

Una forma más limpia de obtener un mayor control podría ser empaquetar los elementos de reintento en una función y hacer que esa función sea recuperable usando un decorador y poner en la lista blanca las excepciones.

He creado lo mismo aquí: http://www.praddy.in/retry-decorator-whitelisted-exceptions/

Reproduciendo el código en ese enlace:

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
fuente