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:
		@@ -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.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user