Feature: Added proper IQ Stanza error handling #21

Merged
sha512sum merged 5 commits from feature/handling_stanza_errors into main 2024-12-24 06:11:48 +00:00
6 changed files with 199 additions and 17 deletions
Showing only changes of commit b565a09c1a - Show all commits

View file

@ -84,14 +84,12 @@ struct Client {
auto set_bind_response = co_await connection.template Read<Iq<::iq::Bind>>(); auto set_bind_response = co_await connection.template Read<Iq<::iq::Bind>>();
std::visit(utempl::Overloaded( std::visit(utempl::Overloaded(
[](auto error) {
throw "Error response on IQ: Set::Bind: ''"; // TODO(unknown): Add exact error parsing
},
[&](::iq::ResultBind r) { [&](::iq::ResultBind r) {
jid.resource = std::move(r.payload.jid->resource); jid.resource = std::move(r.payload.jid->resource);
SPDLOG_INFO("Allocated resource: {}", jid.resource); SPDLOG_INFO("Allocated resource: {}", jid.resource);
}), },
set_bind_response); IqErrThrowVisitor{"Error response on IQ: Set::Bind"}),
std::move(set_bind_response));
co_return; co_return;
} }
@ -101,14 +99,12 @@ struct Client {
const auto get_roster_response = co_await connection.template Read<Iq<::iq::Roster>>(); const auto get_roster_response = co_await connection.template Read<Iq<::iq::Roster>>();
std::visit(utempl::Overloaded( std::visit(utempl::Overloaded(
[](auto error) {
throw "Error response on IQ: Get::Roster: ''"; // TODO(unknown): Add exact error parsing
},
[&](::iq::ResultRoster r) { [&](::iq::ResultRoster r) {
roster = std::move(r.payload); roster = std::move(r.payload);
SPDLOG_INFO("New roster: {}", ToString(roster)); SPDLOG_INFO("New roster: {}", ToString(roster));
}), },
get_roster_response); IqErrThrowVisitor{"Error response on IQ: Get::Roster"}),
std::move(get_roster_response));
co_return; co_return;
} }

View file

@ -1,9 +1,12 @@
#pragma once #pragma once
#include <spdlog/spdlog.h>
#include <larra/jid.hpp> #include <larra/jid.hpp>
#include <larra/serialization.hpp> #include <larra/serialization.hpp>
#include <larra/stream_error.hpp> #include <larra/stanza_error.hpp>
#include <larra/utils.hpp> #include <larra/utils.hpp>
#include <optional> #include <optional>
#include <stdexcept>
#include <string> #include <string>
namespace larra::xmpp { namespace larra::xmpp {
@ -115,7 +118,41 @@ using Error = BaseImplWithPayload<kErrorName, Payload>;
} // namespace iq } // namespace iq
using IqError = iq::Error<stanza::error::StanzaError>;
template <typename Payload> template <typename Payload>
using Iq = std::variant<iq::Get<Payload>, iq::Set<Payload>, iq::Result<Payload>, iq::Error<Payload>>; using Iq = std::variant<iq::Get<Payload>, iq::Set<Payload>, iq::Result<Payload>, IqError>;
struct IqErrThrowVisitor {
Ivan-lis marked this conversation as resolved Outdated

Maybe it would be better to move the templates from here to the () operators?

Maybe it would be better to move the templates from here to the () operators?
constexpr IqErrThrowVisitor(std::string_view baseErrorMsg) : baseErrorMsg(baseErrorMsg) {
}
template <typename Payload>
void operator()(const iq::Get<Payload>&) {
static constexpr std::string_view getErrorMsg = ": 'Get' is an invalid type for IQ result. Expected 'Result' or 'Error'";
auto finalErrorMsg = std::string(baseErrorMsg).append(getErrorMsg);
SPDLOG_ERROR(finalErrorMsg);
throw std::runtime_error{finalErrorMsg};
}
template <typename Payload>
void operator()(const iq::Set<Payload>&) {
static constexpr std::string_view getErrorMsg = ": 'Set' is an invalid type for IQ result. Expected 'Result' or 'Error'";
auto finalErrorMsg = std::string(baseErrorMsg).append(getErrorMsg);
SPDLOG_ERROR(finalErrorMsg);
throw std::runtime_error{finalErrorMsg};
}
void operator()(IqError err) {
SPDLOG_ERROR(baseErrorMsg);
std::visit(
[](auto exception) {
throw exception;
},
std::move(err.payload));
}
std::string_view baseErrorMsg;
};
} // namespace larra::xmpp } // namespace larra::xmpp

View file

@ -0,0 +1,148 @@
#pragma once
#include <libxml++/libxml++.h>
#include <larra/utils.hpp>
#include <optional>
#include <variant>
namespace larra::xmpp::stanza::error {
struct StanzaBaseError : std::exception {};
// DO NOT MOVE TO ANOTHER NAMESPACE(where no heirs). VIA friend A FUNCTION IS ADDED THAT VIA ADL WILL BE SEARCHED FOR HEIRS
// C++20 modules very unstable in clangd :(
template <typename T>
struct StanzaErrorImpl : StanzaBaseError {
static constexpr auto kDefaultName = "error";
static constexpr auto kDefaultNamespace = "urn:ietf:params:xml:ns:xmpp-stanzas";
static inline const auto kKebabCaseName = static_cast<std::string>(utils::ToKebabCaseName<T>());
static constexpr auto kErrorMessage = [] -> std::string_view {
static constexpr auto name = nameof::nameof_short_type<T>();
static constexpr auto str = [] {
return std::array{std::string_view{"Stanza IQ Error: "}, std::string_view{name}, std::string_view{"\0", 1}} | std::views::join;
};
static constexpr auto array = str() | std::ranges::to<utils::RangeToWrapper<std::array<char, std::ranges::distance(str())>>>();
return {array.data(), array.size() - 1};
}();
std::optional<std::string> by{};
std::string type;
// TODO(unknown): Add "optional text children" support for stanza error. Check "XML Stanzas" -> "Syntax" for more details
static constexpr auto TryParse(xmlpp::Element* element) -> std::optional<T> {
if(not element) {
return std::nullopt;
}
auto by = element->get_attribute("by");
auto type = element->get_attribute("type");
if(not type) {
return std::nullopt;
}
auto node = element->get_first_child(kKebabCaseName);
if(not node) {
return std::nullopt;
}
T obj;
obj.type = type->get_value();
if(by) {
obj.by = std::optional{by->get_value()};
}
return obj;
}
static constexpr auto Parse(xmlpp::Element* element) -> T {
return TryParse(element).value();
}
friend constexpr auto operator<<(xmlpp::Element* element, const T& obj) -> void {
element->set_attribute("type", obj.type);
if(obj.by) {
element->set_attribute("by", *obj.by);
}
auto node = element->add_child_element(kKebabCaseName);
node->set_namespace_declaration(kDefaultNamespace);
}
constexpr auto operator==(const StanzaErrorImpl<T>&) const -> bool {
return true;
};
[[nodiscard]] constexpr auto what() const noexcept -> const char* override {
return kErrorMessage.data();
}
};
// Helper class to prevent parsing response stream into an expected return type if its name is an 'error'
struct UnknownStanzaError : StanzaBaseError {
static constexpr auto kDefaultName = "stream:error";
static constexpr std::string_view kErrorMessage = "Unknown XMPP stream error";
static constexpr auto TryParse(xmlpp::Element* element) {
return std::optional{UnknownStanzaError{}};
}
static constexpr auto Parse(xmlpp::Element* element) {
return TryParse(element).value();
}
friend constexpr auto operator<<(xmlpp::Element* element, const UnknownStanzaError& obj) -> void {
throw std::runtime_error{std::format("'{}' must never be written into the real stream!", kErrorMessage)};
}
constexpr auto operator==(const UnknownStanzaError&) const -> bool {
return true;
};
[[nodiscard]] constexpr auto what() const noexcept -> const char* override {
return kErrorMessage.data();
}
};
struct BadRequest : StanzaErrorImpl<BadRequest> {};
struct Conflict : StanzaErrorImpl<Conflict> {};
struct FeatureNotImplemented : StanzaErrorImpl<FeatureNotImplemented> {};
struct Forbidden : StanzaErrorImpl<Forbidden> {};
struct Gone : StanzaErrorImpl<Gone> {};
struct InternalServerError : StanzaErrorImpl<InternalServerError> {};
struct ItemNotFound : StanzaErrorImpl<ItemNotFound> {};
struct JidMalformed : StanzaErrorImpl<JidMalformed> {};
struct NotAcceptable : StanzaErrorImpl<NotAcceptable> {};
struct NotAllowed : StanzaErrorImpl<NotAllowed> {};
struct NotAuthorized : StanzaErrorImpl<NotAuthorized> {};
struct PolicyViolation : StanzaErrorImpl<PolicyViolation> {};
struct RecipientUnavailable : StanzaErrorImpl<RecipientUnavailable> {};
struct Redirect : StanzaErrorImpl<Redirect> {};
struct RegistrationRequired : StanzaErrorImpl<RegistrationRequired> {};
struct RemoteServerNotFound : StanzaErrorImpl<RemoteServerNotFound> {};
struct RemoteServerTimeout : StanzaErrorImpl<RemoteServerTimeout> {};
struct ResourceConstraint : StanzaErrorImpl<ResourceConstraint> {};
struct ServiceUnavailable : StanzaErrorImpl<ServiceUnavailable> {};
struct SubscriptionRequired : StanzaErrorImpl<SubscriptionRequired> {};
struct UndefinedCondition : StanzaErrorImpl<UndefinedCondition> {};
struct UnexpectedRequest : StanzaErrorImpl<UnexpectedRequest> {};
using StanzaError = std::variant<BadRequest,
Conflict,
FeatureNotImplemented,
Forbidden,
Gone,
InternalServerError,
ItemNotFound,
JidMalformed,
NotAcceptable,
NotAllowed,
NotAuthorized,
PolicyViolation,
RecipientUnavailable,
Redirect,
RegistrationRequired,
RemoteServerNotFound,
RemoteServerTimeout,
ResourceConstraint,
ServiceUnavailable,
SubscriptionRequired,
UndefinedCondition,
UnexpectedRequest,
UnknownStanzaError>;
static_assert(std::is_same_v<typename std::variant_alternative_t<std::variant_size_v<StanzaError> - 1, StanzaError>, UnknownStanzaError>,
"'UnknownStanzaError' must be at the end of 'StanzaError' variant");
} // namespace larra::xmpp::stanza::error

View file

@ -52,7 +52,7 @@ struct UnknownXmppError : BaseError {
return TryParse(element).value(); return TryParse(element).value();
} }
friend constexpr auto operator<<(xmlpp::Element* element, const UnknownXmppError& obj) -> void { friend constexpr auto operator<<(xmlpp::Element* element, const UnknownXmppError& obj) -> void {
throw std::format("'{}' must never be written into the real stream!", kErrorMessage); throw std::runtime_error{std::format("'{}' must never be written into the real stream!", kErrorMessage)};
} }
[[nodiscard]] constexpr auto what() const noexcept -> const char* override { [[nodiscard]] constexpr auto what() const noexcept -> const char* override {
return kErrorMessage.data(); return kErrorMessage.data();
@ -112,7 +112,8 @@ using StreamError = std::variant<error::stream::BadFormat,
error::stream::UnsupportedVersion, error::stream::UnsupportedVersion,
error::stream::UnknownXmppError>; error::stream::UnknownXmppError>;
static_assert(!std::is_same_v<typename std::variant_alternative_t<std::variant_size_v<StreamError> - 1, StreamError>, StreamError>, static_assert(
std::is_same_v<typename std::variant_alternative_t<std::variant_size_v<StreamError> - 1, StreamError>, error::stream::UnknownXmppError>,
"'UnknownXmppError' must be at the end of 'StreamError' variant"); "'UnknownXmppError' must be at the end of 'StreamError' variant");
} // namespace larra::xmpp } // namespace larra::xmpp

View file

@ -2,7 +2,7 @@
namespace larra::xmpp::iq { namespace larra::xmpp::iq {
auto operator<<(xmlpp::Element* element, const Bind& bind) -> void { auto operator<<(xmlpp::Element* element, const Bind& bind) -> void {
element->set_attribute("xmlns", Bind::kDefaultNamespace); element->set_namespace_declaration(Bind::kDefaultNamespace);
if(bind.jid) { if(bind.jid) {
auto* jid_el = element->add_child_element("jid"); auto* jid_el = element->add_child_element("jid");

View file

@ -21,7 +21,7 @@ auto RosterItem::Parse(xmlpp::Element* element) -> RosterItem {
} }
auto operator<<(xmlpp::Element* element, const Roster& self) -> void { auto operator<<(xmlpp::Element* element, const Roster& self) -> void {
element->set_attribute("xmlns", Roster::kDefaultNamespace); element->set_namespace_declaration(Roster::kDefaultNamespace);
S::Serialize(element, self); S::Serialize(element, self);
} }
auto Roster::Parse(xmlpp::Element* element) -> Roster { auto Roster::Parse(xmlpp::Element* element) -> Roster {