Allow to cancel asynchronous HTTP requests (#332)

Usage:

	auto args = this->httpClient.createRequest(url, method);
	httpClient.performRequest(args, ...);
	[...]
	// Oops, we don't actually want to complete the request!
	args->cancel = true;
This commit is contained in:
Martin Natano 2021-12-21 08:01:55 +01:00 committed by GitHub
parent 688f85fda6
commit 66cd29e747
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 81 additions and 11 deletions

View File

@ -517,6 +517,9 @@ bool ok = httpClient.performRequest(args, [](const HttpResponsePtr& response)
); );
// ok will be false if your httpClient is not async // ok will be false if your httpClient is not async
// A request in progress can be cancelled by setting the cancel flag. It does nothing if the request already completed.
args->cancel = true;
``` ```
See this [issue](https://github.com/machinezone/IXWebSocket/issues/209) for links about uploading files with HTTP multipart. See this [issue](https://github.com/machinezone/IXWebSocket/issues/209) for links about uploading files with HTTP multipart.

View File

@ -8,6 +8,7 @@
#include "IXProgressCallback.h" #include "IXProgressCallback.h"
#include "IXWebSocketHttpHeaders.h" #include "IXWebSocketHttpHeaders.h"
#include <atomic>
#include <tuple> #include <tuple>
#include <unordered_map> #include <unordered_map>
@ -30,6 +31,7 @@ namespace ix
TooManyRedirects = 12, TooManyRedirects = 12,
ChunkReadError = 13, ChunkReadError = 13,
CannotReadBody = 14, CannotReadBody = 14,
Cancelled = 15,
Invalid = 100 Invalid = 100
}; };
@ -87,6 +89,7 @@ namespace ix
bool compressRequest = false; bool compressRequest = false;
Logger logger; Logger logger;
OnProgressCallback onProgressCallback; OnProgressCallback onProgressCallback;
std::atomic<bool> cancel;
}; };
using HttpRequestArgsPtr = std::shared_ptr<HttpRequestArgs>; using HttpRequestArgsPtr = std::shared_ptr<HttpRequestArgs>;

View File

@ -241,17 +241,21 @@ namespace ix
std::string errMsg; std::string errMsg;
// Make a cancellation object dealing with connection timeout // Make a cancellation object dealing with connection timeout
auto isCancellationRequested = auto cancelled = makeCancellationRequestWithTimeout(args->connectTimeout, args->cancel);
makeCancellationRequestWithTimeout(args->connectTimeout, _stop);
auto isCancellationRequested = [&]() {
return cancelled() || _stop;
};
bool success = _socket->connect(host, port, errMsg, isCancellationRequested); bool success = _socket->connect(host, port, errMsg, isCancellationRequested);
if (!success) if (!success)
{ {
auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::CannotConnect;
std::stringstream ss; std::stringstream ss;
ss << "Cannot connect to url: " << url << " / error : " << errMsg; ss << "Cannot connect to url: " << url << " / error : " << errMsg;
return std::make_shared<HttpResponse>(code, return std::make_shared<HttpResponse>(code,
description, description,
HttpErrorCode::CannotConnect, errorCode,
headers, headers,
payload, payload,
ss.str(), ss.str(),
@ -260,7 +264,7 @@ namespace ix
} }
// Make a new cancellation object dealing with transfer timeout // Make a new cancellation object dealing with transfer timeout
isCancellationRequested = makeCancellationRequestWithTimeout(args->transferTimeout, _stop); cancelled = makeCancellationRequestWithTimeout(args->transferTimeout, args->cancel);
if (args->verbose) if (args->verbose)
{ {
@ -277,10 +281,11 @@ namespace ix
if (!_socket->writeBytes(req, isCancellationRequested)) if (!_socket->writeBytes(req, isCancellationRequested))
{ {
auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::SendError;
std::string errorMsg("Cannot send request"); std::string errorMsg("Cannot send request");
return std::make_shared<HttpResponse>(code, return std::make_shared<HttpResponse>(code,
description, description,
HttpErrorCode::SendError, errorCode,
headers, headers,
payload, payload,
errorMsg, errorMsg,
@ -296,10 +301,11 @@ namespace ix
if (!lineValid) if (!lineValid)
{ {
auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::CannotReadStatusLine;
std::string errorMsg("Cannot retrieve status line"); std::string errorMsg("Cannot retrieve status line");
return std::make_shared<HttpResponse>(code, return std::make_shared<HttpResponse>(code,
description, description,
HttpErrorCode::CannotReadStatusLine, errorCode,
headers, headers,
payload, payload,
errorMsg, errorMsg,
@ -333,10 +339,11 @@ namespace ix
if (!headersValid) if (!headersValid)
{ {
auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::HeaderParsingError;
std::string errorMsg("Cannot parse http headers"); std::string errorMsg("Cannot parse http headers");
return std::make_shared<HttpResponse>(code, return std::make_shared<HttpResponse>(code,
description, description,
HttpErrorCode::HeaderParsingError, errorCode,
headers, headers,
payload, payload,
errorMsg, errorMsg,
@ -405,10 +412,11 @@ namespace ix
contentLength, args->onProgressCallback, isCancellationRequested); contentLength, args->onProgressCallback, isCancellationRequested);
if (!chunkResult.first) if (!chunkResult.first)
{ {
auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::ChunkReadError;
errorMsg = "Cannot read chunk"; errorMsg = "Cannot read chunk";
return std::make_shared<HttpResponse>(code, return std::make_shared<HttpResponse>(code,
description, description,
HttpErrorCode::ChunkReadError, errorCode,
headers, headers,
payload, payload,
errorMsg, errorMsg,
@ -424,6 +432,7 @@ namespace ix
while (true) while (true)
{ {
auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::ChunkReadError;
lineResult = _socket->readLine(isCancellationRequested); lineResult = _socket->readLine(isCancellationRequested);
line = lineResult.second; line = lineResult.second;
@ -431,7 +440,7 @@ namespace ix
{ {
return std::make_shared<HttpResponse>(code, return std::make_shared<HttpResponse>(code,
description, description,
HttpErrorCode::ChunkReadError, errorCode,
headers, headers,
payload, payload,
errorMsg, errorMsg,
@ -458,10 +467,11 @@ namespace ix
(size_t) chunkSize, args->onProgressCallback, isCancellationRequested); (size_t) chunkSize, args->onProgressCallback, isCancellationRequested);
if (!chunkResult.first) if (!chunkResult.first)
{ {
auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::ChunkReadError;
errorMsg = "Cannot read chunk"; errorMsg = "Cannot read chunk";
return std::make_shared<HttpResponse>(code, return std::make_shared<HttpResponse>(code,
description, description,
HttpErrorCode::ChunkReadError, errorCode,
headers, headers,
payload, payload,
errorMsg, errorMsg,
@ -475,9 +485,10 @@ namespace ix
if (!lineResult.first) if (!lineResult.first)
{ {
auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::ChunkReadError;
return std::make_shared<HttpResponse>(code, return std::make_shared<HttpResponse>(code,
description, description,
HttpErrorCode::ChunkReadError, errorCode,
headers, headers,
payload, payload,
errorMsg, errorMsg,

View File

@ -221,4 +221,57 @@ TEST_CASE("http_client", "[http]")
REQUIRE(statusCode1 == 200); REQUIRE(statusCode1 == 200);
REQUIRE(statusCode2 == 200); REQUIRE(statusCode2 == 200);
} }
SECTION("Async API, cancel")
{
bool async = true;
HttpClient httpClient(async);
WebSocketHttpHeaders headers;
SocketTLSOptions tlsOptions;
tlsOptions.caFile = "cacert.pem";
httpClient.setTLSOptions(tlsOptions);
std::string url("http://httpbin.org/delay/10");
auto args = httpClient.createRequest(url);
args->extraHeaders = headers;
args->connectTimeout = 60;
args->transferTimeout = 60;
args->followRedirects = true;
args->maxRedirects = 10;
args->verbose = true;
args->compress = true;
args->logger = [](const std::string& msg) { std::cout << msg; };
args->onProgressCallback = [](int current, int total) -> bool {
std::cerr << "\r"
<< "Downloaded " << current << " bytes out of " << total;
return true;
};
std::atomic<bool> requestCompleted(false);
std::atomic<HttpErrorCode> errorCode(HttpErrorCode::Invalid);
httpClient.performRequest(
args, [&requestCompleted, &errorCode](const HttpResponsePtr& response) {
errorCode = response->errorCode;
requestCompleted = true;
});
// cancel immediately
args->cancel = true;
int wait = 0;
while (wait < 5000)
{
if (requestCompleted) break;
std::chrono::duration<double, std::milli> duration(10);
std::this_thread::sleep_for(duration);
wait += 10;
}
std::cerr << "Done" << std::endl;
REQUIRE(errorCode == HttpErrorCode::Cancelled);
}
} }