From 66cd29e747a125240064374f8f70d8c1be92a8f6 Mon Sep 17 00:00:00 2001 From: Martin Natano Date: Tue, 21 Dec 2021 08:01:55 +0100 Subject: [PATCH] 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; --- docs/usage.md | 3 ++ ixwebsocket/IXHttp.h | 3 ++ ixwebsocket/IXHttpClient.cpp | 33 ++++++++++++++-------- test/IXHttpClientTest.cpp | 53 ++++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 11 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 62a7adf2..ea4116bd 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -517,6 +517,9 @@ bool ok = httpClient.performRequest(args, [](const HttpResponsePtr& response) ); // 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. diff --git a/ixwebsocket/IXHttp.h b/ixwebsocket/IXHttp.h index bfdaefcc..78293c7f 100644 --- a/ixwebsocket/IXHttp.h +++ b/ixwebsocket/IXHttp.h @@ -8,6 +8,7 @@ #include "IXProgressCallback.h" #include "IXWebSocketHttpHeaders.h" +#include #include #include @@ -30,6 +31,7 @@ namespace ix TooManyRedirects = 12, ChunkReadError = 13, CannotReadBody = 14, + Cancelled = 15, Invalid = 100 }; @@ -87,6 +89,7 @@ namespace ix bool compressRequest = false; Logger logger; OnProgressCallback onProgressCallback; + std::atomic cancel; }; using HttpRequestArgsPtr = std::shared_ptr; diff --git a/ixwebsocket/IXHttpClient.cpp b/ixwebsocket/IXHttpClient.cpp index 3be8b8b7..bb876db7 100644 --- a/ixwebsocket/IXHttpClient.cpp +++ b/ixwebsocket/IXHttpClient.cpp @@ -241,17 +241,21 @@ namespace ix std::string errMsg; // Make a cancellation object dealing with connection timeout - auto isCancellationRequested = - makeCancellationRequestWithTimeout(args->connectTimeout, _stop); + auto cancelled = makeCancellationRequestWithTimeout(args->connectTimeout, args->cancel); + + auto isCancellationRequested = [&]() { + return cancelled() || _stop; + }; bool success = _socket->connect(host, port, errMsg, isCancellationRequested); if (!success) { + auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::CannotConnect; std::stringstream ss; ss << "Cannot connect to url: " << url << " / error : " << errMsg; return std::make_shared(code, description, - HttpErrorCode::CannotConnect, + errorCode, headers, payload, ss.str(), @@ -260,7 +264,7 @@ namespace ix } // Make a new cancellation object dealing with transfer timeout - isCancellationRequested = makeCancellationRequestWithTimeout(args->transferTimeout, _stop); + cancelled = makeCancellationRequestWithTimeout(args->transferTimeout, args->cancel); if (args->verbose) { @@ -277,10 +281,11 @@ namespace ix if (!_socket->writeBytes(req, isCancellationRequested)) { + auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::SendError; std::string errorMsg("Cannot send request"); return std::make_shared(code, description, - HttpErrorCode::SendError, + errorCode, headers, payload, errorMsg, @@ -296,10 +301,11 @@ namespace ix if (!lineValid) { + auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::CannotReadStatusLine; std::string errorMsg("Cannot retrieve status line"); return std::make_shared(code, description, - HttpErrorCode::CannotReadStatusLine, + errorCode, headers, payload, errorMsg, @@ -333,10 +339,11 @@ namespace ix if (!headersValid) { + auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::HeaderParsingError; std::string errorMsg("Cannot parse http headers"); return std::make_shared(code, description, - HttpErrorCode::HeaderParsingError, + errorCode, headers, payload, errorMsg, @@ -405,10 +412,11 @@ namespace ix contentLength, args->onProgressCallback, isCancellationRequested); if (!chunkResult.first) { + auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::ChunkReadError; errorMsg = "Cannot read chunk"; return std::make_shared(code, description, - HttpErrorCode::ChunkReadError, + errorCode, headers, payload, errorMsg, @@ -424,6 +432,7 @@ namespace ix while (true) { + auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::ChunkReadError; lineResult = _socket->readLine(isCancellationRequested); line = lineResult.second; @@ -431,7 +440,7 @@ namespace ix { return std::make_shared(code, description, - HttpErrorCode::ChunkReadError, + errorCode, headers, payload, errorMsg, @@ -458,10 +467,11 @@ namespace ix (size_t) chunkSize, args->onProgressCallback, isCancellationRequested); if (!chunkResult.first) { + auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::ChunkReadError; errorMsg = "Cannot read chunk"; return std::make_shared(code, description, - HttpErrorCode::ChunkReadError, + errorCode, headers, payload, errorMsg, @@ -475,9 +485,10 @@ namespace ix if (!lineResult.first) { + auto errorCode = args->cancel ? HttpErrorCode::Cancelled : HttpErrorCode::ChunkReadError; return std::make_shared(code, description, - HttpErrorCode::ChunkReadError, + errorCode, headers, payload, errorMsg, diff --git a/test/IXHttpClientTest.cpp b/test/IXHttpClientTest.cpp index 735dcdb1..722f2f8c 100644 --- a/test/IXHttpClientTest.cpp +++ b/test/IXHttpClientTest.cpp @@ -221,4 +221,57 @@ TEST_CASE("http_client", "[http]") REQUIRE(statusCode1 == 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 requestCompleted(false); + std::atomic 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 duration(10); + std::this_thread::sleep_for(duration); + wait += 10; + } + + std::cerr << "Done" << std::endl; + REQUIRE(errorCode == HttpErrorCode::Cancelled); + } }