Skip to content

Commit 95bb6c7

Browse files
committed
fix(httpweb): resolve pending request future on disconnect
HTTPSClientEx canceled its timeout timer on disconnect without completing the request promise. FileClient::downloadFile() could then block forever in future::get(), and FileClient's destructor froze the GUI while joining the download thread on cancel. Fail the pending promise on disconnect and guard promise completion so a late response cannot satisfy it twice. Log: resolve pending request future on disconnect PMS: TASK-396845 Influence: cancelling a download no longer blocks the GUI while joining the download thread.
1 parent 8e31bef commit 95bb6c7

3 files changed

Lines changed: 79 additions & 2 deletions

File tree

‎src/infrastructure/netutil/include/http/https_client.h‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
1+
// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd.
22
//
33
// SPDX-License-Identifier: GPL-3.0-or-later
44

@@ -11,6 +11,7 @@
1111
#include "asio/ssl_client.h"
1212
#include "asio/timer.h"
1313

14+
#include <atomic>
1415
#include <future>
1516

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

276281
void SetPromiseValue(const HTTPResponse& response);
277282
void SetPromiseError(const std::string& error);

‎src/infrastructure/netutil/src/http/https_client.cpp‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
1+
// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd.
22
//
33
// SPDX-License-Identifier: GPL-3.0-or-later
44

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

7272
_promise = std::promise<HTTPResponse>();
73+
_promise_pending.store(true);
7374
_request = request;
7475

7576
// Check if the HTTP request is valid
@@ -135,7 +136,14 @@ void HTTPSClientEx::onDisconnected()
135136
if (_timeout)
136137
_timeout->Cancel();
137138

139+
// Give a partially received response a chance to complete first.
138140
HTTPSClient::onDisconnected();
141+
142+
// The connection dropped while the request was still pending and the
143+
// timeout timer above is now canceled: nothing else would ever resolve
144+
// the future. Fail it so synchronous callers (FileClient::downloadFile)
145+
// do not block forever.
146+
SetPromiseError("Disconnected!");
139147
}
140148

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

159167
void HTTPSClientEx::SetPromiseValue(const HTTPResponse& response)
160168
{
169+
// A late response after a timeout/disconnect must not complete the
170+
// promise twice, which throws std::future_error on the IO thread.
171+
if (!_promise_pending.exchange(false))
172+
return;
173+
161174
_promise.set_value(response);
162175
_request.Clear();
163176
}
164177

165178
void HTTPSClientEx::SetPromiseError(const std::string& error)
166179
{
180+
if (!_promise_pending.exchange(false))
181+
return;
182+
167183
_promise.set_exception(std::make_exception_ptr(std::runtime_error(error)));
168184
_request.Clear();
169185
}

‎tests/manager/httpclient_test.cpp‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
2+
//
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
#include <gtest/gtest.h>
6+
7+
#include "asioservice.h"
8+
#include "http/https_client.h"
9+
#include "manager/secureconfig.h"
10+
11+
#include <QHostAddress>
12+
#include <QTcpServer>
13+
#include <QTcpSocket>
14+
#include <QTest>
15+
16+
#include <chrono>
17+
18+
// A connection dropped before the response arrives must complete the pending
19+
// request future. Otherwise FileClient::downloadFile() stays blocked in
20+
// future::get() and FileClient::~FileClient(), which joins the download
21+
// thread on the GUI thread, freezes the application on cancel.
22+
TEST(HTTPClientExTest, DisconnectResolvesPendingRequest)
23+
{
24+
QTcpServer server;
25+
ASSERT_TRUE(server.listen(QHostAddress::LocalHost, 0));
26+
QObject::connect(&server, &QTcpServer::newConnection, &server, [&server] {
27+
// Accept the TCP connection but never complete the TLS handshake.
28+
while (QTcpSocket *socket = server.nextPendingConnection())
29+
socket->abort();
30+
});
31+
32+
auto service = std::make_shared<AsioService>();
33+
service->Start();
34+
35+
auto client = std::make_shared<NetUtil::HTTP::HTTPSClientEx>(
36+
service, SecureConfig::clientContext(),
37+
"127.0.0.1", server.serverPort());
38+
auto future = client->SendGetRequest("download/payload.txt&token=test",
39+
BaseKit::Timespan::seconds(30));
40+
41+
// Keep the event loop alive so QTcpServer accepts and drops the socket;
42+
// the request timeout is 30 seconds, so a ready future here can only come
43+
// from the disconnect handler.
44+
for (int i = 0; i < 50
45+
&& future.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready;
46+
++i) {
47+
QTest::qWait(100);
48+
}
49+
50+
ASSERT_EQ(future.wait_for(std::chrono::milliseconds(0)), std::future_status::ready);
51+
EXPECT_THROW(future.get(), std::runtime_error);
52+
53+
client->DisconnectAsync();
54+
client.reset();
55+
service->Stop();
56+
}

0 commit comments

Comments
 (0)