├── .clang-format ├── .github └── FUNDING.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── http.pri ├── http.pro ├── marketplace.json └── src ├── cachedhttp.cpp ├── cachedhttp.h ├── http.cpp ├── http.h ├── httpreply.cpp ├── httpreply.h ├── httprequest.h ├── localcache.cpp ├── localcache.h ├── networkhttpreply.cpp ├── networkhttpreply.h ├── throttledhttp.cpp └── throttledhttp.h /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | IndentWidth: 4 3 | AccessModifierOffset: -4 4 | ColumnLimit: 100 5 | AllowShortIfStatementsOnASingleLine: true 6 | AllowShortFunctionsOnASingleLine: Inline 7 | KeepEmptyLinesAtTheStartOfBlocks: false 8 | ContinuationIndentWidth: 8 9 | AlignAfterOpenBracket: true 10 | BinPackParameters: false 11 | AllowAllParametersOfDeclarationOnNextLine: false 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: flaviotordini 2 | custom: https://flavio.tordini.org/donate 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | cmake-build-debug 3 | *.user 4 | .DS_Store 5 | build 6 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | project(http LANGUAGES CXX) 3 | 4 | set(CMAKE_CXX_STANDARD 14) 5 | set(CMAKE_AUTOMOC ON) 6 | 7 | find_package(Qt5 REQUIRED COMPONENTS Core Network) 8 | 9 | add_library(http 10 | src/cachedhttp.h 11 | src/cachedhttp.cpp 12 | src/http.h 13 | src/http.cpp 14 | src/httpreply.h 15 | src/httprequest.h 16 | src/httpreply.cpp 17 | src/localcache.h 18 | src/localcache.cpp 19 | src/networkhttpreply.h 20 | src/networkhttpreply.cpp 21 | src/throttledhttp.h 22 | src/throttledhttp.cpp 23 | ) 24 | 25 | target_link_libraries(http Qt5::Network) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Flavio Tordini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # A wrapper for the Qt Network Access API 6 | 7 | This is just a wrapper around Qt's QNetworkAccessManager and friends. I use it in my apps at https://flavio.tordini.org . It has a simpler, higher-level API and some functionality not found in Qt: 8 | 9 | - Throttling (as required by many web APIs nowadays) 10 | - Automatic retries 11 | - User agent and request header defaults 12 | - Partial requests 13 | - Easier POST requests 14 | - Read timeouts (don't let your requests get stuck forever). (now supported by Qt >= 5.15) 15 | - Redirection support (now supported by Qt >= 5.6) 16 | - Disk-based cache implementation similar to Qt's but not strictly a HTTP cache, i.e. it ignores HTTP headers. This is good if want to cache successful requests irrespective of what the origin server says you should do. The cache also fallbacks to stale content when the server returns an error. 17 | 18 | ## Design 19 | 20 | This library uses the [Decorator design pattern](https://en.wikipedia.org/wiki/Decorator_pattern) to modularize features and make it easy to add them and use them as needed. The main class is [Http](https://github.com/flaviotordini/http/blob/master/src/http.h), which implements the base features of a HTTP client. More specialized classes are: 21 | 22 | - [CachedHttp](https://github.com/flaviotordini/http/blob/master/src/cachedhttp.h), a simple disk-based cache 23 | - [ThrottledHttp](https://github.com/flaviotordini/http/blob/master/src/throttledhttp.h), implements request throttling (aka limiting) 24 | 25 | The constructor of these classes takes another Http instance for which they will act as a proxy. (See examples below). Following this design you can create your own Http subclass. For example, a different caching mechanism, an event dispatcher, custom request logging, etc. 26 | 27 | 28 | ## Build Instructions 29 | In order to build this library you can use either `qmake` or `cmake`. 30 | 31 | ### qmake 32 | ```shell 33 | mkdir build 34 | cd build 35 | qmake .. 36 | make 37 | ``` 38 | 39 | ### CMake 40 | ```shell 41 | mkdir build 42 | cd build 43 | cmake .. 44 | make 45 | ``` 46 | 47 | ## Integration 48 | 49 | You can use this library as a git submodule. For example, add it to your project inside a lib subdirectory: 50 | 51 | ```shell 52 | git submodule add -b master https://github.com/flaviotordini/http lib/http 53 | ``` 54 | 55 | Then you can update your git submodules like this: 56 | 57 | ```shell 58 | git submodule update --init --recursive --remote 59 | ``` 60 | 61 | To integrate the library in your qmake based project just add this to your .pro file: 62 | 63 | ```qmake 64 | include(lib/http/http.pri) 65 | ``` 66 | 67 | qmake builds all object files in the same directory. In order to avoid filename clashes use: 68 | 69 | ```qmake 70 | CONFIG += object_parallel_to_source 71 | ``` 72 | 73 | If you are using CMake you can integrate the library by adding the following lines to your CMakeLists.txt: 74 | 75 | ```cmake 76 | add_subdirectory(lib/http) 77 | ... 78 | target_link_library(your_super_cool_project http) 79 | ``` 80 | or if you have installed http you can find it via: 81 | 82 | ```cmake 83 | find_library(http REQUIRED) 84 | ... 85 | target_link_library(your_super_cool_project http) 86 | ``` 87 | 88 | 89 | ## Examples 90 | 91 | A basic C++14 example: 92 | 93 | ```cpp 94 | #include "http.h" 95 | 96 | auto reply = Http::instance().get("https://flavio.tordini.org/"); 97 | connect(reply, &HttpReply::finished, this, [](auto &reply) { 98 | if (reply.isSuccessful()) { 99 | qDebug() << "Feel the bytes!" << reply.body(); 100 | } else { 101 | qDebug() << "Something's wrong here" << reply.statusCode() << reply.reasonPhrase(); 102 | } 103 | }); 104 | ``` 105 | 106 | It is important to use the reply object passed by the `finished` signal as it may be a different object than the one used to make the request. For example it may be created by `CacheHttp` or other specialized `Http` subclasses. 107 | 108 | Example using two separate signals for success and failure: 109 | ```cpp 110 | #include "http.h" 111 | 112 | auto reply = Http::instance().get("https://google.com/"); 113 | connect(reply, &HttpReply::data, this, [](auto &bytes) { 114 | qDebug() << "Feel the bytes!" << bytes; 115 | }); 116 | connect(reply, &HttpReply::error, this, [](auto &msg) { 117 | qDebug() << "Something's wrong here" << msg; 118 | }); 119 | ``` 120 | 121 | A POST example taking advantage of C++11 initializer lists: 122 | 123 | ```cpp 124 | QUrl url("https://some.domain.url/"); 125 | auto reply = Http::instance().post(url, {{"myparam", "paramvalue"}, {"otherparam", "foo"}}); 126 | connect(reply, &HttpReply::finished, this, [](auto &reply) { 127 | if (reply.isSuccessful()) { 128 | qDebug() << "Feel the bytes!" << reply.body(); 129 | } else { 130 | qDebug() << "Something's wrong here" << reply.statusCode() << reply.reasonPhrase(); 131 | } 132 | }); 133 | ``` 134 | 135 | This is a real-world example of building a Http object with more complex features. It throttles requests, uses a custom user agent and caches results: 136 | 137 | ```cpp 138 | #include "http.h" 139 | #include "cachedhttp.h" 140 | #include "throttledhttp.h" 141 | 142 | Http &myHttp() { 143 | static Http *http = [] { 144 | auto http = new Http; 145 | http->addRequestHeader("User-Agent", "MyUserAgent"); 146 | 147 | auto throttledHttp = new ThrottledHttp(*http); 148 | throttledHttp->setMilliseconds(1000); 149 | 150 | auto cachedHttp = new CachedHttp(*throttledHttp, "mycache"); 151 | cachedHttp->setMaxSeconds(86400 * 30); 152 | 153 | return cachedHttp; 154 | }(); 155 | return *http; 156 | } 157 | ``` 158 | 159 | If the full power (and complexity) of QNetworkReply is needed you can always fallback to it: 160 | 161 | ```cpp 162 | #include "http.h" 163 | 164 | HttpRequest req; 165 | req.url = "https://flavio.tordini.org/"; 166 | QNetworkReply *reply = Http::instance().networkReply(req); 167 | // Use QNetworkReply as needed... 168 | ``` 169 | 170 | Note that features like redirection, retries and read timeouts won't work in this mode. 171 | 172 | ## License 173 | 174 | You can use this library under the MIT license and at your own risk. If you do, you're welcome contributing your changes and fixes. 175 | -------------------------------------------------------------------------------- /http.pri: -------------------------------------------------------------------------------- 1 | QT *= network 2 | 3 | INCLUDEPATH += $$PWD/src 4 | DEFINES += HTTP 5 | 6 | HEADERS += \ 7 | $$PWD/src/cachedhttp.h \ 8 | $$PWD/src/http.h \ 9 | $$PWD/src/httpreply.h \ 10 | $$PWD/src/httprequest.h \ 11 | $$PWD/src/localcache.h \ 12 | $$PWD/src/networkhttpreply.h \ 13 | $$PWD/src/throttledhttp.h 14 | 15 | SOURCES += \ 16 | $$PWD/src/cachedhttp.cpp \ 17 | $$PWD/src/http.cpp \ 18 | $$PWD/src/httpreply.cpp \ 19 | $$PWD/src/localcache.cpp \ 20 | $$PWD/src/networkhttpreply.cpp \ 21 | $$PWD/src/throttledhttp.cpp 22 | -------------------------------------------------------------------------------- /http.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = lib 2 | include(http.pri) 3 | -------------------------------------------------------------------------------- /marketplace.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://qt.io/schema/extension-schema-v1#", 3 | "title": "Http", 4 | "extensionType": [ 5 | "library" 6 | ], 7 | "version": "1", 8 | "vendor": { 9 | "name": "Flavio Tordini", 10 | "url": "https://flavio.tordini.org" 11 | }, 12 | "contact": "Flavio Tordini ", 13 | "copyright": [ 14 | "Flavio Tordini" 15 | ], 16 | "author": "Flavio Tordini", 17 | "icon": "https://flavio.tordini.org/favicon-196x196.png", 18 | "licenses": [ 19 | { 20 | "licenseType": "MIT", 21 | "licenseUrl": "https://opensource.org/licenses/MIT" 22 | } 23 | ], 24 | "created": "2016-07-02", 25 | "platforms": [ 26 | "Windows", 27 | "Linux", 28 | "macOS", 29 | "Android", 30 | "iOS" 31 | ], 32 | "qtVersions": [ 33 | "5.10.0-or-later" 34 | ], 35 | "tags": [ 36 | "http,web,rest,networking,freeproduct,tools,utility" 37 | ], 38 | "price": { 39 | "listprice": 0 40 | }, 41 | "support": "flavio.tordini@gmail.com", 42 | "bugUrl": "https://github.com/flaviotordini/http/issues", 43 | "sourceRepoUrl": "https://github.com/flaviotordini/http", 44 | "userManuals": [ 45 | "https://github.com/flaviotordini/http/blob/master/README.md" 46 | ], 47 | "dependencies": [ 48 | "Network" 49 | ] 50 | } -------------------------------------------------------------------------------- /src/cachedhttp.cpp: -------------------------------------------------------------------------------- 1 | #include "cachedhttp.h" 2 | #include "localcache.h" 3 | #include 4 | 5 | CachedHttpReply::CachedHttpReply(const QByteArray &body, const QUrl &url, bool autoSignals) 6 | : bytes(body), requestUrl(url) { 7 | if (autoSignals) QTimer::singleShot(0, this, SLOT(emitSignals())); 8 | } 9 | 10 | QByteArray CachedHttpReply::body() const { 11 | return bytes; 12 | } 13 | 14 | void CachedHttpReply::emitSignals() { 15 | emit data(body()); 16 | emit finished(*this); 17 | deleteLater(); 18 | } 19 | 20 | WrappedHttpReply::WrappedHttpReply(CachedHttp &cachedHttp, 21 | LocalCache *cache, 22 | const QByteArray &key, 23 | HttpReply *httpReply) 24 | : HttpReply(httpReply), cachedHttp(cachedHttp), cache(cache), key(key), httpReply(httpReply) { 25 | connect(httpReply, &HttpReply::finished, this, &WrappedHttpReply::originFinished); 26 | } 27 | 28 | void WrappedHttpReply::originFinished(const HttpReply &reply) { 29 | qDebug() << reply.statusCode() << reply.url(); 30 | bool success = reply.isSuccessful(); 31 | if (!success) { 32 | // Fallback to stale cached data on HTTP error 33 | const QByteArray value = cache->possiblyStaleValue(key); 34 | if (!value.isNull()) { 35 | qDebug() << "Using stale cache value" << reply.url(); 36 | emit data(value); 37 | auto replyFromCache = new CachedHttpReply(value, reply.url(), false); 38 | emit finished(*replyFromCache); 39 | replyFromCache->deleteLater(); 40 | return; 41 | } 42 | } 43 | 44 | bool doCache = success; 45 | if (doCache) { 46 | const auto &validators = cachedHttp.getValidators(); 47 | if (!validators.isEmpty()) { 48 | const QByteArray &mime = reply.header("Content-Type"); 49 | auto i = validators.constFind(mime); 50 | if (i != validators.constEnd()) { 51 | auto validator = i.value(); 52 | doCache = validator(reply); 53 | } else { 54 | i = validators.constFind("*"); 55 | if (i != validators.constEnd()) { 56 | auto validator = i.value(); 57 | doCache = validator(reply); 58 | } 59 | } 60 | } 61 | } 62 | 63 | if (doCache) 64 | cache->insert(key, reply.body()); 65 | else 66 | qDebug() << "Not caching" << reply.statusCode() << reply.url(); 67 | 68 | if (success) { 69 | emit data(reply.body()); 70 | } else { 71 | emit error(reply.reasonPhrase()); 72 | } 73 | emit finished(reply); 74 | } 75 | 76 | CachedHttp::CachedHttp(Http &http, const char *name) 77 | : http(http), cache(LocalCache::instance(name)), cachePostRequests(false) {} 78 | 79 | void CachedHttp::setMaxSeconds(uint seconds) { 80 | cache->setMaxSeconds(seconds); 81 | } 82 | 83 | void CachedHttp::setMaxSize(uint maxSize) { 84 | cache->setMaxSize(maxSize); 85 | } 86 | 87 | HttpReply *CachedHttp::request(const HttpRequest &req) { 88 | bool cacheable = req.operation == QNetworkAccessManager::GetOperation || 89 | (cachePostRequests && req.operation == QNetworkAccessManager::PostOperation); 90 | if (!cacheable) { 91 | qDebug() << "Not cacheable" << req.url; 92 | return http.request(req); 93 | } 94 | const QByteArray key = requestHash(req); 95 | const QByteArray value = cache->value(key); 96 | if (!value.isNull()) { 97 | // qDebug() << "HIT" << key << req.url; 98 | return new CachedHttpReply(value, req.url); 99 | } 100 | // qDebug() << "MISS" << key << req.url; 101 | return new WrappedHttpReply(*this, cache, key, http.request(req)); 102 | } 103 | 104 | QByteArray CachedHttp::requestHash(const HttpRequest &req) { 105 | const char sep = '|'; 106 | 107 | QByteArray s; 108 | if (ignoreHostname) { 109 | s = (req.url.scheme() + sep + req.url.path() + sep + req.url.query()).toUtf8(); 110 | } else { 111 | s = req.url.toEncoded(); 112 | } 113 | s += sep + req.body + sep + QByteArray::number(req.offset); 114 | if (req.operation == QNetworkAccessManager::PostOperation) { 115 | s.append(sep); 116 | s.append("POST"); 117 | } 118 | return LocalCache::hash(s); 119 | } 120 | -------------------------------------------------------------------------------- /src/cachedhttp.h: -------------------------------------------------------------------------------- 1 | #ifndef CACHEDHTTP_H 2 | #define CACHEDHTTP_H 3 | 4 | #include "http.h" 5 | 6 | class LocalCache; 7 | 8 | class CachedHttp : public Http { 9 | public: 10 | CachedHttp(Http &http = Http::instance(), const char *name = "http"); 11 | void setMaxSeconds(uint seconds); 12 | void setMaxSize(uint maxSize); 13 | void setCachePostRequests(bool value) { cachePostRequests = value; } 14 | void setIgnoreHostname(bool value) { ignoreHostname = value; } 15 | auto &getValidators() { return validators; } 16 | HttpReply *request(const HttpRequest &req); 17 | 18 | private: 19 | QByteArray requestHash(const HttpRequest &req); 20 | 21 | Http &http; 22 | LocalCache *cache; 23 | bool cachePostRequests; 24 | bool ignoreHostname = false; 25 | 26 | /// Mapping is MIME -> validating function 27 | /// Use * as MIME to define a catch-all validator 28 | QMap> validators; 29 | }; 30 | 31 | class CachedHttpReply : public HttpReply { 32 | Q_OBJECT 33 | 34 | public: 35 | CachedHttpReply(const QByteArray &body, const QUrl &url, bool autoSignals = true); 36 | QUrl url() const { return requestUrl; } 37 | int statusCode() const { return 200; } 38 | QByteArray body() const; 39 | 40 | private slots: 41 | void emitSignals(); 42 | 43 | private: 44 | const QByteArray bytes; 45 | const QUrl requestUrl; 46 | }; 47 | 48 | class WrappedHttpReply : public HttpReply { 49 | Q_OBJECT 50 | 51 | public: 52 | WrappedHttpReply(CachedHttp &cachedHttp, 53 | LocalCache *cache, 54 | const QByteArray &key, 55 | HttpReply *httpReply); 56 | QUrl url() const { return httpReply->url(); } 57 | int statusCode() const { return httpReply->statusCode(); } 58 | QByteArray body() const { return httpReply->body(); } 59 | 60 | private slots: 61 | void originFinished(const HttpReply &reply); 62 | 63 | private: 64 | CachedHttp &cachedHttp; 65 | LocalCache *cache; 66 | QByteArray key; 67 | HttpReply *httpReply; 68 | }; 69 | 70 | #endif // CACHEDHTTP_H 71 | -------------------------------------------------------------------------------- /src/http.cpp: -------------------------------------------------------------------------------- 1 | #include "http.h" 2 | 3 | #include "networkhttpreply.h" 4 | 5 | namespace { 6 | 7 | QNetworkAccessManager *networkAccessManager() { 8 | static thread_local QNetworkAccessManager *nam = [] { 9 | auto nam = new QNetworkAccessManager(); 10 | #if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) 11 | nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); 12 | #endif 13 | return nam; 14 | }(); 15 | return nam; 16 | } 17 | 18 | int defaultReadTimeout = 10000; 19 | int defaultMaxRetries = 3; 20 | } // namespace 21 | 22 | Http::Http() 23 | : requestHeaders(getDefaultRequestHeaders()), readTimeout(defaultReadTimeout), 24 | maxRetries(defaultMaxRetries) {} 25 | 26 | void Http::setRequestHeaders(const QMap &headers) { 27 | requestHeaders = headers; 28 | } 29 | 30 | QMap &Http::getRequestHeaders() { 31 | return requestHeaders; 32 | } 33 | 34 | void Http::addRequestHeader(const QByteArray &name, const QByteArray &value) { 35 | requestHeaders.insert(name, value); 36 | } 37 | 38 | void Http::setReadTimeout(int timeout) { 39 | readTimeout = timeout; 40 | } 41 | 42 | Http &Http::instance() { 43 | static Http i; 44 | return i; 45 | } 46 | 47 | const QMap &Http::getDefaultRequestHeaders() { 48 | static const QMap defaultRequestHeaders = [] { 49 | QMap h; 50 | h.insert("Accept-Charset", "utf-8"); 51 | h.insert("Connection", "Keep-Alive"); 52 | return h; 53 | }(); 54 | return defaultRequestHeaders; 55 | } 56 | 57 | void Http::setDefaultReadTimeout(int timeout) { 58 | defaultReadTimeout = timeout; 59 | } 60 | 61 | QNetworkReply *Http::networkReply(const HttpRequest &req) { 62 | QNetworkRequest request(req.url); 63 | 64 | QMap &headers = requestHeaders; 65 | if (!req.headers.isEmpty()) headers = req.headers; 66 | 67 | QMap::const_iterator it; 68 | for (it = headers.constBegin(); it != headers.constEnd(); ++it) 69 | request.setRawHeader(it.key(), it.value()); 70 | 71 | if (req.offset > 0) 72 | request.setRawHeader("Range", QStringLiteral("bytes=%1-").arg(req.offset).toUtf8()); 73 | 74 | QNetworkAccessManager *manager = networkAccessManager(); 75 | 76 | QNetworkReply *networkReply = nullptr; 77 | switch (req.operation) { 78 | case QNetworkAccessManager::GetOperation: 79 | networkReply = manager->get(request); 80 | break; 81 | 82 | case QNetworkAccessManager::HeadOperation: 83 | networkReply = manager->head(request); 84 | break; 85 | 86 | case QNetworkAccessManager::PostOperation: 87 | networkReply = manager->post(request, req.body); 88 | break; 89 | 90 | case QNetworkAccessManager::PutOperation: 91 | networkReply = manager->put(request, req.body); 92 | break; 93 | 94 | case QNetworkAccessManager::DeleteOperation: 95 | networkReply = manager->deleteResource(request); 96 | break; 97 | 98 | default: 99 | qWarning() << "Unknown operation:" << req.operation; 100 | } 101 | 102 | return networkReply; 103 | } 104 | 105 | HttpReply *Http::request(const HttpRequest &req) { 106 | return new NetworkHttpReply(req, *this); 107 | } 108 | 109 | HttpReply *Http::request(const QUrl &url, 110 | QNetworkAccessManager::Operation operation, 111 | const QByteArray &body, 112 | uint offset) { 113 | HttpRequest req; 114 | req.url = url; 115 | req.operation = operation; 116 | req.body = body; 117 | req.offset = offset; 118 | return request(req); 119 | } 120 | 121 | HttpReply *Http::get(const QUrl &url) { 122 | return request(url, QNetworkAccessManager::GetOperation); 123 | } 124 | 125 | HttpReply *Http::head(const QUrl &url) { 126 | return request(url, QNetworkAccessManager::HeadOperation); 127 | } 128 | 129 | HttpReply *Http::post(const QUrl &url, const QMap ¶ms) { 130 | QByteArray body; 131 | QMapIterator i(params); 132 | while (i.hasNext()) { 133 | i.next(); 134 | body += QUrl::toPercentEncoding(i.key()) + '=' + QUrl::toPercentEncoding(i.value()) + '&'; 135 | } 136 | HttpRequest req; 137 | req.url = url; 138 | req.operation = QNetworkAccessManager::PostOperation; 139 | req.body = body; 140 | req.headers = requestHeaders; 141 | req.headers.insert("Content-Type", "application/x-www-form-urlencoded"); 142 | return request(req); 143 | } 144 | 145 | HttpReply *Http::post(const QUrl &url, const QByteArray &body, const QByteArray &contentType) { 146 | HttpRequest req; 147 | req.url = url; 148 | req.operation = QNetworkAccessManager::PostOperation; 149 | req.body = body; 150 | req.headers = requestHeaders; 151 | QByteArray cType = contentType; 152 | if (cType.isEmpty()) cType = "application/x-www-form-urlencoded"; 153 | req.headers.insert("Content-Type", cType); 154 | return request(req); 155 | } 156 | 157 | 158 | HttpReply *Http::put(const QUrl &url, const QByteArray &body, const QByteArray &contentType) { 159 | HttpRequest req; 160 | req.url = url; 161 | req.operation = QNetworkAccessManager::PutOperation; 162 | req.body = body; 163 | req.headers = requestHeaders; 164 | QByteArray cType = contentType; 165 | if (cType.isEmpty()) cType = "application/x-www-form-urlencoded"; 166 | req.headers.insert("Content-Type", cType); 167 | return request(req); 168 | } 169 | 170 | 171 | HttpReply *Http::deleteResource(const QUrl &url) { 172 | HttpRequest req; 173 | req.url = url; 174 | req.operation = QNetworkAccessManager::DeleteOperation; 175 | req.headers = requestHeaders; 176 | return request(req); 177 | } 178 | 179 | int Http::getMaxRetries() const { 180 | return maxRetries; 181 | } 182 | 183 | void Http::setMaxRetries(int value) { 184 | maxRetries = value; 185 | } 186 | -------------------------------------------------------------------------------- /src/http.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTP_H 2 | #define HTTP_H 3 | 4 | #include 5 | 6 | #include "httpreply.h" 7 | #include "httprequest.h" 8 | 9 | class Http { 10 | public: 11 | static Http &instance(); 12 | static const QMap &getDefaultRequestHeaders(); 13 | static void setDefaultReadTimeout(int timeout); 14 | 15 | Http(); 16 | 17 | void setRequestHeaders(const QMap &headers); 18 | QMap &getRequestHeaders(); 19 | void addRequestHeader(const QByteArray &name, const QByteArray &value); 20 | 21 | void setReadTimeout(int timeout); 22 | int getReadTimeout() { return readTimeout; } 23 | 24 | int getMaxRetries() const; 25 | void setMaxRetries(int value); 26 | 27 | QNetworkReply *networkReply(const HttpRequest &req); 28 | virtual HttpReply *request(const HttpRequest &req); 29 | HttpReply * 30 | request(const QUrl &url, 31 | QNetworkAccessManager::Operation operation = QNetworkAccessManager::GetOperation, 32 | const QByteArray &body = QByteArray(), 33 | uint offset = 0); 34 | HttpReply *get(const QUrl &url); 35 | HttpReply *head(const QUrl &url); 36 | HttpReply *post(const QUrl &url, const QMap ¶ms); 37 | HttpReply *post(const QUrl &url, const QByteArray &body, const QByteArray &contentType); 38 | HttpReply *put(const QUrl &url, const QByteArray &body, const QByteArray &contentType); 39 | HttpReply *deleteResource(const QUrl &url); 40 | 41 | private: 42 | QMap requestHeaders; 43 | int readTimeout; 44 | int maxRetries; 45 | }; 46 | 47 | #endif // HTTP_H 48 | -------------------------------------------------------------------------------- /src/httpreply.cpp: -------------------------------------------------------------------------------- 1 | #include "httpreply.h" 2 | 3 | HttpReply::HttpReply(QObject *parent) : QObject(parent) {} 4 | 5 | int HttpReply::isSuccessful() const { 6 | return statusCode() >= 200 && statusCode() < 300; 7 | } 8 | 9 | QString HttpReply::reasonPhrase() const { 10 | return QString(); 11 | } 12 | 13 | const QList HttpReply::headers() const { 14 | return QList(); 15 | } 16 | 17 | QByteArray HttpReply::header(const QByteArray &headerName) const { 18 | Q_UNUSED(headerName); 19 | return QByteArray(); 20 | } 21 | -------------------------------------------------------------------------------- /src/httpreply.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTPREPLY_H 2 | #define HTTPREPLY_H 3 | 4 | #include 5 | 6 | class HttpReply : public QObject { 7 | Q_OBJECT 8 | 9 | public: 10 | HttpReply(QObject *parent = nullptr); 11 | virtual QUrl url() const = 0; 12 | virtual int statusCode() const = 0; 13 | int isSuccessful() const; 14 | virtual QString reasonPhrase() const; 15 | virtual const QList headers() const; 16 | virtual QByteArray header(const QByteArray &headerName) const; 17 | virtual QByteArray body() const = 0; 18 | 19 | template HttpReply &onData(Functor lambda) { 20 | connect(this, &HttpReply::data, this, lambda); 21 | return *this; 22 | } 23 | template HttpReply &onError(Functor lambda) { 24 | connect(this, &HttpReply::error, this, lambda); 25 | return *this; 26 | } 27 | template HttpReply &onFinished(Functor lambda) { 28 | connect(this, &HttpReply::finished, this, lambda); 29 | return *this; 30 | } 31 | 32 | signals: 33 | void data(const QByteArray &bytes); 34 | void error(const QString &message); 35 | void finished(const HttpReply &reply); 36 | }; 37 | 38 | #endif // HTTPREPLY_H 39 | -------------------------------------------------------------------------------- /src/httprequest.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTPREQUEST_H 2 | #define HTTPREQUEST_H 3 | 4 | #include 5 | 6 | class HttpRequest { 7 | public: 8 | HttpRequest() : operation(QNetworkAccessManager::GetOperation), offset(0) {} 9 | QUrl url; 10 | QNetworkAccessManager::Operation operation; 11 | QByteArray body; 12 | uint offset; 13 | QMap headers; 14 | }; 15 | 16 | #endif // HTTPREQUEST_H 17 | -------------------------------------------------------------------------------- /src/localcache.cpp: -------------------------------------------------------------------------------- 1 | #include "localcache.h" 2 | 3 | LocalCache *LocalCache::instance(const char *name) { 4 | static QMap instances; 5 | auto i = instances.constFind(QByteArray::fromRawData(name, strlen(name))); 6 | if (i != instances.constEnd()) return i.value(); 7 | LocalCache *instance = new LocalCache(name); 8 | instances.insert(instance->getName(), instance); 9 | return instance; 10 | } 11 | 12 | LocalCache::LocalCache(const QByteArray &name) 13 | : name(name), maxSeconds(86400 * 30), maxSize(1024 * 1024 * 100), size(0), insertCount(0) { 14 | directory = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1Char('/') + 15 | QLatin1String(name) + QLatin1Char('/'); 16 | #ifndef QT_NO_DEBUG_OUTPUT 17 | hits = 0; 18 | misses = 0; 19 | #endif 20 | } 21 | 22 | LocalCache::~LocalCache() { 23 | #ifndef QT_NO_DEBUG_OUTPUT 24 | debugStats(); 25 | #endif 26 | } 27 | 28 | QByteArray LocalCache::hash(const QByteArray &s) { 29 | QCryptographicHash hash(QCryptographicHash::Sha1); 30 | hash.addData(s); 31 | const QByteArray h = QByteArray::number(*(qlonglong *)hash.result().constData(), 36); 32 | static const char sep('/'); 33 | QByteArray p; 34 | p.reserve(h.length() + 2); 35 | p.append(h.at(0)); 36 | p.append(sep); 37 | p.append(h.at(1)); 38 | p.append(sep); 39 | p.append(h.constData() + 2, strlen(h.constData()) - 2); // p.append(h.mid(2)); 40 | return p; 41 | } 42 | 43 | bool LocalCache::isCached(const QString &path) { 44 | bool cached = QFile::exists(path) && 45 | (maxSeconds == 0 || QFileInfo(path).birthTime().secsTo( 46 | QDateTime::currentDateTimeUtc()) < maxSeconds); 47 | #ifndef QT_NO_DEBUG_OUTPUT 48 | if (!cached) misses++; 49 | #endif 50 | return cached; 51 | } 52 | 53 | QByteArray LocalCache::value(const QByteArray &key) { 54 | const QString path = cachePath(key); 55 | if (!isCached(path)) return QByteArray(); 56 | 57 | QFile file(path); 58 | if (!file.open(QIODevice::ReadOnly)) { 59 | qWarning() << file.fileName() << file.errorString(); 60 | #ifndef QT_NO_DEBUG_OUTPUT 61 | misses++; 62 | #endif 63 | return QByteArray(); 64 | } 65 | #ifndef QT_NO_DEBUG_OUTPUT 66 | hits++; 67 | #endif 68 | return file.readAll(); 69 | } 70 | 71 | QByteArray LocalCache::possiblyStaleValue(const QByteArray &key) { 72 | const QString path = cachePath(key); 73 | QFile file(path); 74 | if (!file.open(QIODevice::ReadOnly)) { 75 | #ifndef QT_NO_DEBUG_OUTPUT 76 | misses++; 77 | #endif 78 | return QByteArray(); 79 | } 80 | #ifndef QT_NO_DEBUG_OUTPUT 81 | hits++; 82 | #endif 83 | return file.readAll(); 84 | } 85 | 86 | void LocalCache::insert(const QByteArray &key, const QByteArray &value) { 87 | // qDebug() << "Inserting" << key; 88 | const QString path = cachePath(key); 89 | const QString parentDir = path.left(path.lastIndexOf(QLatin1Char('/'))); 90 | if (!QFile::exists(parentDir)) { 91 | QDir().mkpath(parentDir); 92 | } 93 | QFile file(path); 94 | if (!file.open(QIODevice::WriteOnly)) { 95 | qWarning() << "Cannot create" << path; 96 | return; 97 | } 98 | file.write(value); 99 | file.close(); 100 | if (size > 0) size += value.size(); 101 | 102 | // expire cache every n inserts 103 | if (maxSize > 0 && ++insertCount % 100 == 0) { 104 | if (size == 0 || size > maxSize) expire(); 105 | } 106 | } 107 | 108 | void LocalCache::clear() { 109 | #ifndef QT_NO_DEBUG_OUTPUT 110 | hits = 0; 111 | misses = 0; 112 | #endif 113 | size = 0; 114 | insertCount = 0; 115 | mutex.lock(); 116 | QDir(directory).removeRecursively(); 117 | mutex.unlock(); 118 | } 119 | 120 | QString LocalCache::cachePath(const QByteArray &key) const { 121 | return directory + QLatin1String(key); 122 | } 123 | 124 | void LocalCache::expire() { 125 | if (!mutex.tryLock()) return; 126 | 127 | QDir::Filters filters = QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot; 128 | QDirIterator it(directory, filters, QDirIterator::Subdirectories); 129 | 130 | QMultiMap cacheItems; 131 | qint64 totalSize = 0; 132 | while (it.hasNext()) { 133 | QString path = it.next(); 134 | QFileInfo info = it.fileInfo(); 135 | cacheItems.insert(info.birthTime(), path); 136 | totalSize += info.size(); 137 | qApp->processEvents(); 138 | } 139 | 140 | int removedFiles = 0; 141 | qint64 goal = (maxSize * 9) / 10; 142 | auto i = cacheItems.constBegin(); 143 | while (i != cacheItems.constEnd()) { 144 | if (totalSize < goal) break; 145 | QString name = i.value(); 146 | QFile file(name); 147 | qint64 size = file.size(); 148 | file.remove(); 149 | totalSize -= size; 150 | ++removedFiles; 151 | ++i; 152 | qApp->processEvents(); 153 | } 154 | #ifndef QT_NO_DEBUG_OUTPUT 155 | debugStats(); 156 | if (removedFiles > 0) { 157 | qDebug() << "Removed:" << removedFiles << "Kept:" << cacheItems.count() - removedFiles 158 | << "New Size:" << totalSize; 159 | } 160 | #endif 161 | 162 | size = totalSize; 163 | mutex.unlock(); 164 | } 165 | 166 | #ifndef QT_NO_DEBUG_OUTPUT 167 | void LocalCache::debugStats() { 168 | int total = hits + misses; 169 | if (total > 0) { 170 | qDebug() << "Cache:" << name << '\n' 171 | << "Inserts:" << insertCount << '\n' 172 | << "Requests:" << total << '\n' 173 | << "Hits:" << hits << (hits * 100) / total << "%\n" 174 | << "Misses:" << misses << (misses * 100) / total << "%"; 175 | } 176 | } 177 | #endif 178 | -------------------------------------------------------------------------------- /src/localcache.h: -------------------------------------------------------------------------------- 1 | #ifndef LOCALCACHE_H 2 | #define LOCALCACHE_H 3 | 4 | #include 5 | 6 | /** 7 | * @brief Not thread-safe 8 | */ 9 | class LocalCache { 10 | public: 11 | static LocalCache *instance(const char *name); 12 | ~LocalCache(); 13 | static QByteArray hash(const QByteArray &s); 14 | 15 | const QByteArray &getName() const { return name; } 16 | 17 | void setMaxSeconds(uint value) { maxSeconds = value; } 18 | void setMaxSize(uint value) { maxSize = value; } 19 | 20 | QByteArray value(const QByteArray &key); 21 | QByteArray possiblyStaleValue(const QByteArray &key); 22 | void insert(const QByteArray &key, const QByteArray &value); 23 | void clear(); 24 | 25 | private: 26 | LocalCache(const QByteArray &name); 27 | QString cachePath(const QByteArray &key) const; 28 | bool isCached(const QString &path); 29 | void expire(); 30 | #ifndef QT_NO_DEBUG_OUTPUT 31 | void debugStats(); 32 | #endif 33 | 34 | QByteArray name; 35 | QString directory; 36 | uint maxSeconds; 37 | qint64 maxSize; 38 | qint64 size; 39 | QMutex mutex; 40 | uint insertCount; 41 | 42 | #ifndef QT_NO_DEBUG_OUTPUT 43 | uint hits; 44 | uint misses; 45 | #endif 46 | }; 47 | 48 | #endif // LOCALCACHE_H 49 | -------------------------------------------------------------------------------- /src/networkhttpreply.cpp: -------------------------------------------------------------------------------- 1 | #include "networkhttpreply.h" 2 | 3 | NetworkHttpReply::NetworkHttpReply(const HttpRequest &req, Http &http) 4 | : http(http), req(req), retryCount(0) { 5 | if (req.url.isEmpty()) { 6 | qWarning() << "Empty URL"; 7 | } 8 | 9 | networkReply = http.networkReply(req); 10 | setParent(networkReply); 11 | setupReply(); 12 | 13 | readTimeoutTimer = new QTimer(this); 14 | readTimeoutTimer->setInterval(http.getReadTimeout()); 15 | readTimeoutTimer->setSingleShot(true); 16 | connect(readTimeoutTimer, &QTimer::timeout, this, &NetworkHttpReply::readTimeout, 17 | Qt::UniqueConnection); 18 | readTimeoutTimer->start(); 19 | } 20 | 21 | void NetworkHttpReply::setupReply() { 22 | #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 23 | connect(networkReply, &QNetworkReply::errorOccurred, this, &NetworkHttpReply::replyError, 24 | Qt::UniqueConnection); 25 | #else 26 | connect(networkReply, SIGNAL(error(QNetworkReply::NetworkError)), 27 | SLOT(replyError(QNetworkReply::NetworkError)), Qt::UniqueConnection); 28 | #endif 29 | connect(networkReply, &QNetworkReply::finished, this, &NetworkHttpReply::replyFinished, 30 | Qt::UniqueConnection); 31 | connect(networkReply, &QNetworkReply::downloadProgress, this, 32 | &NetworkHttpReply::downloadProgress, Qt::UniqueConnection); 33 | } 34 | 35 | QString NetworkHttpReply::errorMessage() { 36 | return url().toString() + QLatin1Char(' ') + QString::number(statusCode()) + QLatin1Char(' ') + 37 | reasonPhrase(); 38 | } 39 | 40 | void NetworkHttpReply::emitError() { 41 | const QString msg = errorMessage(); 42 | #ifndef QT_NO_DEBUG_OUTPUT 43 | qDebug() << "Http:" << msg; 44 | if (!req.body.isEmpty()) qDebug() << "Http:" << req.body; 45 | #endif 46 | emit error(msg); 47 | emitFinished(); 48 | } 49 | 50 | void NetworkHttpReply::emitFinished() { 51 | readTimeoutTimer->stop(); 52 | 53 | // disconnect to avoid replyFinished() from being called 54 | networkReply->disconnect(); 55 | 56 | emit finished(*this); 57 | 58 | // bye bye my reply 59 | // this will also delete this object and HttpReply as the QNetworkReply is their parent 60 | networkReply->deleteLater(); 61 | } 62 | 63 | void NetworkHttpReply::replyFinished() { 64 | #if QT_VERSION < QT_VERSION_CHECK(5, 9, 0) 65 | QUrl redirection = networkReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); 66 | if (redirection.isValid()) { 67 | HttpRequest redirectReq; 68 | if (redirection.isRelative()) redirection = networkReply->url().resolved(redirection); 69 | redirectReq.url = redirection; 70 | qDebug() << "Redirected to" << redirectReq.url; 71 | redirectReq.operation = req.operation; 72 | redirectReq.body = req.body; 73 | redirectReq.offset = req.offset; 74 | QNetworkReply *redirectReply = http.networkReply(redirectReq); 75 | setParent(redirectReply); 76 | networkReply->deleteLater(); 77 | networkReply = redirectReply; 78 | setupReply(); 79 | readTimeoutTimer->start(); 80 | return; 81 | } 82 | #endif 83 | 84 | if (isSuccessful()) { 85 | bytes = networkReply->readAll(); 86 | emit data(bytes); 87 | 88 | #ifndef QT_NO_DEBUG_OUTPUT 89 | if (!networkReply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool()) 90 | qDebug() << statusCode() << networkReply->url().toString(); 91 | else 92 | qDebug() << "CACHE" << networkReply->url().toString(); 93 | #endif 94 | } 95 | 96 | emitFinished(); 97 | } 98 | 99 | void NetworkHttpReply::replyError(QNetworkReply::NetworkError code) { 100 | Q_UNUSED(code); 101 | const int status = statusCode(); 102 | if (retryCount <= http.getMaxRetries() && status >= 500 && status < 600 && 103 | (networkReply->operation() == QNetworkAccessManager::GetOperation || 104 | networkReply->operation() == QNetworkAccessManager::HeadOperation)) { 105 | qDebug() << "Retrying" << status << QVariant(req.operation).toString() << req.url; 106 | networkReply->disconnect(); 107 | networkReply->deleteLater(); 108 | QNetworkReply *retryReply = http.networkReply(req); 109 | setParent(retryReply); 110 | networkReply = retryReply; 111 | setupReply(); 112 | retryCount++; 113 | readTimeoutTimer->start(); 114 | } else { 115 | emitError(); 116 | return; 117 | } 118 | } 119 | 120 | void NetworkHttpReply::downloadProgress(qint64 bytesReceived, qint64 /* bytesTotal */) { 121 | // qDebug() << "Downloading" << bytesReceived << bytesTotal << networkReply->url(); 122 | if (bytesReceived > 0 && readTimeoutTimer->isActive()) { 123 | readTimeoutTimer->stop(); 124 | disconnect(networkReply, &QNetworkReply::downloadProgress, this, 125 | &NetworkHttpReply::downloadProgress); 126 | } 127 | } 128 | 129 | void NetworkHttpReply::readTimeout() { 130 | qDebug() << "Timeout" << req.url; 131 | 132 | if (!networkReply) return; 133 | 134 | bool shouldRetry = (networkReply->operation() == QNetworkAccessManager::GetOperation || 135 | networkReply->operation() == QNetworkAccessManager::HeadOperation) && 136 | retryCount < http.getMaxRetries(); 137 | 138 | networkReply->disconnect(); 139 | networkReply->abort(); 140 | networkReply->deleteLater(); 141 | 142 | if (!shouldRetry) { 143 | emitError(); 144 | emit finished(*this); 145 | return; 146 | } 147 | 148 | retryCount++; 149 | QNetworkReply *retryReply = http.networkReply(req); 150 | setParent(retryReply); 151 | networkReply = retryReply; 152 | setupReply(); 153 | readTimeoutTimer->start(); 154 | } 155 | 156 | QUrl NetworkHttpReply::url() const { 157 | return networkReply->url(); 158 | } 159 | 160 | int NetworkHttpReply::statusCode() const { 161 | return networkReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); 162 | } 163 | 164 | QString NetworkHttpReply::reasonPhrase() const { 165 | return networkReply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); 166 | } 167 | 168 | const QList NetworkHttpReply::headers() const { 169 | return networkReply->rawHeaderPairs(); 170 | } 171 | 172 | QByteArray NetworkHttpReply::header(const QByteArray &headerName) const { 173 | return networkReply->rawHeader(headerName); 174 | } 175 | 176 | QByteArray NetworkHttpReply::body() const { 177 | return bytes; 178 | } 179 | -------------------------------------------------------------------------------- /src/networkhttpreply.h: -------------------------------------------------------------------------------- 1 | #ifndef NETWORKHTTPREPLY_H 2 | #define NETWORKHTTPREPLY_H 3 | 4 | #include 5 | 6 | #include "http.h" 7 | #include "httpreply.h" 8 | #include "httprequest.h" 9 | 10 | class NetworkHttpReply : public HttpReply { 11 | Q_OBJECT 12 | 13 | public: 14 | NetworkHttpReply(const HttpRequest &req, Http &http); 15 | QUrl url() const; 16 | int statusCode() const; 17 | QString reasonPhrase() const; 18 | const QList headers() const; 19 | QByteArray header(const QByteArray &headerName) const; 20 | QByteArray body() const; 21 | 22 | private slots: 23 | void replyFinished(); 24 | void replyError(QNetworkReply::NetworkError); 25 | void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); 26 | void readTimeout(); 27 | 28 | private: 29 | void setupReply(); 30 | QString errorMessage(); 31 | void emitError(); 32 | void emitFinished(); 33 | 34 | Http &http; 35 | HttpRequest req; 36 | QNetworkReply *networkReply; 37 | QTimer *readTimeoutTimer; 38 | int retryCount; 39 | QByteArray bytes; 40 | }; 41 | 42 | #endif // NETWORKHTTPREPLY_H 43 | -------------------------------------------------------------------------------- /src/throttledhttp.cpp: -------------------------------------------------------------------------------- 1 | #include "throttledhttp.h" 2 | 3 | ThrottledHttp::ThrottledHttp(Http &http) : http(http), milliseconds(1000) { 4 | elapsedTimer.start(); 5 | } 6 | 7 | HttpReply *ThrottledHttp::request(const HttpRequest &req) { 8 | return new ThrottledHttpReply(http, req, milliseconds, elapsedTimer); 9 | } 10 | 11 | ThrottledHttpReply::ThrottledHttpReply(Http &http, 12 | const HttpRequest &req, 13 | int milliseconds, 14 | QElapsedTimer &elapsedTimer) 15 | : http(http), req(req), milliseconds(milliseconds), elapsedTimer(elapsedTimer), timer(nullptr) { 16 | checkElapsed(); 17 | } 18 | 19 | void ThrottledHttpReply::checkElapsed() { 20 | /* 21 | static QMutex mutex; 22 | QMutexLocker locker(&mutex); 23 | */ 24 | 25 | const qint64 elapsedSinceLastRequest = elapsedTimer.elapsed(); 26 | if (elapsedSinceLastRequest < milliseconds) { 27 | if (!timer) { 28 | timer = new QTimer(this); 29 | timer->setSingleShot(true); 30 | timer->setTimerType(Qt::PreciseTimer); 31 | connect(timer, &QTimer::timeout, this, &ThrottledHttpReply::checkElapsed); 32 | } 33 | // qDebug() << "Throttling" << req.url << QStringLiteral("%1ms").arg(milliseconds - 34 | // elapsedSinceLastRequest); 35 | timer->setInterval(milliseconds - elapsedSinceLastRequest); 36 | timer->start(); 37 | return; 38 | } 39 | elapsedTimer.start(); 40 | doRequest(); 41 | } 42 | 43 | void ThrottledHttpReply::doRequest() { 44 | auto reply = http.request(req); 45 | connect(reply, &HttpReply::data, this, &HttpReply::data); 46 | connect(reply, &HttpReply::error, this, &HttpReply::error); 47 | connect(reply, &HttpReply::finished, this, &HttpReply::finished); 48 | 49 | // this will cause the deletion of this object once the request is finished 50 | setParent(reply); 51 | } 52 | -------------------------------------------------------------------------------- /src/throttledhttp.h: -------------------------------------------------------------------------------- 1 | #ifndef THROTTLEDHTTP_H 2 | #define THROTTLEDHTTP_H 3 | 4 | #include "http.h" 5 | #include 6 | #include 7 | 8 | class ThrottledHttp : public Http { 9 | public: 10 | ThrottledHttp(Http &http = Http::instance()); 11 | void setMilliseconds(int milliseconds) { this->milliseconds = milliseconds; } 12 | HttpReply *request(const HttpRequest &req); 13 | 14 | private: 15 | Http &http; 16 | int milliseconds; 17 | QElapsedTimer elapsedTimer; 18 | }; 19 | 20 | class ThrottledHttpReply : public HttpReply { 21 | Q_OBJECT 22 | 23 | public: 24 | ThrottledHttpReply(Http &http, 25 | const HttpRequest &req, 26 | int milliseconds, 27 | QElapsedTimer &elapsedTimer); 28 | QUrl url() const { return req.url; } 29 | int statusCode() const { return 200; } 30 | QByteArray body() const { return QByteArray(); } 31 | 32 | private slots: 33 | void checkElapsed(); 34 | 35 | private: 36 | void doRequest(); 37 | Http &http; 38 | HttpRequest req; 39 | int milliseconds; 40 | QElapsedTimer &elapsedTimer; 41 | QTimer *timer; 42 | }; 43 | 44 | #endif // THROTTLEDHTTP_H 45 | --------------------------------------------------------------------------------