Итак, вы хотите написать HTTP-клиент

Автор: | 27.07.2026

Сегодня редко можно встретить сайт или компанию, которым не приходится делать HTTP-запросы к сторонним сервисам. Мобильные приложения должны обмениваться данными с сервером. Серверы взаимодействуют с такими сторонними инструментами, как Facebook, Twitter, Sendgrid (Twilio!) и другими. Большинство компаний, реализующих сервис-ориентированную архитектуру, используют HTTP для обмена данными между серверами в кластере.

В идеальных условиях ваши HTTP-запросы возвращают код 200, делают это относительно быстро, и панели мониторинга горят зелёным. Но кого мы обманываем — это интернет, где всё ломается постоянно. В нашем стремлении к пяти девяткам надёжности давайте разберёмся, как могут завершаться неудачей HTTP-запросы и как написать надёжный, готовый к продакшену HTTP-клиент, способный справляться с этими проблемами.

Иногда ваш сервер не может подключиться к удалённой машине. Это происходит по разным причинам: сервер может быть недоступен, DNS-запрос может не сработать, или вы можете стать жертвой популярного и трудно диагностируемого «сетевого сбоя». Во всех этих случаях код, который выглядит так:

«`

response = request(‘GET’, ‘exampleurl.com’)

«`

не вернёт объект ответа так, как вы ожидаете, потому что HTTP-запрос никогда не завершится! Вместо этого библиотека выбросит ошибку `ConnectionError`, или `response` будет представлять собой какой-то объект ошибки вместо HTTP-ответа.

Есть два действия, которые нужно предпринять для обработки этой ошибки. Во-первых, необходимо установить тайм-аут на попытку подключения. **Нет ничего хуже, чем неудачное подключение, которое зависает на 30 секунд перед возвратом ошибки.** Как правило, попытка подключения `connect()` либо завершается очень быстро, либо не завершается никогда. В Twilio мы обычно устанавливаем 3100 миллисекунд на тайм-аут подключения — 3 секунды на стандартное окно повторной передачи TCP плюс небольшой запас времени для успешного выполнения запроса. Большинство наших серверов находятся в одном регионе Amazon, поэтому 100 миллисекунд более чем достаточно. Если вы делаете запрос через океан, возможно, стоит немного увеличить это значение.

Во-вторых, **запросы, завершившиеся ошибкой подключения, всегда можно безопасно повторить**, так как это означает, что запрос не дошёл до сервера. Рассмотрите возможность обернуть HTTP-запрос в блок try/catch, который будет обнаруживать ошибки подключения и повторять их несколько раз, возможно, с предварительным вызовом `sleep()`.

Итак, вы установили соединение с сервером и отправили HTTP-запрос. Иногда сервер ведёт себя неправильно и не отправляет корректный HTTP-ответ. Самая распространённая из таких ошибок — закрытое соединение. Она может проявляться в сообщениях об ошибках как «сервер неожиданно закрыл соединение» или как «EOFError» либо «Bad status line». В целом такие ошибки следует рассматривать как ошибки уровня 500. Однако будьте осторожны, так как **большинство клиентов по умолчанию объединяют ошибки закрытого соединения с ошибками подключения.** В отличие от ошибок подключения, эти ошибки означают, что запрос дошёл до сервера и может быть небезопасно повторять. Вам следует аккуратно перехватывать эту ошибку и обрабатывать её как ошибку уровня 500.

Наиболее распространённая причина закрытого соединения — проблема в цепочке тайм-аутов вашего приложения. Обязательно проверьте, что произойдёт, если ваше приложение решит «зависнуть» навсегда. В частности, PHP иногда игнорирует `max_execution_time`, поэтому стоит установить это ограничение в nginx или Apache.

Распространённый сценарий: пользователи регистрируются в вашем приложении, и вы делаете API-вызов к Sendgrid или Mailgun, чтобы отправить им письмо с текстом: «Спасибо за регистрацию! Вот все крутые вещи, которые вы можете делать» и так далее. Ваш код регистрации может выглядеть так:

«`

@app.route(‘/signup’, methods=[‘POST’])

def signup():

# Этот запрос делает HTTP-вызов к стороннему почтовому сервису

send_welcome_email(request.form[’email_address’])

return «Спасибо за регистрацию!»

«`

Теперь представьте, что у Sendgrid плохой день, и на каждый запрос уходит 25 секунд. Поток вашего приложения будет терпеливо ждать ответа от Sendgrid, и все ваши пользователи увидят вращающийся курсор в браузере на 25 секунд, пытаясь зарегистрироваться в вашем модном сервисе. Гарантирую, что это плохо скажется на конверсии.

Одно из решений — выносить задачи в асинхронную обработку, но это выходит за рамки данной статьи. Другое решение — установить тайм-аут на внешний запрос. Если сторонний запрос занимает слишком много времени, прервите его и выполните резервную логику. **Каждый внешний запрос в вашей системе должен иметь установленное значение тайм-аута.**

Примечание: этот тайм-аут отличается от тайм-аута подключения — он срабатывает только после установления соединения и ожидания ответа. Ваш HTTP-клиент может называть его «read» или «request» тайм-аутом. Если у вашего клиента есть только одно значение для «timeout», оно применяется как к подключению, так и к чтению. Это плохо!! Если ваш клиент так делает, попросите мейнтейнера добавить отдельные тайм-ауты, напишите код для разделения значений тайм-аутов или напишите собственный, более качественный клиент.

Наконец, мы подошли к самому распространённому типу ошибок — не 200-му уровню HTTP-ответа. Обычно это означает, что вы попытались что-то сделать, и сервер «выдал ошибку», или вы отправили некорректный запрос. Самый важный вопрос сейчас — *безопасно ли повторять запрос.* Чтобы ответить на него, нужно ввести понятие идемпотентности.

Запрос является *идемпотентным*, если система приходит в одно и то же состояние независимо от того, выполнен запрос один раз или несколько. Запрос *не является идемпотентным*, если при его многократном выполнении система может меняться каждый раз. Идемпотентность ценна тем, что такие запросы **всегда безопасно повторять.** Рассмотрим следующие примеры идемпотентных действий:

— Завершение звонка. Если запрос на завершение не удался, просто повторите его.

— Загрузка электронной почты. Сколько бы раз вы ни нажимали «Загрузить», всегда будет отображаться последнее полученное письмо.

— Мытьё грязной посуды. Она будет в состоянии «чистая» независимо от того, помоете вы её один раз или десять.

А вот примеры действий, которые *не являются* идемпотентными:

— Отправка текстового сообщения. Если вы несколько раз наберёте сообщение и нажмёте «Отправить», сообщение будет отправлено несколько раз.

— Нажатие кнопки «Купить» в большинстве интернет-магазинов. Если вы когда-либо видели паническое сообщение «Не нажимайте кнопку покупки дважды!», вы сталкивались с примером неидемпотентной транзакции.

Рад, что вы спросили! Идемпотентность очень полезна для продакшен-систем, потому что это означает, что **при неудаче идемпотентного запроса его можно просто повторить.** Это настолько важно, что я повторю: при неудаче идемпотентного запроса его можно просто повторить, не беспокоясь о двойном списании средств с карты, многократной отправке одного и того же текстового сообщения и так далее. Благодаря гарантиям ACID это ограничение идемпотентности лучше всего обеспечивать на уровне базы данных.

Некоторые HTTP-методы изначально спроектированы как идемпотентные. В частности, в системе, следующей спецификации HTTP, запрос с использованием методов GET, PUT, DELETE, HEAD или OPTIONS подразумевает, что запрос безопасно повторять. POST-запросы иногда можно повторять, но обычно это небезопасно.

Таким образом, если запрос завершился неудачей (из-за закрытого соединения, ошибки тайм-аута чтения или 5xx-ошибки сервера), его следует повторить, если запрос идемпотентен. Если неудачными оказались запросы к другим методам, например POST, повторять их может быть небезопасно.

Некоторые серверы ограничивают количество одновременных запросов к своему API за определённый период времени, поэтому иногда вы можете получить ответ с просьбой «успокоиться». Для этого случая также выделен HTTP-код состояния 429 Too Many Requests. В целом, если вы получили этот статус, запрос также безопасно повторить, независимо от используемого HTTP-метода, так как сервер ещё не начал обработку.

Я работаю над небольшой библиотекой, которая симулирует различные типы сетевых сбоев. Подключаясь к разным HTTP-портам на сервере, можно симулировать разные типы сбоев и убедиться, что ваш клиент обрабатывает их корректно. Ознакомьтесь с ней здесь.

HTTP-запросы могут завершаться неудачей по нескольким причинам: сервер может быть недоступен, сервер может вести себя неправильно, сервер может работать медленно или не обработать ваш запрос. Стандартное поведение большинства реализаций HTTP-библиотек не подходит для готового к продакшену клиента — как и в случае с сервером базы данных, вам нужно настраивать HTTP-клиент для каждого запроса.

Мы также ищем умных людей, которые хотят научиться писать отказоустойчивые приложения. Подавайте заявку через нашу страницу вакансий или свяжитесь со мной напрямую, если у вас есть вопросы о должности.