Добрый день,сайт на python. И возник вопрос.
Проблема заключается в том что,
#!/usr/bin/env python3
#__author__ = 'ilya khramtsov'
import re
from os import system, mkdir, chdir, path, walk
from zipfile import ZipFile
from urllib import request
import cgi
import cgitb; cgitb.enable()
form = cgi.FieldStorage()
input_url = form.getfirst("url_album")
url_album = input_url.replace("http://vk.com/album","").split("_")
id_groups = url_album[0]
id_albums = url_album[1]
file_name = id_groups+'_'+id_albums+'.txt'
directory = id_groups+'_'+id_albums
url = str(request.urlopen('https://api.vk.com/method/photos.get?owner_id='+id_groups+'&album_id='+id_albums+'&rev=1&extended=0&count=1000').read())
search = re.compile(r'"src_big":"([^"]+)"')
findall = (re.findall(search, url))
mkdir(directory)
chdir(directory)
for elem in findall:
system('wget '+elem.replace('\', ''))
chdir('..')
zip=ZipFile(directory+'.zip',mode='w')
for root, dirs, files in walk(directory):
for file in files:
zip.write(path.join(root,file))
zip.close()
system('rm -R ./'+directory)
#####################################################
print("Content-type: text/htmln")
print("""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Скачать альбом из вконтакте</title>
<!-- Bootstrap -->
<link href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body></br></br>""")
print("""<div class="container">
<center><h1>Архив будет доступен по данной ссылке в течении 24 часов.</h1></center>
<div class="alert alert-success" role="alert">
<a href=http://download-vk.ru/tmp/"""+directory+""".zip class=btn btn-success>Скачать</a></div>""")
print(""" <script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="../js/bootstrap.min.js"></script>
</body>
</html>""")
#####################################################
Пока скрипт полностью все не скачает, и не запакует не выдает страницу. И в итоге если там больше 300 картинок, он просто выдает 504 время вышло.
Подскажите как можно победить это?
И можно ли какой нибудь прогресс сделать?
Requests — это модуль для языка Python, который используют для упрощения работы с HTTP-запросами. Он удобнее и проще встроенного Urllib настолько, что даже в документации Python рекомендовано использовать Requests.
Установка библиотеки Requests
Самый простой вариант установки сторонних пакетов в Python — использовать pip — систему управления пакетами. Обычно pip предустанавливается вместе с интерпретатором. Если его нет — можно скачать. Для этого нужно ввести в командную строку:
Linux / MacOS
python -m ensurepip --upgrade
Windows
py -m ensurepip --upgrade
Когда pip установлен, для установки модуля Requests нужно ввести команду:
pip install requests
Как настроить библиотеку Requests. Библиотека не требует дополнительной настройки — ею можно сразу же пользоваться.
Начало работы. Давайте рассмотрим пример простейшего запроса в модуле Requests:
import requests # делаем запрос на чтение страницы https://sky.pro/media/ response = requests.get('https://sky.pro/media/') print(response.ok) # проверяем успешен ли запрос? print(response.text) # выводим полученный ответ на экран
А вот как сделать то же самое, но при помощи встроенной библиотеки Urllib:
from urllib.request import urlopen # открываем запрос на чтение страницы http://sky.pro/media with urlopen('http://sky.pro/media') as response: response_status = response.status # сохраняем статус запроса в переменную html = response.read() # вычитываем ответ в переменную print(response_status == 200) # проверяем успешен ли запрос print(html.decode()) # выводим полученный ответ на экран
Модуль Requests в Python упрощает и автоматизирует многие действия, которые в стандартной библиотеке надо делать самостоятельно. Именно за это её любят и используют многие разработчики.
Давайте разберёмся, как работать с Requests, и из чего состоят HTTP-запросы.
Методы HTTP-запросов
HTTP — это протокол передачи информации в интернете. Он описывает правила и формат общения между двумя сторонами. Например, как браузеру описать запрос, а серверу — сформировать ответ. HTTP — это текстовый протокол, поэтому его может прочитать и человек.
Давайте разберем простейший запрос:
GET /media/ HTTP/1.1 Host: sky.pro
Первая строка формирует запрос: мы говорим серверу, что хотим прочитать (GET) ресурс по адресу /media/. В конце указывается версия протокола: HTTP/1.1.
Начиная со второй строки передается дополнительная информация, которая называется заголовками. Она опциональная — кроме заголовка Host. Он указывает домен, на котором находится запрашиваемый ресурс.
HTTP-ответ выглядит аналогично:
HTTP/1.1 200 OK Content-Type: text/html <тело ответа>
В первой строке указывается версия протокола и код ответа — статус, который описывает результат запроса. В следующих строках, так же, как и в запросе, перечисляются заголовки. В данном случае сервер говорит, что в ответе находится HTML-страница (Content-Type: text/html).
И в самом конце находится тело ответа: файл, HTML-страница или ничего. Браузер отрисовывает тело ответа — это уже то, что видит человек, когда загружает страницу.
Методы HTTP-запросов нужны, чтобы объяснить серверу, какое действие мы хотим совершить над ресурсом. Ресурс — это цель HTTP-запроса. Это может быть документ, фотография или просто веб-страница.
Разберем на примерах распространённые методы — в чём их суть и чем они отличаются. Важно: ниже разбираются механизмы работы каждого метода в том виде, в котором они описаны в спецификации. На практике поведение может отличаться, но такое встречается нечасто.
OPTIONS
Метод OPTIONS нужен, чтобы спросить сервер о том, какие методы поддерживает ресурс. Он редко используется напрямую, обычно вызывается браузером автоматически. Поддерживается не всеми сайтами/ресурсами. Пример:
HTTP-ответ выглядит аналогично:
import requests response = requests.options('https://httpbin.org') print(response.text) # будет пустым print(response.headers['Allow']) # 'HEAD, GET, OPTIONS'
GET
GET — самый распространённый HTTP-метод. Его используют для чтения интернет-ресурса. Браузер отправляет метод GET, когда мы открываем какой-либо сайт. Пример:
import requests response = requests.get('https://httpbin.org/get') print(response.text)
POST
Метод POST используют для отправки на сервер данных, которые передаются в теле запроса. Для этого при вызове requests.post() надо указать аргумент data, который принимает на вход словарь, список кортежей, байты или файл.
Если для передачи данных используется формат JSON, вместо data можно указать json. Это просто удобная конвенция, которая правильно формирует отправляемый запрос. Пример:
import requests data_response = requests.post('https://httpbin.org/post', data={'foo': 'bar'}) print(data_response.text) # переданные данные находятся по ключу form json_response = requests.post('https://httpbin.org/post', json={'foo': 'bar'}) print(data_response.text) # ключ form пустой, теперь данные лежат в json
HEAD
Этот метод очень похож на GET — с той лишь разницей, что HEAD возвращает пустое тело ответа. Он нужен, когда нужно посмотреть только на заголовки, не загружая ответ целиком.
Например, мы хотим иметь свежую версию PDF-файла с расписанием автобусов. Файл хранится на каком-то сайте и периодически обновляется. Вместо того, чтобы каждый раз скачивать и сверять файл вручную, можно использовать метод HEAD. Он поможет быстро проверить дату изменения файла по заголовкам ответа.
import requests response = requests.get('https://httpbin.org/head') print(response.text) # ответ будет пустым print(response.headers)
PUT
Метод PUT очень похож на POST — с той разницей, что несколько последовательных вызовов PUT должны приводить к одному и тому же результату.
POST этого не гарантирует и может привести к неожиданным результатам, например к дублированию созданной сущности.
import requests response = requests.put('https://httpbin.org/put', data={'foo': 'bar'}) print(response.text)
PATCH
PATCH аналогичен методу POST, но с двумя отличиями: он используется для частичных изменений ресурса и его нельзя использовать в HTML-формах.
В теле запроса передается набор модификаций, которые надо применить.
import requests response = requests.patch('https://httpbin.org/patch', data={'foo': 'bar'}) print(response.text)
DELETE
Метод используется для удаления ресурса. Поддерживает передачу данных, однако не требует её: тело запроса может быть пустым.
Как и PUT, последовательный вызов DELETE должен приводить к одному и тому же результату.
import requests response = requests.delete('https://httpbin.org/delete') print(response.text)
HTTP-коды состояний
Каждый ответ HTTP-запроса обязательно имеет код состояния — трехзначное число, которое как-то характеризует полученный результат. По этому коду можно понять, всё ли успешно отработало, и если произошла ошибка, то почему.
Всего выделяют пять групп кодов состояний:
1хх-коды.
К этой группе относятся информационные коды состояний. Они сообщают клиенту о промежуточном статусе запроса и не являются финальным результатом.
Их немного, и останавливаться на них мы не будем, потому что они встречаются нечасто.
2хх-коды.
Коды из этой группы означают, что запрос принят и обработан сервером без ошибок:
- 200 OK — запрос выполнен успешно. Чаще всего встречается именно это число.
- 201 Created — в результате запроса был создан новый ресурс. Как правило, этим кодом отвечают на POST- и иногда PUT-запросы.
- 202 Accepted — запрос принят, но ещё не выполнен. Используется, когда по какой-то причине сервер не может выполнить его сразу. Например, если обработку делает какой-то сторонний процесс, который выполняется раз в день.
- 204 No Content — указывает, что тело ответа пустое, но заголовки могут содержать полезную информацию. Не используется с методом HEAD, поскольку ответ на него всегда должен быть пустым.
3хх-коды.
Это группа кодов перенаправления. Это значит, что клиенту нужно сделать какое-то действие, чтобы запрос продолжил выполняться:
- 301 Moved Permanently — URL запрашиваемого ресурса изменился, новый URL содержится в ответе.
- 302 Found — аналогичен предыдущему коду. Отличие в том, что URL изменился временно. При этом статусе состояния поисковые системы не будут менять ссылку в своей поисковой выдаче на новую.
- 304 Not Modified — означает, что содержимое ресурса было закешировано, его содержимое не поменялось и запрос можно не продолжать.
4хх-коды.
Это коды ошибок, которые допустил клиент при формировании запроса:
- 400 Bad Request — запрос сформирован с ошибкой, поэтому сервер не может его обработать. Причин может быть много, но чаще всего ошибку надо искать в теле запроса.
- 401 Unauthorized — для продолжения необходимо залогиниться.
- 403 Forbidden — пользователь залогинен, но у него нет прав для доступа к ресурсу.
- 404 Not Found — всем известный код: страница не найдена. Некоторые сайты могут возвращать 404 вместо 403, чтобы скрыть информацию от неавторизованных пользователей.
- 405 Method Not Allowed — данный ресурс не поддерживает метод запроса. Например, так бывает, если разработчик хочет отправить PUT-запрос на ресурс, который его не поддерживает.
- 429 Too Many Requests — означает, что сработал защитный механизм: он ограничивает слишком частые запросы от одного пользователя. Таким образом защищаются от DDoS- или brute-force-атак.
5хх-коды.
Это ошибки, которые возникли на сервере во время выполнения запроса:
- 500 Internal Server Error — на сервере произошла неожиданная ошибка. Как правило, происходит из-за того, что в коде сервера возникает исключение.
- 502 Bad Gateway — возникает, если на сервере используется обратный прокси, который не смог достучаться до приложения.
- 503 Service Unavailable — сервер пока не готов обработать запрос. В ответе также может содержаться информация о том, когда сервис станет доступен.
- 504 Gateway Timeout — эта ошибка означает, что обратный прокси не смог получить ответ за отведенное время (обычно — 60 секунд).
Заголовки, текст ответа и файлы Cookie
Теперь рассмотрим, как работать с запросами и ответами в Requests. Чтобы увидеть результат HTTP-запроса, можно использовать один из трех способов.
Выбор способа зависит от того, какие данные мы получили. В непонятной ситуации можно использовать атрибут text, который возвращает содержимое в виде строки:
import requests response = requests.get('https://httpbin.org/get') print(response.text)
Если заранее известно, что ответ будет в формате JSON, можно использовать одноименный атрибут, который автоматически распарсит ответ и вернет его в виде словаря:
json_response = response.json() print(json_response)
Обратите внимание, как изменится вывод функции print().
Наконец, если ответом на запрос является файл, стоит использовать атрибут content, который возвращает байты:
import requests response = requests.get('https://httpbin.org/image/jpeg') print(response.content)
Попробуйте вывести на экран response.text для предыдущего запроса и сравните результат.
Заголовок — это дополнительная информация, которой обмениваются клиент и сервер. В заголовках могут содержаться: размер ответа (Content-Length), формат передаваемых данных (Content-Type) или информация о клиенте (User-Agent).
Полный список очень длинный, знать их все необязательно, а часть и вовсе подставляется автоматом. Например, модуль Requests зачастую сам проставляет Content-Type — формат передаваемых данных.
Заголовок состоит из названия и значения, которые разделяются двоеточием, поэтому удобнее всего передавать их в виде словаря. Рассмотрим на примере, как это работает:
import requests response = requests.get('https://httpbin.org/image', headers={'Accept': 'image/jpeg'}) print(response.headers)
Здесь мы передали заголовок, который указывает, в каком формате мы хотим получить изображение. Попробуйте поменять значение на image/png и посмотрите, как изменится ответ.
Так же можно посмотреть и на заголовки запроса:
print(response.request.headers)
Обратите внимание, что Requests сам подставил информацию о клиенте — User-Agent.
Cookie (куки) — это информация, которую сервер отправляет браузеру для хранения. Они позволяют зафиксировать некоторое состояние. Например, в куки может храниться информация о том, что пользователь уже залогинен. Она хранится в браузере и передается на сервер при каждом запросе, поэтому нам не нужно каждый раз проходить авторизацию заново.
Работать с куками в модуле Requests очень просто:
import requests response = requests.get('https://httpbin.org/cookies', cookies={'foo': 'bar'}) print(response.text)
Посмотреть, какие куки пришли от сервера, можно при помощи атрибута cookies объекта Response:
print(response.cookies)
Как отправлять запросы при помощи Python Requests
Рассмотрим несколько частых примеров использования модуля Requests, чтобы понять, как отправлять запросы.
Скачивание файлов
import requests response = requests.get('https://www.python.org/static/img/python-logo.png') with open('python_logo.png', 'wb') as image: image.write(response.content)
Выше описан не самый эффективный способ скачивания файлов. Если файл окажется большого размера, код выше загрузит результат целиком в оперативную память. В лучшем случае программа упадет с ошибкой, в худшем — всё намертво зависнет.
Вот как это можно исправить:
import requests response = requests.get('https://www.python.org/static/img/python-logo@2x.png', stream=True) with open('python_logo.png', 'wb') as image: for chunk in response.iter_content(chunk_size=1024): image.write(chunk)
В этом варианте мы используем параметр stream=True, который открывает соединение, но не скачивает содержимое. Затем мы задаем размер чанка — кусочка информации, который будет скачиваться за одну итерацию цикла, и делаем его равным 1 Кб (1024 байт). Модуль Requests сам закрывает соединение после прочтения последнего чанка.
Чтобы заранее узнать размер файла, можно воспользоваться методом HEAD. Эта информация передается в заголовке ‘Content-Length’ и исчисляется в байтах.
import requests head_response = requests.head('https://www.python.org/static/img/python-logo@2x.png') image_size = int(head_response.headers['Content-Length']) print('Размер загружаемого файла: {0} кб'.format(image_size / 1024))
Авторизация на сайте
Рассмотрим два способа авторизации, которые встречаются чаще всего: Basic Auth и Bearer Auth. В обоих случаях механизм очень похожий — запрос должен передать заголовок ‘Authorization’ с каким-то значением. Для Basic Auth — это логин и пароль, закодированные в base64, для Bearer — токен, который мы получили на сайте заранее.
Для базовой авторизации у модуля Requests есть очень удобный параметр auth=, который делает всю работу за нас:
import requests response = requests.get('https://httpbin.org/basic-auth/foo/bar') print(response.status_code) # 401 response = requests.get('https://httpbin.org/basic-auth/foo/bar', auth=('foo', 'bar')) print(response.status_code) # 200 print(response.request.headers[‘Authorization’]) # 'Basic Zm9vOmJhcg=='
Обратите внимание, что модуль Requests сам добавил заголовок Authorization и подставил туда закодированные логин и пароль.
Для Bearer Auth нам придется добавлять его самостоятельно:
import requests response = requests.get('https://httpbin.org/bearer') print(response.status_code) # 401 headers = {'Authorization': 'Bearer some_token'} response = requests.get('https://httpbin.org/bearer', headers=headers) print(response.status_code) # 200
У каждого API своя спецификация — вместо Bearer может быть Token или что-то другое. Поэтому важно внимательно читать документацию сервиса.
Мультискачивание
Напишем код, который умеет скачивать сразу несколько файлов. Для этого вынесем работу с модулем Requests в отдельную функцию и параметризируем место сохранения файла.
Не забывайте про сохранение файла по чанкам, чтобы крупные файлы не загружались в память целиком.
import requests def download_file(url, save_path): response = requests.get(url, stream=True) with open(save_path, 'wb') as file: for chunk in response.iter_content(chunk_size=1024): file.write(chunk) download_list = [ 'https://cdn.pixabay.com/photo/2022/04/10/19/33/house-7124141_1280.jpg', 'https://cdn.pixabay.com/photo/2022/08/05/18/50/houseplant-7367379_1280.jpg', 'https://cdn.pixabay.com/photo/2022/06/09/04/53/ride-7251713_1280.png', ] for url in download_list: save_path = url.split('/')[-1] download_file(url, save_path)
Заключение
Модуль Requests — мощный инструмент, с которым разработчик может сделать сложный HTTP-запрос всего в пару строк. У него интуитивно понятный интерфейс, поэтому он так популярен в сообществе Python.
С помощью модуля реквест можно выполнить множество функций: от авторизации на сайте до скачивания нескольких файлов одновременно.
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and
privacy statement. We’ll occasionally send you account related emails.
Already on GitHub?
Sign in
to your account
Comments
Hey guys,
Sometimes we receive a HTTP 504 response error that is not appropriately being caught. In parse_response
when you call payload = resp.json()
, it will throw a ValueError("No JSON object could be decoded")
error.
What I’m thinking of adding before payload = resp.json()
:
if resp.status_code == 504: raise error.APIConnectionError(resp.content, resp.content, resp.status_code, resp)
Thoughts? Happy to submit a PR if you think this is good.
Hey @FabioFleitas thanks for looking into this. I think this is solid way to handle this case. It’s a unique situation because when a 504 occurs, the response returned is empty, so I’m curious to see what value resp.content
has. Is it empty as well or is it the status message of Gateway Timeout
.
If you submit a PR for it, we’ll gladly merge it in!
Copy link
Contributor
Author
You’re right, it seems like the response returned is an empty string. Perhaps we could modify it to be:
if resp.status_code == 504: raise error.APIConnectionError(resp.content or 'Gateway Timeout', resp.content, resp.status_code, resp)
In the chance that resp.content
is an empty string?
Hmm… Try seeing if there’s any content in resp.reason
. That might have the more generic message that is mapped to the status code.
Copy link
Contributor
Author
Unfortunately our logs didn’t save the resp.reason
so not sure how I could go about testing to see what it says.
Copy link
Contributor
Author
Perhaps we go with error_msg = resp.content or resp.reason or 'Gateway Timeout'
if we’re unsure?
I just quickly tested it using http://httpstat.us/504 and this:
print resp.status_code if resp.status_code == 504: print resp.reason
printed this:
So I think relying on resp.reason
is a safe bet 👍
Copy link
Contributor
Author
Alright, let’s do resp.content or resp.reason
in case later down the road there is something in content? I noticed for example that http://httpstat.us/504 had «504 Gateway Timeout» as the content.
2 participants
tl;dr; I have a lot of code that does response = requests.get(...)
in various Python projects. This is nice and simple but the problem is that networks are unreliable. So it’s a good idea to wrap these network calls with retries. Here’s one such implementation.
The First Hack
import time import requests # DON'T ACTUALLY DO THIS. # THERE ARE BETTER WAYS. HANG ON! def get(url): try: return requests.get(url) except Exception: # sleep for a bit in case that helps time.sleep(1) # try again return get(url)
This, above, is a terrible solution. It might fail for sooo many reasons. For example SSL errors due to missing Python libraries. Or the URL might have a typo in it, like get('http:/www.example.com')
.
Also, perhaps it did work but the response is a 500 error from the server and you know that if you just tried again, the problem would go away.
# ALSO A TERRIBLE SOLUTION while True: response = get('http://www.example.com') if response.status_code != 500: break else: # Hope it won't 500 a little later time.sleep(1)
What we need is a solution that does this right. Both for 500 errors and for various network errors.
The Solution
Here’s what I propose:
import requests from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry def requests_retry_session( retries=3, backoff_factor=0.3, status_forcelist=(500, 502, 504), session=None, ): session = session or requests.Session() retry = Retry( total=retries, read=retries, connect=retries, backoff_factor=backoff_factor, status_forcelist=status_forcelist, ) adapter = HTTPAdapter(max_retries=retry) session.mount('http://', adapter) session.mount('https://', adapter) return session
Usage example…
response = requests_retry_session().get('https://www.peterbe.com/') print(response.status_code) s = requests.Session() s.auth = ('user', 'pass') s.headers.update({'x-test': 'true'}) response = requests_retry_session(session=s).get( 'https://www.peterbe.com' )
It’s an opinionated solution but by its existence it demonstrates how it works so you can copy and modify it.
Testing The Solution
Suppose you try to connect to a URL that will definitely never work, like this:
t0 = time.time() try: response = requests_retry_session().get( 'http://localhost:9999', ) except Exception as x: print('It failed :(', x.__class__.__name__) else: print('It eventually worked', response.status_code) finally: t1 = time.time() print('Took', t1 - t0, 'seconds')
There is no server running in :9999
here on localhost
. So the outcome of this is…
It failed :( ConnectionError Took 1.8215010166168213 seconds
Where…
1.8 = 0 + 0.6 + 1.2
The algorithm for that backoff is documented here and it says:
A backoff factor to apply between attempts after the second try (most errors are resolved immediately by a second try without a delay). urllib3 will sleep for:
{backoff factor} * (2 ^ ({number of total retries} - 1))
seconds. If the backoff_factor is 0.1, then sleep() will sleep for [0.0s, 0.2s, 0.4s, …] between retries. It will never be longer than Retry.BACKOFF_MAX.
By default, backoff is disabled (set to 0).
It does 3 retry attempts, after the first failure, with a backoff sleep escalation of: 0.6s, 1.2s.
So if the server never responds at all, after a total of ~1.8 seconds it will raise an error:
In this example, the simulation is matching the expectations (1.82 seconds) because my laptop’s DNS lookup is near instant for localhost
. If it had to do a DNS lookup, it’d potentially be slightly more on the first failure.
Works In Conjunction With timeout
Timeout configuration is not something you set up in the session. It’s done on a per-request basis. httpbin makes this easy to test. With a sleep delay of 10 seconds it will never work (with a timeout of 5 seconds) but it does use the timeout this time. Same code as above but with a 5 second timeout:
t0 = time.time() try: response = requests_retry_session().get( 'http://httpbin.org/delay/10', timeout=5 ) except Exception as x: print('It failed :(', x.__class__.__name__) else: print('It eventually worked', response.status_code) finally: t1 = time.time() print('Took', t1 - t0, 'seconds')
And the output of this is:
It failed :( ConnectionError Took 21.829053163528442 seconds
That makes sense. Same backoff algorithm as before but now with 5 seconds for each attempt:
21.8 = 5 + 0 + 5 + 0.6 + 5 + 1.2 + 5
Works For 500ish Errors Too
This time, let’s run into a 500 error:
t0 = time.time() try: response = requests_retry_session().get( 'http://httpbin.org/status/500', ) except Exception as x: print('It failed :(', x.__class__.__name__) else: print('It eventually worked', response.status_code) finally: t1 = time.time() print('Took', t1 - t0, 'seconds')
The output becomes:
It failed :( RetryError Took 2.353440046310425 seconds
Here, the reason the total time is 2.35 seconds and not the expected 1.8 is because there’s a delay between my laptop and httpbin.org
. I tested with a local Flask server to do the same thing and then it took a total of 1.8 seconds.
Discussion
Yes, this suggested implementation is very opinionated. But when you’ve understood how it works, understood your choices and have the documentation at hand you can easily implement your own solution.
Personally, I’m trying to replace all my requests.get(...)
with requests_retry_session().get(...)
and when I’m making this change I make sure I set a timeout on the .get()
too.
The choice to consider a 500, 502 and 504 errors «retry’able» is actually very arbitrary. It totally depends on what kind of service you’re reaching for. Some services only return 500’ish errors if something really is broken and is likely to stay like that for a long time. But this day and age, with load balancers protecting a cluster of web heads, a lot of 500 errors are just temporary. Obivously, if you’re trying to do something very specific like requests_retry_session().post(...)
with very specific parameters you probably don’t want to retry on 5xx errors.
Python is a simple, minimalistic, and easy-to-comprehend programming language that is globally-accepted and universally-used today. Its simple, easy-to-learn syntax can sometimes lead Python developers – especially those who are newer to the language – into missing some of its subtleties and underestimating the power of the diverse Python language.
One of the most popular error messages that new developers encounter when using requests
library in Python is the “Max retries exceeded with URL” (besides timeout errors). While it seems simple, sometimes this somewhat vague error message can make even advanced Python developers scratching their head for a few good hours.
This article will show you what causes “Max retries exceeded with URL” error and a few ways to debug it.
Max retries exceeded with URL is a common error, you will encounter it when using requests
library to make a request. The error indicates that the request cannot be made successfully. Usually, the verbose error message should look like the output below
Code language: JavaScript (javascript)
Traceback (most recent call last): File "/home/nl/example.py", line 17, in <module> page1 = requests.get(ap) File "/usr/local/lib/python2.7/dist-packages/requests/api.py", line 55, in get return request('get', url, **kwargs) File "/usr/local/lib/python2.7/dist-packages/requests/api.py", line 44, in request return session.request(method=method, url=url, **kwargs) File "/usr/local/lib/python2.7/dist-packages/requests/sessions.py", line 383, in request resp = self.send(prep, **send_kwargs) File "/usr/local/lib/python2.7/dist-packages/requests/sessions.py", line 486, in send r = adapter.send(request, **kwargs) File "/usr/local/lib/python2.7/dist-packages/requests/adapters.py", line 378, in send raise ConnectionError(e) requests.exceptions.ConnectionError: HTTPSConnectionPool(host='localhost.com', port=443): Max retries exceeded with url: /api (Caused by <class 'socket.gaierror'>: [Errno -2] Name or service not known)
Sometimes, the error message may look slightly different, like below :
Code language: HTML, XML (xml)
requests.exceptions.ConnectionError(MaxRetryError("HTTPSConnectionPool(host='api.example.com', port=443): Max retries exceeded with url: /api.json ( Caused by <class 'socket.error'>: [Errno 10054] An existing connection was forcibly closed by the remote host)",),)
Code language: JavaScript (javascript)
requests.exceptions.ConnectionError: HTTPConnectionPool(host='localhost', port=8001): Max retries exceeded with url: /api (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x10f96ecc0>: Failed to establish a new connection: [Errno 61] Connection refused'))
requests.exceptions.ConnectionError: HTTPConnectionPool(host='www.example.com', port=80): Max retries exceeded with url: /api (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x0000008EC69AAA90>: Failed to establish a new connection: [Errno 11001] getaddrinfo failed'))
Code language: JavaScript (javascript)
Code language: JavaScript (javascript)
requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded with url: /api (Caused by SSLError(SSLEOFError(8, 'EOF occurred in violation of protocol (_ssl.c:997)')))
The error message usually begins with requests.exceptions.ConnectionError
, which tell us that there is something bad happened when requests
was trying to connect. Sometimes, the exception is requests.exceptions.SSLError
which is obviously a SSL-related problem.
The exception then followed by a more detailed string about the error, which could be Failed to establish a new connection: [Errno 61] Connection refused
, [Errno 11001] getaddrinfo failed
, [Errno 10054] An existing connection was forcibly closed by the remote host
or [Errno -2] Name or service not known
. These messages were produced by the underlying system library which requests
called internally. Based on these texts, we can further isolate and fix the problems.
Double-check the URL
There are a possibility that your requested URL wrong. It may be malformed or leading to a non-existent endpoint. In reality, this is usually the case among Python beginners. Seasoned developers can also encounter this error, especially when the URL is parsed from a webpage, which can be a relative URL or schemeless URL.
One way to further debug this is to prepare the URL in advance, then print it before actually making a connection.
Code language: PHP (php)
# ... url = soup.find("#linkout").href print(url) # prints out "/api" which is a non-valid URL r = requests.get(url)
Unstable internet connection / server overload
The underlying problem may be related to your own connection or the server you’re trying to connect to. Unstable internet connection may cause packet loss between network hops, leading to unsuccessful connection. There are times the server has received so many requests that it cannot process them all, therefore your requests won’t receive a response.
In this case, you can try increasing retry attempts and disable keep-alive connections to see if the problems go away. The amount of time spent for each request will certainly increase too, but that’s a trade-off you must accept. Better yet, find a more reliable internet connection.
Code language: PHP (php)
import requests requests.adapters.DEFAULT_RETRIES = 5 # increase retries number s = requests.session() s.keep_alive = False # disable keep alive s.get(url)
Increase request timeout
Another way that you can avoid “Max retries exceeded with URL” error, especially when the server is busy handling a huge number of connections, is to increase the amount of time requests
library waits for a response from the server. In other words, you wait longer for a response, but increase the chance for a request to successfully finishes. This method can also be applied when the server is in a location far away from yours.
In order to increase request timeout, simply pass the time value in seconds to the get
or post
method :
r = requests.get(url, timeout=3)
You can also pass a tuple to timeout
with the first element being a connect timeout (the time it allows for the client to establish a connection to the server), and the second being a read timeout (the time it will wait on a response once your client has established a connection).
If the request establishes a connection within 2 seconds and receives data within 5 seconds of the connection being established, then the response will be returned as it was before. If the request times out, then the function will raise a Timeout
exception:
Code language: JavaScript (javascript)
requests.get('https://api.github.com', timeout=(2, 5))
Apply backoff factor
backoff_factor
is an urllib3 argument, the library which requests
relies on to initialize a network connection. Below is an example where we use backoff_factor
to slow down the requests to the servers whenever there’s a failed one.
Code language: JavaScript (javascript)
import requests from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry session = requests.Session() retry = Retry(connect=3, backoff_factor=1) adapter = HTTPAdapter(max_retries=retry) session.mount('http://', adapter) session.mount('https://', adapter) session.get(url)
According to urllib3 documentation, backoff_factor is base value which the library use to calculate sleep interval between retries. Specifically, urllib3 will sleep for {backoff factor} * (2 ^ ({number of total retries} - 1))
seconds after every failed connection attempt.
For example, If the backoff_factor is 0.1, then sleep()
will sleep for 0.0s, 0.2s, 0.4s, … between retries. By default, backoff is disabled (set to 0). It will also force a retry if the status code returned is 500, 502, 503 or 504.
You can customize Retry
to have even more granular control over retries. Other notable options are:
- total – Total number of retries to allow.
- connect – How many connection-related errors to retry on.
- read – How many times to retry on read errors.
- redirect – How many redirects to perform.
- _methodwhitelist – Set of uppercased HTTP method verbs that we should retry on.
- _statusforcelist – A set of HTTP status codes that we should force a retry on.
- _backofffactor – A backoff factor to apply between attempts.
- _raise_onredirect – Whether, if the number of redirects is exhausted, to raise a
MaxRetryError
, or to return a response with a response code in the 3xx range. - raise_on_status – Similar meaning to _raise_onredirect: whether we should raise an exception, or return a response, if status falls in _statusforcelist range and retries have been exhausted.
We hope that the article helped you successfully debugged “Max retries exceeded with URL” error in Python requests library, as well as avoid encountering it in the future. We’ve also written a few other guides for fixing common Python errors, such as Timeout in Python requests, Python Unresolved Import in VSCode or “IndexError: List Index Out of Range” in Python. If you have any suggestion, please feel free to leave a comment below.