Небольшая предыстория
Несколько лет назад был реализован компонент выгрузки данных во внутренний бэкэнд компании. В какой‑то момент, как это часто бывает, пришлось временно переключиться на помощь другой команде и в авральном режиме закрывать проект. В результате задачи по развитию приложения, включая переход на новый бэкэнд, были мной отложены.
Через пару месяцев я вернулся к этой части системы и обнаружил, что вместо адаптации существующего решения появился классический Ctrl+C → Ctrl+V. В качестве сообщений использовался огромный std::variant, который передавался между потоками через QEventLoop, сигналы и слоты Qt. Со временем этот тип разросся настолько, что превратился в универсальный контейнер «на все случаи жизни».
Сам код приводить не буду — там слишком много однотипной логики и довольно внушительная коллекция анти‑паттернов. Но именно этот опыт и подтолкнул меня к мысли полностью пересмотреть подход, переписать компонент и сделать его действительно thread‑safe, избавившись от лишнего копирования, чрезмерной связности и зависимости от внутренней механики Qt.
Введение
Qt предоставляет достаточно удобный сетевой стек в виде QNetworkAccessManager, однако при разработке крупных приложений довольно быстро начинают проявляться типичные проблемы: большое количество шаблонного сетевого кода, дублирование обработки ошибок, отсутствие compile‑time гарантий соответствия между Request и Response, ручная реализация retry‑механизмов, таймауты, разбросанные по приложению, сложность корректного завершения запросов при shutdown и проблемы управления жизненным циклом QNetworkReply.
В этой статье я хочу показать архитектуру небольшого HTTP runtime, построенного поверх Qt Network и современного C++20. Она предоставляет контракты через concepts, типобезопасные модели запросов и ответов, callback— и future—ориентированные API, настраиваемые политики повторных попыток с экспоненциальной задержкой, корректное завершение работы, централизованное управление жизненным циклом запросов и выделенный поток обработки I/O, в котором также выполняется конструирование моделей сообщений.
Как выглядит API и что находится внутри
Хотелось получить максимально простой пользовательский код вместо классического подхода с использованием Qt Network. В идеале работа с HTTP‑запросами должна выглядеть как обычный вызов типобезопасного API, скрывая всю инфраструктуру выполнения. Например:
auto response = server .send(GetUserRequest{}) .wait(); if (response.success()) { ... } или server.send(GetUserRequest{}, [](UserResponse response) { ... }); вместо auto* const reply = manager->get(request); connect(reply, &QNetworkReply::finished, this, [reply]() { ... });
Чтобы добиться такого интерфейса, одной обёртки над QNetworkAccessManager оказалось недостаточно. В результате получилась архитектура небольшого сетевого runtime, отвечающего за выполнение запросов, управление их жизненным циклом, повторные попытки, таймауты и корректное завершение работы приложения.

Compile‑time контракты
Одной из ключевых идей стало использование concepts.
template<typename T> concept ResponseT = requires( QNetworkReply& reply, std::exception_ptr ex) { { T::construct(reply) } -> std::same_as<T>; { T::construct(ex) } noexcept -> std::same_as<T>; };
Каждый Response обязан поддерживать 2 метода: создание объекта из полученного ответа и регистрация исключений при возникновении проблем с сетью, неосознанных ошибок программиста и тому подобное
Важно: в последнем случае не должно быть исключений, чтобы гарантировать создание объекта, так как не хотелось бы при сетевой ошибке далеко разворачивать стек и в худшем случае «крашить» программу.
struct UserResponse { static UserResponse construct(QNetworkReply&); static UserResponse construct(std::exception_ptr) noexcept; };
Это гарантирует, что любой Response: умеет десериализоваться, умеет представлять ошибку, имеет единый интерфейс и удовлетворяет compile‑time контракту. Если разработчик забудет реализовать один из методов, код просто не соберётся.
template<typename T> concept RequestT = requires( T t, QNetworkRequest&& req, QString host, IHttpClient& client) { typename T::Response; requires ResponseT<typename T::Response>; t.send(std::move(req), host, client); }; // пример struct GetUserRequest { using Response = UserResponse; [[nodiscard]] QNetworkReply* send( QNetworkRequest&&, QString, IHttpClient&); };
Аналогичным образом устроена связь между запросом и ответом. Каждый тип Request явно определяет соответствующий ему тип Response. Благодаря этому становится невозможно случайно вернуть ответ другого типа или перепутать модели при дальнейшем развитии проекта.
В результате удалось полностью отказаться от универсальных контейнеров и динамически типизированных прослоек вроде QVariant, std::variant и подобных решений, которые нередко используются для передачи разнородных данных между слоями приложения. Вместо этого все связи между запросами, ответами и моделями становятся явными и проверяются компилятором, что делает код проще для сопровождения и существенно снижает вероятность ошибок при рефакторинге.
Интерфейс Server
Максимально простое API из разряда: отправить синхронно или асинхронно. Поэтому интерфейс спроектирован следующим образом (тут я опущу описание всех параметров методов, так как иначе статья сильно разрастется, но замечу, что для удобства вызова первого метода использовал tag‑dispatch):
class Server final { public: template<typename T> class Result { public: explicit Result(const QFuture<T>& future) : m_future(future) { } [[nodiscard]] T wait() { m_future.waitForFinished(); return m_future.takeResult(); } private: QFuture<T> m_future; }; public: // feature api template<detail::RequestT Req, detail::ResponseT Resp = typename std::remove_cvref_t<Req>::Response> [[nodiscard]] Result<Resp> send(Req&& request, const Designator<Req>& = { }) { auto p = QPromise<Resp>(); auto f = p.future(); p.start(); schedule(...); return Result(f); } // callback api template<detail::RequestT Req, typename Func, detail::ResponseT Resp = typename std::remove_cvref_t<Req>::Response> void send(Req&& request, Func&& cb) { schedule(...); } private: void schedule(...); };
«Серый кардинал» Worker
Несмотря на то, что публичный API выглядит достаточно компактным, вся основная работа выполняется внутри класса Worker. Именно он является центральным элементом runtime и координирует выполнение каждой сетевой операции.
Для простоты я опущу описание структуры Task. Она представляет собой внутреннюю модель отдельного HTTP‑запроса, содержащую всю информацию, необходимую для реализации функциональных возможностей runtime, и будет подробно рассмотрена в исходном коде.
class Worker final : public QObject { // часть приватных методов намерано удалена public: void send(std::shared_ptr<Task> task); void stop(); private: QThread m_thread; QNetworkAccessManager* const m_manager; std::map<QNetworkReply*, std::shared_ptr<Task>> m_awaiting; std::map<QUuid, std::shared_ptr<Task>> m_pending; std::atomic_bool m_stopping; };
Worker инкапсулирует всю инфраструктурную логику выполнения HTTP‑запросов. Он отвечает за их отправку, повторные попытки при возникновении временных ошибок, контроль таймаутов, управление жизненным циклом активных операций, корректное завершение работы (graceful shutdown) и доставку результатов вызывающей стороне. Благодаря этому бизнес‑логика взаимодействует только с типизированными моделями запросов и ответов, не заботясь о деталях выполнения сетевых операций.
Выполнение запросов и управление их жизненным циклом
Для выполнения сетевых операций Worker использует собственный поток QThread, внутри которого создаётся единственный экземпляр QNetworkAccessManager.

Фактически Worker выступает специализированным executor для HTTP‑запросов. Все операции — создание QNetworkReply, обработка сигналов, повторные попытки и завершение запросов — выполняются в одном execution context. Такой подход позволяет отказаться от дополнительной синхронизации между потоками, исключить гонки при работе с сетевыми объектами и централизовать управление всей сетевой подсистемой.

Для отслеживания состояния запросов используются две внутренние коллекции: awaiting и pending.
awaiting содержит все активные HTTP‑запросы, которым уже соответствует объект QNetworkReply. После завершения операции запись удаляется из этой коллекции.
pending напротив, хранит задачи, ожидающие повторной отправки согласно выбранной retry policy. После истечения времени ожидания задача вновь передаётся на выполнение и при успешной отправке перемещается в ожидание.

Таким образом каждый запрос последовательно проходит несколько состояний: создание задачи, отправку, ожидание ответа, при необходимости — одну или несколько повторных попыток, после чего завершается успешным результатом либо ошибкой. При этом пользователь библиотеки никогда не взаимодействует напрямую с QNetworkReply — наружу всегда возвращается уже готовый типизированный Response.
Особое внимание уделено корректному завершению работы (graceful shutdown).

После вызова stop() Worker устанавливает атомарный флаг m_stopping, запрещающий приём новых задач. Далее runtime последовательно очищает очередь ожидающих повторной отправки запросов, останавливает механизмы retry, завершает обработку активных задач, отменяет все существующие QNetworkReply и уведомляет пользователей API о завершении каждой операции — независимо от того, используется ли callback или future‑интерфейс.
Главная гарантия такого подхода заключается в том, что каждый отправленный запрос завершается ровно один раз. Независимо от сценария — успешное выполнение, ошибка, отмена или остановка приложения — пользователь получает единственный, корректно сформированный результат без риска двойного вызова callback или повторного завершения.
Итоги
На первый взгляд проект выглядит как обёртка над QNetworkAccessManager, однако в процессе разработки он постепенно превратился в полноценный HTTP runtime. Он обеспечивает строгие compile‑time гарантии для моделей Request и Response, предоставляет два способа работы с асинхронностью — через callback и future, централизованно управляет повторными попытками, таймаутами и жизненным циклом запросов, выполняет всю сетевую работу в выделенном потоке и гарантирует корректное завершение операций даже во время graceful shutdown. Именно эти свойства, на мой взгляд, позволяют сделать использование Qt Network значительно более предсказуемым и удобным в проектах.
Исходный код доступен в репозитории по ссылке. Надеюсь, этот материал окажется полезным и, возможно, подскажет идеи для проектирования собственных сетевых библиотек и HTTP‑клиентов при использовали Qt.
Это моя первая публикация, поэтому буду признателен за конструктивную обратную связь. Понимаю, что универсальных решений не существует и предложенный подход подойдёт далеко не для всех сценариев. Тем не менее, мне хотелось показать один из возможных способов использования Qt Network в сочетании с возможностями современного C++, такими как строгая типизация, декларативное описание DTO и разделение ответственности между компонентами системы.
Возможные улучшения
Дальнейшее развитие может включать:
std::stop_token;rate limiting;
structured concurrency.