aboutsummaryrefslogtreecommitdiff
path: root/lib/jobs/basejob.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/jobs/basejob.cpp')
-rw-r--r--lib/jobs/basejob.cpp164
1 files changed, 121 insertions, 43 deletions
diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp
index 2519713e..3978dbcb 100644
--- a/lib/jobs/basejob.cpp
+++ b/lib/jobs/basejob.cpp
@@ -19,12 +19,11 @@
#include "basejob.h"
#include "connectiondata.h"
-#include "util.h"
-#include <QtCore/QJsonObject>
#include <QtCore/QRegularExpression>
#include <QtCore/QTimer>
#include <QtCore/QStringBuilder>
+#include <QtCore/QMetaEnum>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkRequest>
@@ -37,6 +36,7 @@ using namespace std::chrono_literals;
BaseJob::StatusCode BaseJob::Status::fromHttpCode(int httpCode)
{
+ // Based on https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
if (httpCode / 10 == 41) // 41x errors
return httpCode == 410 ? IncorrectRequestError : NotFoundError;
switch (httpCode) {
@@ -113,6 +113,13 @@ public:
}
void sendRequest();
+ /*! \brief Parse the response byte array into JSON
+ *
+ * This calls QJsonDocument::fromJson() on rawResponse, converts
+ * the QJsonParseError result to BaseJob::Status and stores the resulting
+ * JSON in jsonResponse.
+ */
+ Status parseJson();
ConnectionData* connection = nullptr;
@@ -131,9 +138,17 @@ public:
// type QMimeType is of little help with MIME type globs (`text/*` etc.)
QByteArrayList expectedContentTypes { "application/json" };
+ QByteArrayList expectedKeys;
+
QScopedPointer<QNetworkReply, NetworkReplyDeleter> reply;
Status status = Unprepared;
QByteArray rawResponse;
+ /// Contains a null document in case of non-JSON body (for a successful
+ /// or unsuccessful response); a document with QJsonObject or QJsonArray
+ /// in case of a successful response with JSON payload, as per the API
+ /// definition (including an empty JSON object - QJsonObject{});
+ /// and QJsonObject in case of an API error.
+ QJsonDocument jsonResponse;
QUrl errorUrl; //< May contain a URL to help with some errors
LoggingCategory logCat = JOBS;
@@ -243,6 +258,19 @@ void BaseJob::setExpectedContentTypes(const QByteArrayList& contentTypes)
d->expectedContentTypes = contentTypes;
}
+const QByteArrayList BaseJob::expectedKeys() const { return d->expectedKeys; }
+
+void BaseJob::addExpectedKey(const QByteArray& key) { d->expectedKeys << key; }
+
+void BaseJob::setExpectedKeys(const QByteArrayList& keys)
+{
+ d->expectedKeys = keys;
+}
+
+const QNetworkReply* BaseJob::reply() const { return d->reply.data(); }
+
+QNetworkReply* BaseJob::reply() { return d->reply.data(); }
+
QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QString& path,
const QUrlQuery& query)
{
@@ -304,7 +332,7 @@ void BaseJob::doPrepare() { }
void BaseJob::onSentRequest(QNetworkReply*) { }
-void BaseJob::beforeAbandon(QNetworkReply*) { }
+void BaseJob::beforeAbandon() { }
void BaseJob::initiate(ConnectionData* connData, bool inBackground)
{
@@ -346,40 +374,74 @@ void BaseJob::sendRequest()
emit aboutToSendRequest();
d->sendRequest();
Q_ASSERT(d->reply);
- connect(d->reply.data(), &QNetworkReply::finished, this, &BaseJob::gotReply);
+ connect(reply(), &QNetworkReply::finished, this, [this] {
+ gotReply();
+ finishJob();
+ });
if (d->reply->isRunning()) {
- connect(d->reply.data(), &QNetworkReply::metaDataChanged, this,
- &BaseJob::checkReply);
- connect(d->reply.data(), &QNetworkReply::uploadProgress, this,
+ connect(reply(), &QNetworkReply::metaDataChanged, this,
+ [this] { checkReply(reply()); });
+ connect(reply(), &QNetworkReply::uploadProgress, this,
&BaseJob::uploadProgress);
- connect(d->reply.data(), &QNetworkReply::downloadProgress, this,
+ connect(reply(), &QNetworkReply::downloadProgress, this,
&BaseJob::downloadProgress);
d->timer.start(getCurrentTimeout());
qCInfo(d->logCat).noquote() << "Sent" << d->dumpRequest();
- onSentRequest(d->reply.data());
+ onSentRequest(reply());
emit sentRequest();
} else
qCCritical(d->logCat).noquote()
<< "Request could not start:" << d->dumpRequest();
}
+BaseJob::Status BaseJob::Private::parseJson()
+{
+ QJsonParseError error { 0, QJsonParseError::MissingObject };
+ jsonResponse = QJsonDocument::fromJson(rawResponse, &error);
+ return { error.error == QJsonParseError::NoError
+ ? BaseJob::NoError
+ : BaseJob::IncorrectResponse,
+ error.errorString() };
+}
+
void BaseJob::gotReply()
{
- checkReply();
+ setStatus(checkReply(reply()));
+
+ if (status().good()
+ && d->expectedContentTypes == QByteArrayList { "application/json" }) {
+ d->rawResponse = reply()->readAll();
+ setStatus(d->parseJson());
+ if (status().good() && !expectedKeys().empty()) {
+ const auto& responseObject = jsonData();
+ QByteArrayList missingKeys;
+ for (const auto& k: expectedKeys())
+ if (!responseObject.contains(k))
+ missingKeys.push_back(k);
+ if (!missingKeys.empty())
+ setStatus(IncorrectResponse, tr("Required JSON keys missing: ")
+ + missingKeys.join());
+ }
+ if (!status().good()) // Bad JSON in a "good" reply: bail out
+ return;
+ } // else {
+ // If the endpoint expects anything else than just (API-related) JSON
+ // reply()->readAll() is not performed and the whole reply processing
+ // is left to derived job classes: they may read it piecemeal or customise
+ // per content type in prepareResult(), or even have read it already
+ // (see, e.g., DownloadFileJob).
+ // }
+
if (status().good())
- setStatus(parseReply(d->reply.data()));
+ setStatus(prepareResult());
else {
- d->rawResponse = d->reply->readAll();
- const auto jsonBody = d->reply->rawHeader("Content-Type")
- == "application/json";
+ d->rawResponse = reply()->readAll();
qCDebug(d->logCat).noquote()
- << "Error body (truncated if long):" << d->rawResponse.left(500);
- if (jsonBody)
- setStatus(
- parseError(d->reply.data(),
- QJsonDocument::fromJson(d->rawResponse).object()));
+ << "Error body (truncated if long):" << rawDataSample(500);
+ // Parse the error payload and update the status if needed
+ if (const auto newStatus = prepareError(); !newStatus.good())
+ setStatus(newStatus);
}
- finishJob();
}
bool checkContentType(const QByteArray& type, const QByteArrayList& patterns)
@@ -408,12 +470,10 @@ bool checkContentType(const QByteArray& type, const QByteArrayList& patterns)
return false;
}
-BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const
+BaseJob::Status BaseJob::checkReply(const QNetworkReply* reply) const
{
- // QNetworkReply error codes seem to be flawed when it comes to HTTP;
- // see, e.g., https://github.com/quotient-im/libQuotient/issues/200
- // so check genuine HTTP codes. The below processing is based on
- // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
+ // QNetworkReply error codes are insufficient for our purposes (e.g. they
+ // don't allow to discern HTTP code 429) so check the original code instead
const auto httpCodeHeader =
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
if (!httpCodeHeader.isValid()) {
@@ -444,24 +504,23 @@ BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const
return Status::fromHttpCode(httpCode, message);
}
-void BaseJob::checkReply() { setStatus(doCheckReply(d->reply.data())); }
+BaseJob::Status BaseJob::prepareResult() { return Success; }
-BaseJob::Status BaseJob::parseReply(QNetworkReply* reply)
+BaseJob::Status BaseJob::prepareError()
{
- d->rawResponse = reply->readAll();
- QJsonParseError error { 0, QJsonParseError::MissingObject };
- const auto& json = QJsonDocument::fromJson(d->rawResponse, &error);
- if (error.error == QJsonParseError::NoError)
- return parseJson(json);
+ // Since it's an error, the expected content type is of no help;
+ // check the actually advertised content type instead
+ if (reply()->rawHeader("Content-Type") != "application/json")
+ return NoError; // Retain the status if the error payload is not JSON
- return { IncorrectResponseError, error.errorString() };
-}
+ if (const auto status = d->parseJson(); !status.good())
+ return status;
-BaseJob::Status BaseJob::parseJson(const QJsonDocument&) { return Success; }
+ if (d->jsonResponse.isArray())
+ return { IncorrectResponse,
+ tr("Malformed error JSON: an array instead of an object") };
-BaseJob::Status BaseJob::parseError(QNetworkReply* /*reply*/,
- const QJsonObject& errorJson)
-{
+ const auto& errorJson = jsonData();
const auto errCode = errorJson.value("errcode"_ls).toString();
if (error() == TooManyRequestsError || errCode == "M_LIMIT_EXCEEDED") {
QString msg = tr("Too many requests");
@@ -499,6 +558,16 @@ BaseJob::Status BaseJob::parseError(QNetworkReply* /*reply*/,
return d->status;
}
+QJsonValue BaseJob::takeValueFromJson(const QString& key)
+{
+ if (!d->jsonResponse.isObject())
+ return QJsonValue::Undefined;
+ auto o = d->jsonResponse.object();
+ auto v = o.take(key);
+ d->jsonResponse.setObject(o);
+ return v;
+}
+
void BaseJob::stop()
{
// This method is (also) used to semi-finalise the job before retrying; so
@@ -616,10 +685,19 @@ QString BaseJob::rawDataSample(int bytesAtMost) const
Q_ASSERT(data.size() <= d->rawResponse.size());
return data.size() == d->rawResponse.size()
? data
- : data
- + tr("...(truncated, %Ln bytes in total)",
- "Comes after trimmed raw network response",
- d->rawResponse.size());
+ : data + tr("...(truncated, %Ln bytes in total)",
+ "Comes after trimmed raw network response",
+ d->rawResponse.size());
+}
+
+QJsonObject BaseJob::jsonData() const
+{
+ return d->jsonResponse.object();
+}
+
+QJsonArray BaseJob::jsonItems() const
+{
+ return d->jsonResponse.array();
}
QString BaseJob::statusCaption() const
@@ -704,7 +782,7 @@ void BaseJob::setStatus(int code, QString message)
void BaseJob::abandon()
{
- beforeAbandon(d->reply ? d->reply.data() : nullptr);
+ beforeAbandon();
d->timer.stop();
d->retryTimer.stop(); // In case abandon() was called between retries
setStatus(Abandoned);