Axivion: Analyze Dashboard server response for errors

Change-Id: Id66c3ad5d8a6c7d73e7ad781893c936b0829cfbf
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
Andreas Loth
2023-09-19 12:17:18 +02:00
parent a60b43f028
commit 1f96b1b7ed
7 changed files with 247 additions and 16 deletions

View File

@@ -14,4 +14,5 @@ add_qtc_plugin(Axivion
dashboard/dto.cpp dashboard/dto.h
dashboard/concat.cpp dashboard/concat.h
dashboard/dashboardclient.cpp dashboard/dashboardclient.h
dashboard/error.h dashboard/error.cpp
)

View File

@@ -26,14 +26,12 @@ QtcPlugin {
"axivionsettings.cpp",
"axivionsettings.h",
"axiviontr.h",
"dashboard/dashboardclient.cpp",
"dashboard/dashboardclient.h",
]
cpp.includePaths: base.concat(["."]) // needed for the generated stuff below
Group {
name: "Generated DTOs"
name: "Dashboard Communication"
prefix: "dashboard/"
files: [
@@ -41,6 +39,10 @@ QtcPlugin {
"concat.h",
"dto.cpp",
"dto.h",
"dashboardclient.cpp",
"dashboardclient.h",
"error.cpp",
"error.h",
]
}
}

View File

@@ -259,7 +259,8 @@ void AxivionPluginPrivate::handleProjectInfo(DashboardClient::RawProjectInfo raw
{
m_runningQuery = false;
if (!rawInfo) {
Core::MessageManager::writeFlashing(QStringLiteral(u"Axivion: ") + rawInfo.error());
Core::MessageManager::writeFlashing(
QStringLiteral(u"Axivion: %1").arg(rawInfo.error().message()));
return;
}
m_currentProjectInfo = std::make_shared<const DashboardClient::ProjectInfo>(std::move(rawInfo.value()));

View File

@@ -30,13 +30,17 @@ static void deleteLater(QObject *obj)
obj->deleteLater();
}
using RawBody = Utils::expected_str<DataWithOrigin<QByteArray>>;
using RawBody = Utils::expected<DataWithOrigin<QByteArray>, Error>;
static constexpr int httpStatusCodeOk = 200;
static constexpr QLatin1String jsonContentType{ "application/json" };
class RawBodyReader final
{
public:
RawBodyReader(std::shared_ptr<QNetworkReply> reply)
: m_reply(std::move(reply))
RawBodyReader(std::shared_ptr<QNetworkReply> reply, QAnyStringView expectedContentType)
: m_reply(std::move(reply)),
m_expectedContentType(expectedContentType)
{ }
~RawBodyReader() { }
@@ -44,19 +48,47 @@ public:
RawBody operator()()
{
QNetworkReply::NetworkError error = m_reply->error();
if (error != QNetworkReply::NetworkError::NoError)
return tl::make_unexpected(QString::number(error)
+ QLatin1String(": ")
+ m_reply->errorString());
return DataWithOrigin(m_reply->url(), m_reply->readAll());
int statusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QString contentType = m_reply->header(QNetworkRequest::ContentTypeHeader)
.toString()
.split(';')
.constFirst()
.trimmed()
.toLower();
if (error == QNetworkReply::NetworkError::NoError
&& statusCode == httpStatusCodeOk
&& contentType == m_expectedContentType) {
return DataWithOrigin(m_reply->url(), m_reply->readAll());
}
if (contentType == jsonContentType) {
try {
return tl::make_unexpected(DashboardError(
m_reply->url(),
statusCode,
m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(),
Dto::ErrorDto::deserialize(m_reply->readAll())));
} catch (const Dto::invalid_dto_exception &) {
// ignore
}
}
if (statusCode != 0) {
return tl::make_unexpected(HttpError(
m_reply->url(),
statusCode,
m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(),
QString::fromUtf8(m_reply->readAll()))); // encoding?
}
return tl::make_unexpected(
NetworkError(m_reply->url(), error, m_reply->errorString()));
}
private:
std::shared_ptr<QNetworkReply> m_reply;
QAnyStringView m_expectedContentType;
};
template<typename T>
static Utils::expected_str<DataWithOrigin<T>> RawBodyParser(RawBody rawBody)
static Utils::expected<DataWithOrigin<T>, Error> RawBodyParser(RawBody rawBody)
{
if (!rawBody)
return tl::make_unexpected(std::move(rawBody.error()));
@@ -65,7 +97,8 @@ static Utils::expected_str<DataWithOrigin<T>> RawBodyParser(RawBody rawBody)
return DataWithOrigin(std::move(rawBody.value().origin),
std::move(data));
} catch (const Dto::invalid_dto_exception &e) {
return tl::make_unexpected(QString::fromUtf8(e.what()));
return tl::make_unexpected(GeneralError(std::move(rawBody.value().origin),
QString::fromUtf8(e.what())));
}
}
@@ -91,7 +124,7 @@ QFuture<DashboardClient::RawProjectInfo> DashboardClient::fetchProjectInfo(const
std::shared_ptr<QNetworkReply> reply{ this->m_networkAccessManager.get(request), deleteLater };
return QtFuture::connect(reply.get(), &QNetworkReply::finished)
.onCanceled(reply.get(), [reply] { reply->abort(); })
.then(RawBodyReader(reply))
.then(RawBodyReader(reply, jsonContentType))
.then(QtFuture::Launch::Async, &RawBodyParser<Dto::ProjectInfoDto>);
}

View File

@@ -8,6 +8,7 @@
*/
#include "dashboard/dto.h"
#include "dashboard/error.h"
#include <utils/expected.h>
#include <utils/networkaccessmanager.h>
@@ -32,7 +33,7 @@ class DashboardClient
{
public:
using ProjectInfo = DataWithOrigin<Dto::ProjectInfoDto>;
using RawProjectInfo = Utils::expected_str<ProjectInfo>;
using RawProjectInfo = Utils::expected<ProjectInfo, Error>;
DashboardClient(Utils::NetworkAccessManager &networkAccessManager);

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2022-current by Axivion GmbH
* https://www.axivion.com/
*
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
*/
#include "dashboard/error.h"
#include <type_traits>
#include <utility>
namespace Axivion::Internal
{
CommunicationError::CommunicationError(QUrl replyUrl)
: replyUrl(std::move(replyUrl))
{
}
GeneralError::GeneralError(QUrl replyUrl, QString message)
: CommunicationError(replyUrl),
message(std::move(message))
{
}
NetworkError::NetworkError(QUrl replyUrl,
QNetworkReply::NetworkError networkError,
QString networkErrorString)
: CommunicationError(std::move(replyUrl)),
networkError(std::move(networkError)),
networkErrorString(std::move(networkErrorString))
{
}
HttpError::HttpError(QUrl replyUrl, int httpStatusCode, QString httpReasonPhrase, QString body)
: CommunicationError(std::move(replyUrl)),
httpStatusCode(httpStatusCode),
httpReasonPhrase(std::move(httpReasonPhrase)),
body(std::move(body))
{
}
DashboardError::DashboardError(QUrl replyUrl, int httpStatusCode, QString httpReasonPhrase, Dto::ErrorDto error)
: CommunicationError(std::move(replyUrl)),
httpStatusCode(httpStatusCode),
httpReasonPhrase(std::move(httpReasonPhrase)),
dashboardVersion(std::move(error.dashboardVersionNumber)),
type(std::move(error.type)),
message(std::move(error.message))
{
}
Error::Error(GeneralError error) : m_error(std::move(error))
{
}
Error::Error(NetworkError error) : m_error(std::move(error))
{
}
Error::Error(HttpError error) : m_error(std::move(error))
{
}
Error::Error(DashboardError error) : m_error(std::move(error))
{
}
// https://www.cppstories.com/2018/09/visit-variants/
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>; // line not needed in C++20...
QString Error::message() const
{
return std::visit(
overloaded{
[](const GeneralError &error) {
return QStringLiteral(u"GeneralError (%1) %2")
.arg(error.replyUrl.toString(),
error.message);
},
[](const NetworkError &error) {
return QStringLiteral(u"NetworkError (%1) %2: %3")
.arg(error.replyUrl.toString(),
QString::number(error.networkError),
error.networkErrorString);
},
[](const HttpError &error) {
return QStringLiteral(u"HttpError (%1) %2: %3\n%4")
.arg(error.replyUrl.toString(),
QString::number(error.httpStatusCode),
error.httpReasonPhrase,
error.body);
},
[](const DashboardError &error) {
return QStringLiteral(u"DashboardError (%1) [%2 %3] %4: %5")
.arg(error.replyUrl.toString(),
QString::number(error.httpStatusCode),
error.httpReasonPhrase,
error.type,
error.message);
},
}, this->m_error);
}
} // namespace Axivion::Internal

View File

@@ -0,0 +1,86 @@
#pragma once
/*
* Copyright (C) 2022-current by Axivion GmbH
* https://www.axivion.com/
*
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
*/
#include "dashboard/dto.h"
#include <QNetworkReply>
#include <variant>
namespace Axivion::Internal
{
class CommunicationError
{
public:
QUrl replyUrl;
CommunicationError(QUrl replyUrl);
};
class GeneralError : public CommunicationError
{
public:
QString message;
GeneralError(QUrl replyUrl, QString message);
};
class NetworkError : public CommunicationError
{
public:
QNetworkReply::NetworkError networkError;
QString networkErrorString;
NetworkError(QUrl replyUrl, QNetworkReply::NetworkError networkError, QString networkErrorString);
};
class HttpError : public CommunicationError
{
public:
int httpStatusCode;
QString httpReasonPhrase;
QString body;
HttpError(QUrl replyUrl, int httpStatusCode, QString httpReasonPhrase, QString body);
};
class DashboardError : public CommunicationError
{
public:
int httpStatusCode;
QString httpReasonPhrase;
std::optional<QString> dashboardVersion;
QString type;
QString message;
DashboardError(QUrl replyUrl, int httpStatusCode, QString httpReasonPhrase, Dto::ErrorDto error);
};
class Error
{
public:
Error(GeneralError error);
Error(NetworkError error);
Error(HttpError error);
Error(DashboardError error);
QString message() const;
private:
std::variant<GeneralError,
NetworkError,
HttpError,
DashboardError> m_error;
};
} // namespace Axivion::Internal