Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/infrastructure/netutil/include/http/https_client.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

Expand All @@ -11,6 +11,7 @@
#include "asio/ssl_client.h"
#include "asio/timer.h"

#include <atomic>
#include <future>

namespace NetUtil {
Expand Down Expand Up @@ -272,6 +273,10 @@ class HTTPSClientEx : public HTTPSClient
std::shared_ptr<Asio::TCPResolver> _resolver;
std::shared_ptr<Asio::Timer> _timeout;
std::promise<HTTPResponse> _promise;
// Guards the promise against being completed twice: the timeout handler
// and a disconnect can race, and a duplicate set_value/set_exception
// throws std::future_error on the IO thread.
std::atomic<bool> _promise_pending { false };

void SetPromiseValue(const HTTPResponse& response);
void SetPromiseError(const std::string& error);
Expand Down
18 changes: 17 additions & 1 deletion src/infrastructure/netutil/src/http/https_client.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

Expand Down Expand Up @@ -70,6 +70,7 @@ std::future<HTTPResponse> HTTPSClientEx::SendRequest(const HTTPRequest& request,
_timeout = std::make_shared<Asio::Timer>(service());

_promise = std::promise<HTTPResponse>();
_promise_pending.store(true);
_request = request;

// Check if the HTTP request is valid
Expand Down Expand Up @@ -135,7 +136,14 @@ void HTTPSClientEx::onDisconnected()
if (_timeout)
_timeout->Cancel();

// Give a partially received response a chance to complete first.
HTTPSClient::onDisconnected();

// The connection dropped while the request was still pending and the
// timeout timer above is now canceled: nothing else would ever resolve
// the future. Fail it so synchronous callers (FileClient::downloadFile)
// do not block forever.
SetPromiseError("Disconnected!");
}

void HTTPSClientEx::onReceivedResponse(const HTTPResponse& response)
Expand All @@ -158,12 +166,20 @@ void HTTPSClientEx::onReceivedResponseError(const HTTPResponse& response, const

void HTTPSClientEx::SetPromiseValue(const HTTPResponse& response)
{
// A late response after a timeout/disconnect must not complete the
// promise twice, which throws std::future_error on the IO thread.
if (!_promise_pending.exchange(false))
return;

_promise.set_value(response);
_request.Clear();
}

void HTTPSClientEx::SetPromiseError(const std::string& error)
{
if (!_promise_pending.exchange(false))
return;

_promise.set_exception(std::make_exception_ptr(std::runtime_error(error)));
_request.Clear();
}
Expand Down
56 changes: 56 additions & 0 deletions tests/manager/httpclient_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

#include <gtest/gtest.h>

#include "asioservice.h"
#include "http/https_client.h"
#include "manager/secureconfig.h"

#include <QHostAddress>
#include <QTcpServer>
#include <QTcpSocket>
#include <QTest>

#include <chrono>

// A connection dropped before the response arrives must complete the pending
// request future. Otherwise FileClient::downloadFile() stays blocked in
// future::get() and FileClient::~FileClient(), which joins the download
// thread on the GUI thread, freezes the application on cancel.
TEST(HTTPClientExTest, DisconnectResolvesPendingRequest)
{
QTcpServer server;
ASSERT_TRUE(server.listen(QHostAddress::LocalHost, 0));
QObject::connect(&server, &QTcpServer::newConnection, &server, [&server] {
// Accept the TCP connection but never complete the TLS handshake.
while (QTcpSocket *socket = server.nextPendingConnection())
socket->abort();
});

auto service = std::make_shared<AsioService>();
service->Start();

auto client = std::make_shared<NetUtil::HTTP::HTTPSClientEx>(
service, SecureConfig::clientContext(),
"127.0.0.1", server.serverPort());
auto future = client->SendGetRequest("download/payload.txt&token=test",
BaseKit::Timespan::seconds(30));

// Keep the event loop alive so QTcpServer accepts and drops the socket;
// the request timeout is 30 seconds, so a ready future here can only come
// from the disconnect handler.
for (int i = 0; i < 50
&& future.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready;
++i) {
QTest::qWait(100);
}

ASSERT_EQ(future.wait_for(std::chrono::milliseconds(0)), std::future_status::ready);
EXPECT_THROW(future.get(), std::runtime_error);

client->DisconnectAsync();
client.reset();
service->Stop();
}
Loading