summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJuha Vuolle <juha.vuolle@qt.io>2023-06-12 11:23:19 +0300
committerJuha Vuolle <juha.vuolle@qt.io>2023-12-08 15:53:33 +0200
commite560adef213301318dcc13d4db155624846e0420 (patch)
tree237ffa17c837ee0f270885641b781d9bb47c6fd6
parentf587ba1036164691a0981897397bdcc8f3472438 (diff)
Add REST client convenience wrappers
[ChangeLog][QtNetwork][QRestAccessManager] Added new convenience classes QRestAccessManager and QRestReply for typical RESTful client application usage Task-number: QTBUG-114637 Task-number: QTBUG-114701 Change-Id: I65057e56bf27f365b54bfd528565efd5f09386aa Reviewed-by: MÃ¥rten Nordheim <marten.nordheim@qt.io> Reviewed-by: Mate Barany <mate.barany@qt.io> Reviewed-by: Marc Mutz <marc.mutz@qt.io>
-rw-r--r--src/network/CMakeLists.txt2
-rw-r--r--src/network/access/qrestaccessmanager.cpp819
-rw-r--r--src/network/access/qrestaccessmanager.h110
-rw-r--r--src/network/access/qrestaccessmanager_p.h87
-rw-r--r--src/network/access/qrestreply.cpp364
-rw-r--r--src/network/access/qrestreply.h55
-rw-r--r--src/network/access/qrestreply_p.h39
-rw-r--r--src/network/doc/snippets/code/src_network_access_qrestaccessmanager.cpp84
-rw-r--r--tests/auto/network/access/CMakeLists.txt1
-rw-r--r--tests/auto/network/access/qrestaccessmanager/CMakeLists.txt11
-rw-r--r--tests/auto/network/access/qrestaccessmanager/httptestserver.cpp243
-rw-r--r--tests/auto/network/access/qrestaccessmanager/httptestserver_p.h78
-rw-r--r--tests/auto/network/access/qrestaccessmanager/tst_qrestaccessmanager.cpp794
13 files changed, 2687 insertions, 0 deletions
diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt
index d1ac08251a..4ef2699ee8 100644
--- a/src/network/CMakeLists.txt
+++ b/src/network/CMakeLists.txt
@@ -135,6 +135,8 @@ qt_internal_extend_target(Network CONDITION QT_FEATURE_http
access/qnetworkreplyhttpimpl.cpp access/qnetworkreplyhttpimpl_p.h
access/qnetworkrequestfactory.cpp access/qnetworkrequestfactory_p.h
access/qnetworkrequestfactory.h
+ access/qrestaccessmanager.cpp access/qrestaccessmanager.h access/qrestaccessmanager_p.h
+ access/qrestreply.cpp access/qrestreply.h access/qrestreply_p.h
socket/qhttpsocketengine.cpp socket/qhttpsocketengine_p.h
)
diff --git a/src/network/access/qrestaccessmanager.cpp b/src/network/access/qrestaccessmanager.cpp
new file mode 100644
index 0000000000..c4efa846a2
--- /dev/null
+++ b/src/network/access/qrestaccessmanager.cpp
@@ -0,0 +1,819 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qrestaccessmanager.h"
+#include "qrestaccessmanager_p.h"
+#include "qrestreply.h"
+
+#include <QtNetwork/qhttpmultipart.h>
+#include <QtNetwork/qnetworkaccessmanager.h>
+#include <QtNetwork/qnetworkreply.h>
+
+#if QT_CONFIG(ssl)
+#include <QtNetwork/qsslsocket.h>
+#endif
+
+#include <QtCore/qjsondocument.h>
+#include <QtCore/qjsonobject.h>
+#include <QtCore/qloggingcategory.h>
+#include <QtCore/qthread.h>
+
+QT_BEGIN_NAMESPACE
+
+using namespace Qt::StringLiterals;
+
+Q_LOGGING_CATEGORY(lcQrest, "qt.network.access.rest")
+
+/*!
+
+ \class QRestAccessManager
+ \brief The QRestAccessManager is a networking convenience class for RESTful
+ client applications.
+ \since 6.7
+
+ \ingroup network
+ \inmodule QtNetwork
+ \reentrant
+
+ QRestAccessManager provides a networking API for typical REST client
+ applications. It provides the means to issue HTTP requests such as GET
+ and POST. The responses to these requests can be handled with traditional
+ Qt signal and slot mechanisms, as well as by providing callbacks
+ directly - see \l {Issuing Network Requests and Handling Replies}.
+
+ The class is a wrapper on top of QNetworkAccessManager, and it both amends
+ convenience methods and omits typically less used features. These
+ features are still accessible by configuring the underlying
+ QNetworkAccessManager directly. QRestAccessManager is closely related to
+ the QRestReply class, which it returns when issuing network requests.
+
+ QRestAccessManager and related QRestReply classes can only be used in the
+ thread they live in. For further information see
+ \l {QObject#Thread Affinity}{QObject thread affinity} documentation.
+
+ \section1 Issuing Network Requests and Handling Replies
+
+ Network requests are initiated with a function call corresponding to
+ the desired HTTP method, such as \c get() and \c post().
+
+ \section2 Using Signals and Slots
+
+ The function returns a QRestReply* object, whose signals can be used
+ to follow up on the completion of the request in a traditional
+ Qt-signals-and-slots way.
+
+ Here's an example of how you could send a GET request and handle the
+ response:
+
+ \snippet code/src_network_access_qrestaccessmanager.cpp 0
+
+ \section2 Using Callbacks and Context Objects
+
+ The functions also take a context object of QObject (subclass) type
+ and a callback function as parameters. The callback takes one QRestReply*
+ as a parameter. The callback can be any callable, incl. a
+ pointer-to-member-function..
+
+ These callbacks are invoked when the QRestReply has finished processing
+ (also in the case the processing finished due to an error).
+
+ The context object can be \c nullptr, although, generally speaking,
+ this is discouraged. Using a valid context object ensures that if the
+ context object is destroyed during request processing, the callback will
+ not be called. Stray callbacks which access a destroyed context is a source
+ of application misbehavior.
+
+ Here's an example of how you could send a GET request and check the
+ response:
+
+ \snippet code/src_network_access_qrestaccessmanager.cpp 1
+
+ Many of the functions take in data for sending to a server. The data is
+ supplied as the second parameter after the request.
+
+ Here's an example of how you could send a POST request and check the
+ response:
+
+ \snippet code/src_network_access_qrestaccessmanager.cpp 2
+
+ \section2 Supported data types
+
+ The following table summarizes the methods and the supported data types.
+ \c X means support.
+
+ \table
+ \header
+ \li Data type
+ \li \c get()
+ \li \c post()
+ \li \c put()
+ \li \c head()
+ \li \c deleteResource()
+ \row
+ \li No data
+ \li X
+ \li -
+ \li -
+ \li X
+ \li X
+ \row
+ \li QByteArray
+ \li X
+ \li X
+ \li X
+ \li -
+ \li -
+ \row
+ \li QJsonObject *)
+ \li X
+ \li X
+ \li X
+ \li -
+ \li -
+ \row
+ \li QJsonArray *)
+ \li -
+ \li X
+ \li X
+ \li -
+ \li -
+ \row
+ \li QVariantMap **)
+ \li -
+ \li X
+ \li X
+ \li -
+ \li -
+ \row
+ \li QHttpMultiPart
+ \li -
+ \li X
+ \li X
+ \li -
+ \li -
+ \row
+ \li QIODevice
+ \li X
+ \li X
+ \li X
+ \li -
+ \li -
+ \endtable
+
+ *) QJsonObject and QJsonArray are sent in \l QJsonDocument::Compact format,
+ and the \c Content-Type header is set to \c {application/json} if the
+ \c Content-Type header was not set
+
+ **) QVariantMap is converted to and treated as a QJsonObject
+
+ \sa QRestReply, QNetworkRequestFactory, QNetworkAccessManager
+*/
+
+/*!
+ \fn void QRestAccessManager::authenticationRequired(QRestReply *reply,
+ QAuthenticator *authenticator)
+
+ This signal is emitted when the final server requires authentication.
+ The authentication relates to the provided \a reply instance, and any
+ credentials are to be filled in the provided \a authenticator instance.
+
+ See \l QNetworkAccessManager::authenticationRequired() for details.
+*/
+
+/*!
+ \fn void QRestAccessManager::proxyAuthenticationRequired(
+ const QNetworkProxy &proxy, QAuthenticator *authenticator)
+
+ This signal is emitted when a proxy authentication requires action.
+ The proxy details are in \a proxy object, and any credentials are
+ to be filled in the provided \a authenticator object.
+
+ See \l QNetworkAccessManager::proxyAuthenticationRequired() for details.
+*/
+
+/*!
+ \fn void QRestAccessManager::requestFinished(QRestReply *reply)
+
+ This signal is emitted whenever a pending network reply is
+ finished. \a reply parameter will contain a pointer to the
+ reply that has just finished. This signal is emitted in tandem
+ with the QRestReply::finished() signal. QRestReply provides
+ functions for checking the status of the request, as well as for
+ acquiring any received data.
+
+ \note Do not delete \a reply object in the slot connected to this
+ signal. Use deleteLater() if needed. See also \l deletesRepliesOnFinished().
+
+ \sa QRestReply::finished()
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get(
+ const QNetworkRequest &request,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ Issues an \c {HTTP GET} based on \a request.
+
+ The optional \a callback and \a context object can be provided for
+ handling the request completion as illustrated below:
+
+ \snippet code/src_network_access_qrestaccessmanager.cpp 3
+
+ Alternatively the signals of the returned QRestReply* object can be
+ used. For further information see
+ \l {Issuing Network Requests and Handling Replies}.
+
+ \sa QRestReply, QRestReply::finished(),
+ QRestAccessManager::requestFinished()
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get(
+ const QNetworkRequest &request, const QByteArray &data,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ Issues an \c {HTTP GET} based on \a request and provided \a data.
+
+ The optional \a callback and \a context object can be provided for
+ handling the request completion as illustrated below:
+
+ \snippet code/src_network_access_qrestaccessmanager.cpp 4
+
+ Alternatively the signals of the returned QRestReply* object can be
+ used. For further information see
+ \l {Issuing Network Requests and Handling Replies}.
+
+ \sa QRestReply, QRestReply::finished(),
+ QRestAccessManager::requestFinished()
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get(
+ const QNetworkRequest &request, const QJsonObject &data,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ \overload
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get(
+ const QNetworkRequest &request, QIODevice *data,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ \overload
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post(
+ const QNetworkRequest &request, const QJsonObject &data,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ Issues an \c {HTTP POST} based on \a request.
+
+ The optional \a callback and \a context object can be provided for
+ handling the request completion as illustrated below:
+
+ \snippet code/src_network_access_qrestaccessmanager.cpp 5
+
+ Alternatively, the signals of the returned QRestReply* object can be
+ used. For further information see
+ \l {Issuing Network Requests and Handling Replies}.
+
+ The \c post() method always requires \a data parameter. The following
+ data types are supported:
+ \list
+ \li QByteArray
+ \li QJsonObject *)
+ \li QJsonArray *)
+ \li QVariantMap **)
+ \li QHttpMultiPart*
+ \li QIODevice*
+ \endlist
+
+ *) Sent in \l QJsonDocument::Compact format, and the
+ \c Content-Type header is set to \c {application/json} if the
+ \c Content-Type header was not set
+ **) QVariantMap is converted to and treated as a QJsonObject
+
+ \sa QRestReply, QRestReply::finished(),
+ QRestAccessManager::requestFinished()
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post(
+ const QNetworkRequest &request, const QJsonArray &data,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ \overload
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post(
+ const QNetworkRequest &request, const QVariantMap &data,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ \overload
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post(
+ const QNetworkRequest &request, const QByteArray &data,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ \overload
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post(
+ const QNetworkRequest &request, QHttpMultiPart *data,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ \overload
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post(
+ const QNetworkRequest &request, QIODevice *data,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ \overload
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put(
+ const QNetworkRequest &request, const QJsonObject &data,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ Issues an \c {HTTP PUT} based on \a request.
+
+ The optional \a callback and \a context object can be provided for
+ handling the request completion as illustrated below:
+
+ \snippet code/src_network_access_qrestaccessmanager.cpp 6
+
+ Alternatively the signals of the returned QRestReply* object can be
+ used. For further information see
+ \l {Issuing Network Requests and Handling Replies}.
+
+ The \c put() method always requires \a data parameter. The following
+ data types are supported:
+ \list
+ \li QByteArray
+ \li QJsonObject *)
+ \li QJsonArray *)
+ \li QVariantMap **)
+ \li QHttpMultiPart*
+ \li QIODevice*
+ \endlist
+
+ *) Sent in \l QJsonDocument::Compact format, and the
+ \c Content-Type header is set to \c {application/json} if the
+ \c Content-Type header was not set
+ **) QVariantMap is converted to and treated as a QJsonObject
+
+ \sa QRestReply, QRestReply::finished(), QRestAccessManager::requestFinished()
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put(
+ const QNetworkRequest &request, const QJsonArray &data,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ \overload
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put(
+ const QNetworkRequest &request, const QVariantMap &data,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ \overload
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put(
+ const QNetworkRequest &request, const QByteArray &data,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ \overload
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put(
+ const QNetworkRequest &request, QHttpMultiPart *data,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ \overload
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put(
+ const QNetworkRequest &request, QIODevice *data,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ \overload
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::head(
+ const QNetworkRequest &request,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ Issues an \c {HTTP HEAD} based on \a request.
+
+ The optional \a callback and \a context object can be provided for
+ handling the request completion as illustrated below:
+
+ \snippet code/src_network_access_qrestaccessmanager.cpp 7
+
+ Alternatively the signals of the returned QRestReply* object can be
+ used. For further information see
+ \l {Issuing Network Requests and Handling Replies}.
+
+ \c head() request does not support providing data.
+
+ \sa QRestReply, QRestReply::finished(),
+ QRestAccessManager::requestFinished()
+*/
+
+/*!
+ \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::deleteResource(
+ const QNetworkRequest &request,
+ const ContextTypeForFunctor<Functor> *context,
+ Functor &&callback)
+
+ Issues an \c {HTTP DELETE} based on \a request.
+
+ The optional \a callback and \a context object can be provided for
+ handling the request completion as illustrated below:
+
+ \snippet code/src_network_access_qrestaccessmanager.cpp 8
+
+ Alternatively the signals of the returned QRestReply* object can be
+ used. For further information see
+ \l {Issuing Network Requests and Handling Replies}.
+
+ \c deleteResource() request does not support providing data.
+
+ \sa QRestReply, QRestReply::finished(),
+ QRestAccessManager::requestFinished()
+*/
+
+/*
+ Memory management/object ownership:
+ - QRestAM is parent of QNAM and QRestReplies
+ - QRestReplies are parents of QNetworkReplies
+*/
+
+/*!
+ Constructs a QRestAccessManager and sets \a parent as the parent object.
+*/
+QRestAccessManager::QRestAccessManager(QObject *parent)
+ : QObject(*new QRestAccessManagerPrivate, parent)
+{
+ Q_D(QRestAccessManager);
+ d->ensureNetworkAccessManager();
+}
+
+/*!
+ Destroys the QRestAccessManager object and frees up any
+ resources, including any unfinished QRestReply objects.
+*/
+QRestAccessManager::~QRestAccessManager()
+ = default;
+
+/*!
+ Returns whether QRestAccessManager is currently configured to automatically
+ delete replies once they have finished. By default this is \c true.
+
+ \sa setDeletesRepliesOnFinished()
+*/
+bool QRestAccessManager::deletesRepliesOnFinished() const
+{
+ Q_D(const QRestAccessManager);
+ return d->deletesRepliesOnFinished;
+}
+
+/*!
+ Enables or disables automatic deletion of QRestReply instances
+ once the request has finished, according to the provided
+ \a autoDelete parameter. The deletion is done with deleteLater()
+ so that using the replies in directly-connected slots or callbacks is safe.
+
+ \sa deletesRepliesOnFinished()
+*/
+void QRestAccessManager::setDeletesRepliesOnFinished(bool autoDelete)
+{
+ Q_D(QRestAccessManager);
+ d->deletesRepliesOnFinished = autoDelete;
+}
+
+/*!
+ Aborts all unfinished network requests. Calling this function is same
+ as calling QRestReply::abort() for all individual unfinished requests.
+
+ \sa QRestReply::abort(), QNetworkReply::abort()
+*/
+void QRestAccessManager::abortRequests()
+{
+ Q_D(QRestAccessManager);
+
+ // Make copy of the reply container, as it might get modified when
+ // aborting individual requests if they finish immediately
+ const auto requests = d->activeRequests;
+ for (const auto &[req, _] : requests.asKeyValueRange())
+ req->abort();
+}
+
+/*!
+ Returns the underlying QNetworkAccessManager instance. The instance
+ can be used for accessing less-frequently used features and configurations.
+
+ \sa QNetworkAccessManager
+*/
+QNetworkAccessManager *QRestAccessManager::networkAccessManager() const
+{
+ Q_D(const QRestAccessManager);
+ return d->qnam;
+}
+
+QRestAccessManagerPrivate::QRestAccessManagerPrivate()
+ = default;
+
+QRestAccessManagerPrivate::~QRestAccessManagerPrivate()
+{
+ if (!activeRequests.isEmpty()) {
+ qCWarning(lcQrest, "Access manager destroyed while %lld requests were still in progress",
+ qlonglong(activeRequests.size()));
+ }
+}
+
+QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request,
+ const QJsonObject &data, const QObject *context,
+ QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&](auto req, auto json) { return d->qnam->post(req, json); },
+ data, request, context, slot);
+}
+
+QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request,
+ const QJsonArray &data, const QObject *context,
+ QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&](auto req, auto json) { return d->qnam->post(req, json); },
+ data, request, context, slot);
+}
+
+QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request,
+ const QVariantMap &data, const QObject *context,
+ QtPrivate::QSlotObjectBase *slot)
+{
+ return postWithDataImpl(request, QJsonObject::fromVariantMap(data), context, slot);
+}
+
+QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request,
+ const QByteArray &data, const QObject *context,
+ QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&]() { return d->qnam->post(request, data); }, context, slot);
+}
+
+QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request,
+ QHttpMultiPart *data, const QObject *context,
+ QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&]() { return d->qnam->post(request, data); }, context, slot);
+}
+
+QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request,
+ QIODevice *data, const QObject *context,
+ QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&]() { return d->qnam->post(request, data); }, context, slot);
+}
+
+QRestReply *QRestAccessManager::getNoDataImpl(const QNetworkRequest &request,
+ const QObject *context, QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&]() { return d->qnam->get(request); }, context, slot);
+}
+
+QRestReply *QRestAccessManager::getWithDataImpl(const QNetworkRequest &request,
+ const QByteArray &data, const QObject *context,
+ QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&]() { return d->qnam->get(request, data); }, context, slot);
+}
+
+QRestReply *QRestAccessManager::getWithDataImpl(const QNetworkRequest &request,
+ const QJsonObject &data, const QObject *context,
+ QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&](auto req, auto json) { return d->qnam->get(req, json); },
+ data, request, context, slot);
+}
+
+QRestReply *QRestAccessManager::getWithDataImpl(const QNetworkRequest &request,
+ QIODevice *data, const QObject *context,
+ QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&]() { return d->qnam->get(request, data); }, context, slot);
+}
+
+QRestReply *QRestAccessManager::deleteResourceNoDataImpl(const QNetworkRequest &request,
+ const QObject *context, QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&]() { return d->qnam->deleteResource(request); }, context, slot);
+}
+
+QRestReply *QRestAccessManager::headNoDataImpl(const QNetworkRequest &request,
+ const QObject *context, QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&]() { return d->qnam->head(request); }, context, slot);
+}
+
+QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request,
+ const QJsonObject &data, const QObject *context,
+ QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&](auto req, auto json) { return d->qnam->put(req, json); },
+ data, request, context, slot);
+}
+
+QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request,
+ const QJsonArray &data, const QObject *context,
+ QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&](auto req, auto json) { return d->qnam->put(req, json); },
+ data, request, context, slot);
+}
+
+QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request,
+ const QVariantMap &data, const QObject *context,
+ QtPrivate::QSlotObjectBase *slot)
+{
+ return putWithDataImpl(request, QJsonObject::fromVariantMap(data), context, slot);
+}
+
+QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request,
+ const QByteArray &data, const QObject *context,
+ QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&]() { return d->qnam->put(request, data); }, context, slot);
+}
+
+QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request,
+ QHttpMultiPart *data, const QObject *context,
+ QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&]() { return d->qnam->put(request, data); }, context, slot);
+}
+
+QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request, QIODevice *data,
+ const QObject *context, QtPrivate::QSlotObjectBase *slot)
+{
+ Q_D(QRestAccessManager);
+ return d->executeRequest([&]() { return d->qnam->put(request, data); }, context, slot);
+}
+
+QRestReply *QRestAccessManagerPrivate::createActiveRequest(QNetworkReply *networkReply,
+ const QObject *contextObject,
+ QtPrivate::QSlotObjectBase *slot)
+{
+ Q_Q(QRestAccessManager);
+ Q_ASSERT(networkReply);
+ auto restReply = new QRestReply(networkReply, q);
+ QtPrivate::SlotObjSharedPtr slotPtr(QtPrivate::SlotObjUniquePtr{slot}); // adopts
+ activeRequests.insert(restReply, CallerInfo{contextObject, slotPtr});
+
+ // If context object is provided, use it with connect => context object lifecycle is considered
+ const QObject *context = contextObject ? contextObject : q;
+ QObject::connect(networkReply, &QNetworkReply::finished, context, [restReply, this]() {
+ handleReplyFinished(restReply);
+ });
+ // Safe guard in case reply is destroyed before it's finished
+ QObject::connect(restReply, &QRestReply::destroyed, q, [restReply, this]() {
+ activeRequests.remove(restReply);
+ });
+ // If context object is destroyed, clean up any possible replies it had associated with it
+ if (contextObject) {
+ QObject::connect(contextObject, &QObject::destroyed, q, [restReply, this]() {
+ activeRequests.remove(restReply);
+ });
+ }
+ return restReply;
+}
+
+void QRestAccessManagerPrivate::verifyThreadAffinity(const QObject *contextObject)
+{
+ Q_Q(QRestAccessManager);
+ if (QThread::currentThread() != q->thread()) {
+ qCWarning(lcQrest, "QRestAccessManager can only be called in the thread it belongs to");
+ Q_ASSERT(false);
+ }
+ if (contextObject && (contextObject->thread() != q->thread())) {
+ qCWarning(lcQrest, "QRestAccessManager: the context object must reside in the same thread");
+ Q_ASSERT(false);
+ }
+}
+
+void QRestAccessManagerPrivate::ensureNetworkAccessManager()
+{
+ Q_Q(QRestAccessManager);
+ if (!qnam) {
+ qnam = new QNetworkAccessManager(q);
+ connect(qnam, &QNetworkAccessManager::authenticationRequired, this,
+ &QRestAccessManagerPrivate::handleAuthenticationRequired);
+#ifndef QT_NO_NETWORKPROXY
+ QObject::connect(qnam, &QNetworkAccessManager::proxyAuthenticationRequired,
+ q, &QRestAccessManager::proxyAuthenticationRequired);
+#endif
+ }
+}
+
+void QRestAccessManagerPrivate::handleReplyFinished(QRestReply *restReply)
+{
+ Q_Q(QRestAccessManager);
+
+ auto request = activeRequests.find(restReply);
+ if (request == activeRequests.end()) {
+ qCWarning(lcQrest, "Unexpected reply received, ignoring");
+ return;
+ }
+
+ CallerInfo caller = request.value();
+ activeRequests.erase(request);
+
+ if (caller.slot) {
+ // Callback was provided. If we have context object, use it.
+ // For clarity: being here with a context object means it has not been destroyed
+ // while the request has been in progress
+ void *argv[] = { nullptr, &restReply };
+ QObject *context = caller.contextObject
+ ? const_cast<QObject*>(caller.contextObject) : nullptr;
+ caller.slot->call(context, argv);
+ }
+ if (restReply->hasError())
+ emit restReply->errorOccurred(restReply);
+ emit restReply->finished(restReply);
+ emit q->requestFinished(restReply);
+
+ if (deletesRepliesOnFinished)
+ restReply->deleteLater();
+}
+
+void QRestAccessManagerPrivate::handleAuthenticationRequired(QNetworkReply *networkReply,
+ QAuthenticator *authenticator)
+{
+ Q_Q(QRestAccessManager);
+ QRestReply *restReply = restReplyFromNetworkReply(networkReply);
+ if (restReply)
+ emit q->authenticationRequired(restReply, authenticator);
+ else
+ qCWarning(lcQrest, "No matching QRestReply for authentication, ignoring.");
+}
+
+QRestReply *QRestAccessManagerPrivate::restReplyFromNetworkReply(QNetworkReply *networkReply)
+{
+ for (const auto &[restReply,_] : activeRequests.asKeyValueRange()) {
+ if (restReply->networkReply() == networkReply)
+ return restReply;
+ }
+ return nullptr;
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qrestaccessmanager.cpp"
diff --git a/src/network/access/qrestaccessmanager.h b/src/network/access/qrestaccessmanager.h
new file mode 100644
index 0000000000..7b67486a2e
--- /dev/null
+++ b/src/network/access/qrestaccessmanager.h
@@ -0,0 +1,110 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QRESTACCESSMANAGER_H
+#define QRESTACCESSMANAGER_H
+
+#include <QtNetwork/qnetworkaccessmanager.h>
+
+QT_BEGIN_NAMESPACE
+
+class QRestReply;
+
+#define QREST_METHOD_WITH_DATA(METHOD, DATA) \
+public: \
+template <typename Functor, if_compatible_callback<Functor> = true> \
+QRestReply *METHOD(const QNetworkRequest &request, DATA data, \
+ const ContextTypeForFunctor<Functor> *context, \
+ Functor &&callback) \
+{ \
+ return METHOD##WithDataImpl(request, data, context, \
+ QtPrivate::makeCallableObject<CallbackPrototype>(std::forward<Functor>(callback))); \
+} \
+QRestReply *METHOD(const QNetworkRequest &request, DATA data) \
+{ \
+ return METHOD##WithDataImpl(request, data, nullptr, nullptr); \
+} \
+private: \
+QRestReply *METHOD##WithDataImpl(const QNetworkRequest &request, DATA data, \
+ const QObject *context, QtPrivate::QSlotObjectBase *slot); \
+/* end */
+
+
+#define QREST_METHOD_NO_DATA(METHOD) \
+public: \
+template <typename Functor, if_compatible_callback<Functor> = true> \
+QRestReply *METHOD(const QNetworkRequest &request, \
+ const ContextTypeForFunctor<Functor> *context, \
+ Functor &&callback) \
+{ \
+ return METHOD##NoDataImpl(request, context, \
+ QtPrivate::makeCallableObject<CallbackPrototype>(std::forward<Functor>(callback))); \
+} \
+QRestReply *METHOD(const QNetworkRequest &request) \
+{ \
+ return METHOD##NoDataImpl(request, nullptr, nullptr); \
+} \
+private: \
+QRestReply *METHOD##NoDataImpl(const QNetworkRequest &request, \
+ const QObject *context, QtPrivate::QSlotObjectBase *slot); \
+/* end */
+
+class QRestAccessManagerPrivate;
+class Q_NETWORK_EXPORT QRestAccessManager : public QObject
+{
+ Q_OBJECT
+
+ using CallbackPrototype = void(*)(QRestReply*);
+ template <typename Functor>
+ using ContextTypeForFunctor = typename QtPrivate::ContextTypeForFunctor<Functor>::ContextType;
+ template <typename Functor>
+ using if_compatible_callback = std::enable_if_t<
+ QtPrivate::AreFunctionsCompatible<CallbackPrototype, Functor>::value, bool>;
+public:
+ explicit QRestAccessManager(QObject *parent = nullptr);
+ ~QRestAccessManager() override;
+
+ QNetworkAccessManager *networkAccessManager() const;
+
+ bool deletesRepliesOnFinished() const;
+ void setDeletesRepliesOnFinished(bool autoDelete);
+
+ void abortRequests();
+
+ QREST_METHOD_NO_DATA(deleteResource)
+ QREST_METHOD_NO_DATA(head)
+ QREST_METHOD_NO_DATA(get)
+ QREST_METHOD_WITH_DATA(get, const QByteArray &)
+ QREST_METHOD_WITH_DATA(get, const QJsonObject &)
+ QREST_METHOD_WITH_DATA(get, QIODevice *)
+ QREST_METHOD_WITH_DATA(post, const QJsonObject &)
+ QREST_METHOD_WITH_DATA(post, const QJsonArray &)
+ QREST_METHOD_WITH_DATA(post, const QVariantMap &)
+ QREST_METHOD_WITH_DATA(post, const QByteArray &)
+ QREST_METHOD_WITH_DATA(post, QHttpMultiPart *)
+ QREST_METHOD_WITH_DATA(post, QIODevice *)
+ QREST_METHOD_WITH_DATA(put, const QJsonObject &)
+ QREST_METHOD_WITH_DATA(put, const QJsonArray &)
+ QREST_METHOD_WITH_DATA(put, const QVariantMap &)
+ QREST_METHOD_WITH_DATA(put, const QByteArray &)
+ QREST_METHOD_WITH_DATA(put, QHttpMultiPart *)
+ QREST_METHOD_WITH_DATA(put, QIODevice *)
+
+Q_SIGNALS:
+#ifndef QT_NO_NETWORKPROXY
+ void proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *authenticator);
+#endif
+ void authenticationRequired(QRestReply *reply, QAuthenticator *authenticator);
+ void requestFinished(QRestReply *reply);
+
+private:
+ Q_DECLARE_PRIVATE(QRestAccessManager)
+ Q_DISABLE_COPY(QRestAccessManager)
+};
+
+#undef QREST_METHOD_NO_DATA
+#undef QREST_METHOD_WITH_DATA
+
+QT_END_NAMESPACE
+
+#endif
diff --git a/src/network/access/qrestaccessmanager_p.h b/src/network/access/qrestaccessmanager_p.h
new file mode 100644
index 0000000000..91c6f89dd0
--- /dev/null
+++ b/src/network/access/qrestaccessmanager_p.h
@@ -0,0 +1,87 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QRESTACCESSMANAGER_P_H
+#define QRESTACCESSMANAGER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists for the convenience
+// of the Network Access API. This header file may change from
+// version to version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include "qrestaccessmanager.h"
+#include "private/qobject_p.h"
+
+#include <QtNetwork/qnetworkaccessmanager.h>
+
+#include <QtCore/qjsonarray.h>
+#include <QtCore/qhash.h>
+#include <QtCore/qjsondocument.h>
+#include <QtCore/qjsonobject.h>
+
+QT_BEGIN_NAMESPACE
+
+class QRestReply;
+class QRestAccessManagerPrivate : public QObjectPrivate
+{
+public:
+ QRestAccessManagerPrivate();
+ ~QRestAccessManagerPrivate() override;
+
+ void ensureNetworkAccessManager();
+
+ QRestReply *createActiveRequest(QNetworkReply *networkReply, const QObject *contextObject,
+ QtPrivate::QSlotObjectBase *slot);
+
+ void removeActiveRequest(QRestReply *restReply);
+ void handleReplyFinished(QRestReply *restReply);
+ void handleAuthenticationRequired(QNetworkReply *networkReply, QAuthenticator *authenticator);
+ QRestReply *restReplyFromNetworkReply(QNetworkReply *networkReply);
+
+ template<typename Functor>
+ QRestReply *executeRequest(Functor requestOperation,
+ const QObject *context, QtPrivate::QSlotObjectBase *slot)
+ {
+ verifyThreadAffinity(context);
+ QNetworkReply *reply = requestOperation();
+ return createActiveRequest(reply, context, slot);
+ }
+
+ template<typename Functor, typename Json>
+ QRestReply *executeRequest(Functor requestOperation, Json jsonData,
+ const QNetworkRequest &request,
+ const QObject *context, QtPrivate::QSlotObjectBase *slot)
+ {
+ verifyThreadAffinity(context);
+ QNetworkRequest req(request);
+ if (!request.header(QNetworkRequest::ContentTypeHeader).isValid()) {
+ req.setHeader(QNetworkRequest::ContentTypeHeader,
+ QLatin1StringView{"application/json"});
+ }
+ QJsonDocument json(jsonData);
+ QNetworkReply *reply = requestOperation(req, json.toJson(QJsonDocument::Compact));
+ return createActiveRequest(reply, context, slot);
+ }
+
+ void verifyThreadAffinity(const QObject *contextObject);
+
+ struct CallerInfo {
+ const QObject *contextObject = nullptr;
+ QtPrivate::SlotObjSharedPtr slot;
+ };
+ QHash<QRestReply*, CallerInfo> activeRequests;
+
+ QNetworkAccessManager *qnam = nullptr;
+ bool deletesRepliesOnFinished = true;
+ Q_DECLARE_PUBLIC(QRestAccessManager)
+};
+
+QT_END_NAMESPACE
+
+#endif
diff --git a/src/network/access/qrestreply.cpp b/src/network/access/qrestreply.cpp
new file mode 100644
index 0000000000..efff394f83
--- /dev/null
+++ b/src/network/access/qrestreply.cpp
@@ -0,0 +1,364 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qrestreply.h"
+#include "qrestreply_p.h"
+
+#include <QtCore/qjsonarray.h>
+#include <QtCore/qjsondocument.h>
+#include <QtCore/qjsonobject.h>
+#include <QtCore/qlatin1stringmatcher.h>
+#include <QtCore/qloggingcategory.h>
+#include <QtCore/qstringconverter.h>
+
+QT_BEGIN_NAMESPACE
+
+using namespace Qt::StringLiterals;
+Q_DECLARE_LOGGING_CATEGORY(lcQrest)
+
+/*!
+ \class QRestReply
+ \since 6.7
+ \brief QRestReply is the class for following up the requests sent with
+ QRestAccessManager.
+
+ \reentrant
+ \ingroup network
+ \inmodule QtNetwork
+
+ QRestReply is a convenience class for typical RESTful client
+ applications. It wraps the more detailed QNetworkReply and provides
+ convenience methods for data and status handling.
+
+ \sa QRestAccessManager, QNetworkReply
+*/
+
+/*!
+ \fn void QRestReply::finished(QRestReply *reply)
+
+ This signal is emitted when \a reply has finished processing. This
+ signal is emitted also in cases when the reply finished due to network
+ or protocol errors (the server did not reply with an HTTP status).
+
+ \sa isFinished(), httpStatus(), error()
+*/
+
+/*!
+ \fn void QRestReply::errorOccurred(QRestReply *reply)
+
+ This signal is emitted if, while processing \a reply, an error occurs that
+ is considered to be a network/protocol error. These errors are
+ disctinct from HTTP error responses such as \c {500 Internal Server Error}.
+ This signal is emitted together with the
+ finished() signal, and often connecting to that is sufficient.
+
+ \sa finished(), isFinished(), httpStatus(), error()
+*/
+
+QRestReply::QRestReply(QNetworkReply *reply, QObject *parent)
+ : QObject(*new QRestReplyPrivate, parent)
+{
+ Q_D(QRestReply);
+ Q_ASSERT(reply);
+ d->networkReply = reply;
+ // Reparent so that destruction of QRestReply destroys QNetworkReply
+ reply->setParent(this);
+}
+
+/*!
+ Destroys this QRestReply object.
+
+ \sa abort()
+*/
+QRestReply::~QRestReply()
+ = default;
+
+/*!
+ Returns a pointer to the underlying QNetworkReply wrapped by this object.
+*/
+QNetworkReply *QRestReply::networkReply() const
+{
+ Q_D(const QRestReply);
+ return d->networkReply;
+}
+
+/*!
+ Aborts the network operation immediately. The finished() signal
+ will be emitted.
+
+ \sa QRestAccessManager::abortRequests() QNetworkReply::abort()
+*/
+void QRestReply::abort()
+{
+ Q_D(QRestReply);
+ d->networkReply->abort();
+}
+
+/*!
+ Returns the received data as a QJsonObject. Requires the reply to be
+ finished.
+
+ The returned value is wrapped in \c std::optional. If the conversion
+ from the received data fails (empty data or JSON parsing error),
+ \c std::nullopt is returned.
+
+ Calling this function consumes the received data, and any further calls
+ to get response data will return empty.
+
+ This function returns \c {std::nullopt} and will not consume
+ any data if the reply is not finished.
+
+ \sa jsonArray(), body(), text(), finished(), isFinished()
+*/
+std::optional<QJsonObject> QRestReply::json()
+{
+ Q_D(QRestReply);
+ if (!isFinished()) {
+ qCWarning(lcQrest, "Attempt to read json() of an unfinished reply, ignoring.");
+ return std::nullopt;
+ }
+ const QJsonDocument json = d->replyDataToJson();
+ return json.isObject() ? std::optional{json.object()} : std::nullopt;
+}
+
+/*!
+ Returns the received data as a QJsonArray. Requires the reply to be
+ finished.
+
+ The returned value is wrapped in \c std::optional. If the conversion
+ from the received data fails (empty data or JSON parsing error),
+ \c std::nullopt is returned.
+
+ Calling this function consumes the received data, and any further calls
+ to get response data will return empty.
+
+ This function returns \c {std::nullopt} and will not consume
+ any data if the reply is not finished.
+
+ \sa json(), body(), text(), finished(), isFinished()
+*/
+std::optional<QJsonArray> QRestReply::jsonArray()
+{
+ Q_D(QRestReply);
+ if (!isFinished()) {
+ qCWarning(lcQrest, "Attempt to read jsonArray() of an unfinished reply, ignoring.");
+ return std::nullopt;
+ }
+ const QJsonDocument json = d->replyDataToJson();
+ return json.isArray() ? std::optional{json.array()} : std::nullopt;
+}
+
+/*!
+ Returns the received data as a QByteArray.
+
+ Calling this function consumes the data received so far, and any further
+ calls to get response data will return empty until further data has been
+ received.
+
+ \sa json(), text()
+*/
+QByteArray QRestReply::body()
+{
+ Q_D(QRestReply);
+ return d->networkReply->readAll();
+}
+
+/*!
+ Returns the received data as a QString. Requires the reply to be finished.
+
+ The received data is decoded into a QString (UTF-16). The decoding
+ uses the \e Content-Type header's \e charset parameter to determine the
+ source encoding, if available. If the encoding information is not
+ available or not supported by \l QStringConverter, UTF-8 is used as a
+ default.
+
+ Calling this function consumes the received data, and any further calls
+ to get response data will return empty.
+
+ This function returns a default-constructed value and will not consume
+ any data if the reply is not finished.
+
+ \sa json(), body(), isFinished(), finished()
+*/
+QString QRestReply::text()
+{
+ Q_D(QRestReply);
+ if (!isFinished()) {
+ qCWarning(lcQrest, "Attempt to read text() of an unfinished reply, ignoring.");
+ return {};
+ }
+ QByteArray data = d->networkReply->readAll();
+ if (data.isEmpty())
+ return {};
+
+ const QByteArray charset = d->contentCharset();
+ QStringDecoder decoder(charset);
+ if (!decoder.isValid()) { // the decoder may not support the mimetype's charset
+ qCWarning(lcQrest, "Charset \"%s\" is not supported, defaulting to UTF-8",
+ charset.constData());
+ decoder = QStringDecoder(QStringDecoder::Utf8);
+ }
+ return decoder(data);
+}
+
+/*!
+ Returns the HTTP status received in the server response.
+ The value is \e 0 if not available (the status line has not been received,
+ yet).
+
+ \note The HTTP status is reported as indicated by the received HTTP
+ response. It is possible that an error() occurs after receiving the status,
+ for instance due to network disconnection while receiving a long response.
+ These potential subsequent errors are not represented by the reported
+ HTTP status.
+
+ \sa isSuccess(), hasError(), error()
+*/
+int QRestReply::httpStatus() const
+{
+ Q_D(const QRestReply);
+ return d->networkReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+}
+
+/*!
+ \fn bool QRestReply::isSuccess() const
+
+ Returns whether the HTTP status is between 200..299 and no
+ further errors have occurred while receiving the response (for example
+ abrupt disconnection while receiving the body data). This function
+ is a convenient way to check whether the response is considered successful.
+
+ \sa httpStatus(), hasError(), error()
+*/
+
+/*!
+ Returns whether the HTTP status is between 200..299.
+
+ \sa isSuccess(), httpStatus(), hasError(), error()
+*/
+bool QRestReply::isHttpStatusSuccess() const
+{
+ const int status = httpStatus();
+ return status >= 200 && status < 300;
+}
+
+/*!
+ Returns whether an error has occurred. This includes errors such as
+ network and protocol errors, but excludes cases where the server
+ successfully responded with an HTTP error status (for example
+ \c {500 Internal Server Error}). Use \l httpStatus() or
+ \l isHttpStatusSuccess() to get the HTTP status information.
+
+ \sa httpStatus(), isSuccess(), error(), errorString()
+*/
+bool QRestReply::hasError() const
+{
+ Q_D(const QRestReply);
+ return d->hasNonHttpError();
+}
+
+/*!
+ Returns the last error, if any. The errors include
+ errors such as network and protocol errors, but exclude
+ cases when the server successfully responded with an HTTP status.
+
+ \sa httpStatus(), isSuccess(), hasError(), errorString()
+*/
+QNetworkReply::NetworkError QRestReply::error() const
+{
+ Q_D(const QRestReply);
+ if (!hasError())
+ return QNetworkReply::NetworkError::NoError;
+ return d->networkReply->error();
+}
+
+/*!
+ Returns a human-readable description of the last network error.
+
+ \sa httpStatus(), isSuccess(), hasError(), error()
+*/
+QString QRestReply::errorString() const
+{
+ Q_D(const QRestReply);
+ if (hasError())
+ return d->networkReply->errorString();
+ return {};
+}
+
+/*!
+ Returns whether the network request has finished.
+*/
+bool QRestReply::isFinished() const
+{
+ Q_D(const QRestReply);
+ return d->networkReply->isFinished();
+}
+
+QRestReplyPrivate::QRestReplyPrivate()
+ = default;
+
+QRestReplyPrivate::~QRestReplyPrivate()
+ = default;
+
+QByteArray QRestReplyPrivate::contentCharset() const
+{
+ // Content-type consists of mimetype and optional parameters, of which one may be 'charset'
+ // Example values and their combinations below are all valid, see RFC 7231 section 3.1.1.5
+ // and RFC 2045 section 5.1
+ //
+ // text/plain; charset=utf-8
+ // text/plain; charset=utf-8;version=1.7
+ // text/plain; charset = utf-8
+ // text/plain; charset ="utf-8"
+ QByteArray contentTypeValue =
+ networkReply->header(QNetworkRequest::KnownHeaders::ContentTypeHeader).toByteArray();
+ // Default to the most commonly used UTF-8.
+ QByteArray charset{"UTF-8"};
+
+ QList<QByteArray> parameters = contentTypeValue.split(';');
+ if (parameters.size() >= 2) { // Need at least one parameter in addition to the mimetype itself
+ parameters.removeFirst(); // Exclude the mimetype itself, only interested in parameters
+ QLatin1StringMatcher matcher("charset="_L1, Qt::CaseSensitivity::CaseInsensitive);
+ qsizetype matchIndex = -1;
+ for (auto &parameter : parameters) {
+ // Remove whitespaces and parantheses
+ const QByteArray curated = parameter.replace(" ", "").replace("\"","");
+ // Check for match
+ matchIndex = matcher.indexIn(QLatin1String(curated.constData()));
+ if (matchIndex >= 0) {
+ charset = curated.sliced(matchIndex + 8); // 8 is size of "charset="
+ break;
+ }
+ }
+ }
+ return charset;
+}
+
+// Returns true if there's an error that isn't appropriately indicated by the HTTP status
+bool QRestReplyPrivate::hasNonHttpError() const
+{
+ const int status = networkReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ if (status > 0) {
+ // The HTTP status is set upon receiving the response headers, but the
+ // connection might still fail later while receiving the body data.
+ return networkReply->error() == QNetworkReply::RemoteHostClosedError;
+ }
+ return networkReply->error() != QNetworkReply::NoError;
+}
+
+QJsonDocument QRestReplyPrivate::replyDataToJson()
+{
+ QJsonParseError parseError;
+ const QByteArray data = networkReply->readAll();
+ const QJsonDocument json = QJsonDocument::fromJson(data, &parseError);
+
+ if (parseError.error != QJsonParseError::NoError) {
+ qCDebug(lcQrest) << "Response data not JSON:" << parseError.errorString()
+ << "at" << parseError.offset << data;
+ }
+ return json;
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qrestreply.cpp"
diff --git a/src/network/access/qrestreply.h b/src/network/access/qrestreply.h
new file mode 100644
index 0000000000..6e35b45003
--- /dev/null
+++ b/src/network/access/qrestreply.h
@@ -0,0 +1,55 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QRESTREPLY_H
+#define QRESTREPLY_H
+
+#include <QtNetwork/qnetworkreply.h>
+
+QT_BEGIN_NAMESPACE
+
+class QRestReplyPrivate;
+class Q_NETWORK_EXPORT QRestReply : public QObject
+{
+ Q_OBJECT
+
+public:
+ ~QRestReply() override;
+
+ QNetworkReply *networkReply() const;
+
+ std::optional<QJsonObject> json();
+ std::optional<QJsonArray> jsonArray();
+ QByteArray body();
+ QString text();
+
+ bool isSuccess() const
+ {
+ return !hasError() && isHttpStatusSuccess();
+ }
+ int httpStatus() const;
+ bool isHttpStatusSuccess() const;
+
+ bool hasError() const;
+ QNetworkReply::NetworkError error() const;
+ QString errorString() const;
+
+ bool isFinished() const;
+
+public Q_SLOTS:
+ void abort();
+
+Q_SIGNALS:
+ void finished(QRestReply *reply);
+ void errorOccurred(QRestReply *reply);
+
+private:
+ friend class QRestAccessManagerPrivate;
+ explicit QRestReply(QNetworkReply *reply, QObject *parent = nullptr);
+ Q_DECLARE_PRIVATE(QRestReply)
+ Q_DISABLE_COPY_MOVE(QRestReply)
+};
+
+QT_END_NAMESPACE
+
+#endif // QRESTREPLY_H
diff --git a/src/network/access/qrestreply_p.h b/src/network/access/qrestreply_p.h
new file mode 100644
index 0000000000..4b156b3793
--- /dev/null
+++ b/src/network/access/qrestreply_p.h
@@ -0,0 +1,39 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QRESTREPLY_P_H
+#define QRESTREPLY_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists for the convenience
+// of the Network Access API. This header file may change from
+// version to version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include "private/qobject_p.h"
+#include <QtNetwork/qnetworkreply.h>
+#include <QtCore/qjsondocument.h>
+
+QT_BEGIN_NAMESPACE
+
+class QRestReplyPrivate : public QObjectPrivate
+{
+public:
+ QRestReplyPrivate();
+ ~QRestReplyPrivate() override;
+
+ QNetworkReply *networkReply = nullptr;
+
+ QByteArray contentCharset() const;
+ bool hasNonHttpError() const;
+ QJsonDocument replyDataToJson();
+};
+
+QT_END_NAMESPACE
+
+#endif
diff --git a/src/network/doc/snippets/code/src_network_access_qrestaccessmanager.cpp b/src/network/doc/snippets/code/src_network_access_qrestaccessmanager.cpp
new file mode 100644
index 0000000000..89c7348483
--- /dev/null
+++ b/src/network/doc/snippets/code/src_network_access_qrestaccessmanager.cpp
@@ -0,0 +1,84 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+//! [0]
+QRestReply *reply = manager->get(request);
+QObject::connect(reply, &QRestReply::finished, this, &MyClass::handleFinished);
+//! [0]
+
+
+//! [1]
+// With lambda
+manager->get(request, this, [this](QRestReply *reply) {
+ if (reply->isSuccess()) {
+ // ...
+ }
+});
+// With member function
+manager->get(request, this, &MyClass::handleFinished);
+//! [1]
+
+
+//! [2]
+QJsonObject myJson;
+// ...
+manager->post(request, myJson, this, [this](QRestReply *reply) {
+ if (!reply->isSuccess()) {
+ // ...
+ }
+ if (std::optional<QJsonObject> json = reply->json()) {
+ // use *json
+ }
+});
+//! [2]
+
+
+//! [3]
+manager->get(request, this, [this](QRestReply *reply) {
+ if (!reply->isSuccess())
+ // handle error
+ if (std::optional<QJsonObject> json = reply->json())
+ // use *json
+});
+//! [3]
+
+
+//! [4]
+manager->get(request, myData, this, [this](QRestReply *reply) {
+ if (reply->isSuccess())
+ // ...
+});
+//! [4]
+
+
+//! [5]
+manager->post(request, myData, this, [this](QRestReply *reply) {
+ if (reply->isSuccess())
+ // ...
+});
+//! [5]
+
+
+//! [6]
+manager->put(request, myData, this, [this](QRestReply *reply) {
+ if (reply->isSuccess())
+ // ...
+});
+//! [6]
+
+
+//! [7]
+manager->head(request, this, [this](QRestReply *reply) {
+ if (reply->isSuccess())
+ // ...
+});
+//! [7]
+
+
+//! [8]
+manager->deleteResource(request, this, [this](QRestReply *reply) {
+ if (reply->isSuccess())
+ // ...
+});
+//! [8]
+
diff --git a/tests/auto/network/access/CMakeLists.txt b/tests/auto/network/access/CMakeLists.txt
index 91c886d3fb..741e6b7879 100644
--- a/tests/auto/network/access/CMakeLists.txt
+++ b/tests/auto/network/access/CMakeLists.txt
@@ -11,6 +11,7 @@ add_subdirectory(qnetworkrequestfactory)
add_subdirectory(qnetworkreply)
add_subdirectory(qnetworkcachemetadata)
add_subdirectory(qabstractnetworkcache)
+add_subdirectory(qrestaccessmanager)
if(QT_FEATURE_private_tests)
add_subdirectory(qhttpheaderparser)
add_subdirectory(qhttpnetworkconnection)
diff --git a/tests/auto/network/access/qrestaccessmanager/CMakeLists.txt b/tests/auto/network/access/qrestaccessmanager/CMakeLists.txt
new file mode 100644
index 0000000000..08e452331f
--- /dev/null
+++ b/tests/auto/network/access/qrestaccessmanager/CMakeLists.txt
@@ -0,0 +1,11 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_test(tst_qrestaccessmanager
+ SOURCES
+ tst_qrestaccessmanager.cpp
+ httptestserver.cpp httptestserver_p.h
+ LIBRARIES
+ Qt::Network
+ Qt::CorePrivate
+)
diff --git a/tests/auto/network/access/qrestaccessmanager/httptestserver.cpp b/tests/auto/network/access/qrestaccessmanager/httptestserver.cpp
new file mode 100644
index 0000000000..ff222dbfb2
--- /dev/null
+++ b/tests/auto/network/access/qrestaccessmanager/httptestserver.cpp
@@ -0,0 +1,243 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "httptestserver_p.h"
+
+#include <QtNetwork/qtcpsocket.h>
+
+#include <QtCore/qcoreapplication.h>
+#include <private/qlocale_p.h>
+
+using namespace Qt::StringLiterals;
+
+static constexpr char CRLF[] = "\r\n";
+
+HttpTestServer::HttpTestServer(QObject *parent) : QTcpServer(parent)
+{
+ QObject::connect(this, &QTcpServer::newConnection, this, &HttpTestServer::handleConnected);
+ const auto ok = listen(QHostAddress::LocalHost);
+ Q_ASSERT(ok);
+};
+
+HttpTestServer::~HttpTestServer()
+{
+ if (isListening())
+ close();
+}
+
+QUrl HttpTestServer::url()
+{
+ return QUrl(u"http://127.0.0.1:%1"_s.arg(serverPort()));
+}
+
+void HttpTestServer::setHandler(const Handler &handler) {
+ m_handler = handler;
+}
+
+void HttpTestServer::handleConnected()
+{
+ Q_ASSERT(!m_socket); // No socket must exist previously, this is a single-connection server
+ m_socket = nextPendingConnection();
+ Q_ASSERT(m_socket);
+ QObject::connect(m_socket, &QTcpSocket::readyRead,
+ this, &HttpTestServer::handleDataAvailable);
+}
+
+void HttpTestServer::handleDataAvailable()
+{
+ Q_ASSERT(m_socket);
+ bool ok = true;
+
+ // Parse the incoming request data into the HttpData object
+ while (m_socket->bytesAvailable()) {
+ if (state == State::ReadingMethod && !(ok = readMethod(m_socket)))
+ qWarning("Invalid Method");
+ if (ok && state == State::ReadingUrl && !(ok = readUrl(m_socket)))
+ qWarning("Invalid URL");
+ if (ok && state == State::ReadingStatus && !(ok = readStatus(m_socket)))
+ qWarning("Invalid Status");
+ if (ok && state == State::ReadingHeader && !(ok = readHeaders(m_socket)))
+ qWarning("Invalid Header");
+ if (ok && state == State::ReadingBody && !(ok = readBody(m_socket)))
+ qWarning("Invalid Body");
+ } // while bytes available
+
+ Q_ASSERT(ok);
+ Q_ASSERT(m_handler);
+ Q_ASSERT(state == State::AllDone);
+
+ if (m_request.headers.contains("Host")) {
+ const auto parts = m_request.headers["Host"].split(':');
+ m_request.url.setHost(parts.at(0));
+ if (parts.size() == 2)
+ m_request.url.setPort(parts.at(1).toUInt());
+ }
+ HttpData response;
+ // Inform the testcase about request and ask for response data
+ m_handler(m_request, response);
+
+ if (response.respond) {
+ QByteArray responseMessage;
+ responseMessage += "HTTP/1.1 ";
+ responseMessage += QByteArray::number(response.status);
+ responseMessage += CRLF;
+ // Insert headers if any
+ for (const auto &[name,value] : response.headers.asKeyValueRange()) {
+ responseMessage += name;
+ responseMessage += value;
+ responseMessage += CRLF;
+ }
+ responseMessage += CRLF;
+ responseMessage += response.body;
+
+ /*
+ qDebug() << "HTTPTestServer received request"
+ << "\nMethod:" << m_request.method
+ << "\nHeaders:" << m_request.headers
+ << "\nBody:" << m_request.body;
+ qDebug() << "HTTPTestServer sends response:" << responseMessage;
+ */
+ m_socket->write(responseMessage);
+ }
+ m_socket->disconnectFromHost();
+ m_request = {};
+ m_socket = nullptr; // deleted by QTcpServer during destruction
+ state = State::ReadingMethod;
+ fragment.clear();
+}
+
+bool HttpTestServer::readMethod(QTcpSocket *socket)
+{
+ bool finished = false;
+ while (socket->bytesAvailable() && !finished) {
+ const auto c = socket->read(1).at(0);
+ if (ascii_isspace(c))
+ finished = true;
+ else if (std::isupper(c) && fragment.size() < 8)
+ fragment += c;
+ else
+ return false;
+ }
+ if (finished) {
+ if (fragment == "HEAD")
+ method = Method::Head;
+ else if (fragment == "GET")
+ method = Method::Get;
+ else if (fragment == "PUT")
+ method = Method::Put;
+ else if (fragment == "POST")
+ method = Method::Post;
+ else if (fragment == "DELETE")
+ method = Method::Delete;
+ else
+ qWarning("Invalid operation %s", fragment.data());
+
+ state = State::ReadingUrl;
+ m_request.method = fragment;
+ fragment.clear();
+
+ return method != Method::Unknown;
+ }
+ return true;
+}
+
+bool HttpTestServer::readUrl(QTcpSocket *socket)
+{
+ bool finished = false;
+ while (socket->bytesAvailable() && !finished) {
+ const auto c = socket->read(1).at(0);
+ if (std::isspace(c))
+ finished = true;
+ else
+ fragment += c;
+ }
+ if (finished) {
+ if (!fragment.startsWith('/')) {
+ qWarning("Invalid URL path %s", fragment.constData());
+ return false;
+ }
+ m_request.url = QStringLiteral("http://127.0.0.1:") + QString::number(m_request.port) +
+ QString::fromUtf8(fragment);
+ state = State::ReadingStatus;
+ if (!m_request.url.isValid()) {
+ qWarning("Invalid URL %s", fragment.constData());
+ return false;
+ }
+ fragment.clear();
+ }
+ return true;
+}
+
+bool HttpTestServer::readStatus(QTcpSocket *socket)
+{
+ bool finished = false;
+ while (socket->bytesAvailable() && !finished) {
+ fragment += socket->read(1);
+ if (fragment.endsWith(CRLF)) {
+ finished = true;
+ fragment.resize(fragment.size() - 2);
+ }
+ }
+ if (finished) {
+ if (!std::isdigit(fragment.at(fragment.size() - 3)) ||
+ fragment.at(fragment.size() - 2) != '.' ||
+ !std::isdigit(fragment.at(fragment.size() - 1))) {
+ qWarning("Invalid version");
+ return false;
+ }
+ m_request.version = std::pair(fragment.at(fragment.size() - 3) - '0',
+ fragment.at(fragment.size() - 1) - '0');
+ state = State::ReadingHeader;
+ fragment.clear();
+ }
+ return true;
+}
+
+bool HttpTestServer::readHeaders(QTcpSocket *socket)
+{
+ while (socket->bytesAvailable()) {
+ fragment += socket->read(1);
+ if (fragment.endsWith(CRLF)) {
+ if (fragment == CRLF) {
+ state = State::ReadingBody;
+ fragment.clear();
+ return true;
+ } else {
+ fragment.chop(2);
+ const int index = fragment.indexOf(':');
+ if (index == -1)
+ return false;
+
+ QByteArray key = fragment.sliced(0, index).trimmed();
+ QByteArray value = fragment.sliced(index + 1).trimmed();
+ m_request.headers.insert(std::move(key), std::move(value));
+ fragment.clear();
+ }
+ }
+ }
+ return true;
+}
+
+bool HttpTestServer::readBody(QTcpSocket *socket)
+{
+ qint64 bytesLeft = 0;
+ if (m_request.headers.contains("Content-Length")) {
+ bool conversionResult;
+ bytesLeft = m_request.headers["Content-Length"].toInt(&conversionResult);
+ if (!conversionResult)
+ return false;
+ fragment.resize(bytesLeft);
+ }
+ while (bytesLeft) {
+ qint64 got = socket->read(&fragment.data()[fragment.size() - bytesLeft], bytesLeft);
+ if (got < 0)
+ return false; // error
+ bytesLeft -= got;
+ if (bytesLeft)
+ qApp->processEvents();
+ }
+ fragment.swap(m_request.body);
+ state = State::AllDone;
+ return true;
+}
+
diff --git a/tests/auto/network/access/qrestaccessmanager/httptestserver_p.h b/tests/auto/network/access/qrestaccessmanager/httptestserver_p.h
new file mode 100644
index 0000000000..3af34dd0f0
--- /dev/null
+++ b/tests/auto/network/access/qrestaccessmanager/httptestserver_p.h
@@ -0,0 +1,78 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#ifndef QRESTACCESSSMANAGER_HTTPTESTSERVER_P_H
+#define QRESTACCESSSMANAGER_HTTPTESTSERVER_P_H
+
+#include <QtNetwork/qtcpserver.h>
+
+#include <QtCore/qmap.h>
+#include <QtCore/qurl.h>
+
+// This struct is used for parsing the incoming network request data into, as well
+// as getting the response data from the testcase
+struct HttpData {
+ QUrl url;
+ int status = 0;
+ bool respond = true;
+ QByteArray body;
+ QByteArray method;
+ quint16 port = 0;
+ QPair<quint8, quint8> version;
+ QMap<QByteArray, QByteArray> headers;
+};
+
+// Simple HTTP server. Currently supports only one concurrent connection
+class HttpTestServer : public QTcpServer
+{
+ Q_OBJECT
+
+public:
+ explicit HttpTestServer(QObject *parent = nullptr);
+ ~HttpTestServer() override;
+
+ // Returns this server's URL for the testcase to send requests to
+ QUrl url();
+
+ enum class State {
+ ReadingMethod,
+ ReadingUrl,
+ ReadingStatus,
+ ReadingHeader,
+ ReadingBody,
+ AllDone
+ } state = State::ReadingMethod;
+
+ enum class Method {
+ Unknown,
+ Head,
+ Get,
+ Put,
+ Post,
+ Delete,
+ } method = Method::Unknown;
+
+ // Parsing helpers for incoming data => HttpData
+ bool readMethod(QTcpSocket *socket);
+ bool readUrl(QTcpSocket *socket);
+ bool readStatus(QTcpSocket *socket);
+ bool readHeaders(QTcpSocket *socket);
+ bool readBody(QTcpSocket *socket);
+ // Parsing-time buffer in case data is received a small chunk at a time (readyRead())
+ QByteArray fragment;
+
+ // Settable callback for testcase. Gives the received request data, and takes in response data
+ using Handler = std::function<void(const HttpData &request, HttpData &response)>;
+ void setHandler(const Handler &handler);
+
+private slots:
+ void handleConnected();
+ void handleDataAvailable();
+
+private:
+ QTcpSocket *m_socket = nullptr;
+ HttpData m_request;
+ Handler m_handler = nullptr;
+};
+
+#endif // QRESTACCESSSMANAGER_HTTPTESTSERVER_P_H
diff --git a/tests/auto/network/access/qrestaccessmanager/tst_qrestaccessmanager.cpp b/tests/auto/network/access/qrestaccessmanager/tst_qrestaccessmanager.cpp
new file mode 100644
index 0000000000..0dd6bdd9c1
--- /dev/null
+++ b/tests/auto/network/access/qrestaccessmanager/tst_qrestaccessmanager.cpp
@@ -0,0 +1,794 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "httptestserver_p.h"
+
+#include <QtNetwork/qhttpmultipart.h>
+#include <QtNetwork/qrestaccessmanager.h>
+#include <QtNetwork/qauthenticator.h>
+#include <QtNetwork/qrestreply.h>
+
+#include <QTest>
+#include <QtTest/qsignalspy.h>
+
+#include <QtCore/qbuffer.h>
+#include <QtCore/qjsonobject.h>
+#include <QtCore/qjsondocument.h>
+#include <QtCore/qjsonarray.h>
+#include <QtCore/qstringconverter.h>
+
+using namespace Qt::StringLiterals;
+
+class tst_QRestAccessManager : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void initTestCase();
+
+ void initialization();
+ void destruction();
+ void callbacks();
+ void threading();
+ void networkRequestReply();
+ void abort();
+ void authentication();
+ void errors();
+ void body();
+ void json();
+ void text();
+
+private:
+ void memberHandler(QRestReply *reply);
+
+ friend class Transient;
+ QList<QRestReply*> m_expectedReplies;
+ QList<QRestReply*> m_actualReplies;
+};
+
+void tst_QRestAccessManager::initTestCase()
+{
+ qRegisterMetaType<QRestReply*>(); // For QSignalSpy
+}
+
+void tst_QRestAccessManager::initialization()
+{
+ QRestAccessManager manager;
+ QVERIFY(manager.networkAccessManager());
+ QCOMPARE(manager.deletesRepliesOnFinished(), true);
+}
+
+#define VERIFY_REPLY_OK(METHOD) \
+ QTRY_VERIFY(replyFromServer); \
+ QCOMPARE(serverSideRequest.method, METHOD); \
+ QVERIFY(replyFromServer->isSuccess()); \
+ QVERIFY(!replyFromServer->hasError()); \
+ replyFromServer->deleteLater(); \
+ replyFromServer = nullptr; \
+
+void tst_QRestAccessManager::networkRequestReply()
+{
+ // A basic test for each HTTP method against the local testserver.
+ QRestAccessManager manager;
+ manager.setDeletesRepliesOnFinished(false);
+ HttpTestServer server;
+ QTRY_VERIFY(server.isListening());
+ QNetworkRequest request(server.url());
+ request.setRawHeader("Content-Type"_ba, "text/plain"); // To silence missing content-type warn
+ QRestReply *replyFromServer = nullptr;
+ std::unique_ptr<QHttpMultiPart> multiPart;
+ QHttpPart part;
+ part.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"text\""));
+ part.setBody("multipart_text");
+ QByteArray ioDeviceData{"io_device_data"_ba};
+ QBuffer bufferIoDevice(&ioDeviceData);
+
+ HttpData serverSideRequest; // The request data the server received
+ HttpData serverSideResponse; // The response data the server responds with
+ serverSideResponse.status = 200;
+ server.setHandler([&](HttpData request, HttpData &response) {
+ serverSideRequest = request;
+ response = serverSideResponse;
+
+ });
+ auto callback = [&](QRestReply *reply) { replyFromServer = reply; };
+ const QByteArray byteArrayData{"some_data"_ba};
+ const QJsonObject jsonObjectData{{"key1", "value1"}, {"key2", "value2"}};
+ const QJsonArray jsonArrayData{{"arrvalue1", "arrvalue2", QJsonObject{{"key1", "value1"}}}};
+ const QVariantMap variantMapData{{"key1", "value1"}, {"key2", "value2"}};
+ const QByteArray methodDELETE{"DELETE"_ba};
+ const QByteArray methodHEAD{"HEAD"_ba};
+ const QByteArray methodPOST{"POST"_ba};
+ const QByteArray methodGET{"GET"_ba};
+ const QByteArray methodPUT{"PUT"_ba};
+
+ // DELETE
+ manager.deleteResource(request, this, callback);
+ VERIFY_REPLY_OK(methodDELETE);
+ QCOMPARE(serverSideRequest.body, ""_ba);
+
+ // HEAD
+ manager.head(request, this, callback);
+ VERIFY_REPLY_OK(methodHEAD);
+ QCOMPARE(serverSideRequest.body, ""_ba);
+
+ // GET
+ manager.get(request, this, callback);
+ VERIFY_REPLY_OK(methodGET);
+ QCOMPARE(serverSideRequest.body, ""_ba);
+
+ manager.get(request, byteArrayData, this, callback);
+ VERIFY_REPLY_OK(methodGET);
+ QCOMPARE(serverSideRequest.body, byteArrayData);
+
+ manager.get(request, jsonObjectData, this, callback);
+ VERIFY_REPLY_OK(methodGET);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData);
+
+ manager.get(request, &bufferIoDevice, this, callback);
+ VERIFY_REPLY_OK(methodGET);
+ QCOMPARE(serverSideRequest.body, ioDeviceData);
+
+ // POST
+ manager.post(request, byteArrayData, this, callback);
+ VERIFY_REPLY_OK(methodPOST);
+ QCOMPARE(serverSideRequest.body, byteArrayData);
+
+ manager.post(request, jsonObjectData, this, callback);
+ VERIFY_REPLY_OK(methodPOST);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData);
+
+ manager.post(request, jsonArrayData, this, callback);
+ VERIFY_REPLY_OK(methodPOST);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).array(), jsonArrayData);
+
+ manager.post(request, variantMapData, this, callback);
+ VERIFY_REPLY_OK(methodPOST);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData);
+
+ multiPart = std::make_unique<QHttpMultiPart>(QHttpMultiPart::FormDataType);
+ multiPart->append(part);
+ manager.post(request, multiPart.get(), this, callback);
+ VERIFY_REPLY_OK(methodPOST);
+ QVERIFY(serverSideRequest.body.contains("--boundary"_ba));
+ QVERIFY(serverSideRequest.body.contains("multipart_text"_ba));
+
+ manager.post(request, &bufferIoDevice, this, callback);
+ VERIFY_REPLY_OK(methodPOST);
+ QCOMPARE(serverSideRequest.body, ioDeviceData);
+
+ // PUT
+ manager.put(request, byteArrayData, this, callback);
+ VERIFY_REPLY_OK(methodPUT);
+ QCOMPARE(serverSideRequest.body, byteArrayData);
+
+ manager.put(request, jsonObjectData, this, callback);
+ VERIFY_REPLY_OK(methodPUT);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData);
+
+ manager.put(request, jsonArrayData, this, callback);
+ VERIFY_REPLY_OK(methodPUT);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).array(), jsonArrayData);
+
+ manager.put(request, variantMapData, this, callback);
+ VERIFY_REPLY_OK(methodPUT);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData);
+
+ multiPart = std::make_unique<QHttpMultiPart>(QHttpMultiPart::FormDataType);
+ multiPart->append(part);
+ manager.put(request, multiPart.get(), this, callback);
+ VERIFY_REPLY_OK(methodPUT);
+ QVERIFY(serverSideRequest.body.contains("--boundary"_ba));
+ QVERIFY(serverSideRequest.body.contains("multipart_text"_ba));
+
+ manager.put(request, &bufferIoDevice, this, callback);
+ VERIFY_REPLY_OK(methodPUT);
+ QCOMPARE(serverSideRequest.body, ioDeviceData);
+
+ //These must NOT compile
+ //manager.get(request, [](){}); // callback without context object
+ //manager.get(request, ""_ba, [](){}); // callback without context object
+ //manager.get(request, QString()); // wrong datatype
+ //manager.get(request, 123); // wrong datatype
+ //manager.post(request, QString()); // wrong datatype
+ //manager.put(request, 123); // wrong datatype
+ //manager.post(request); // data is required
+ //manager.put(request, QString()); // wrong datatype
+ //manager.put(request); // data is required
+ //manager.deleteResource(request, "f"_ba); // data not allowed
+ //manager.head(request, "f"_ba); // data not allowed
+ //manager.post(request, ""_ba, this, [](int param){}); // Wrong callback signature
+ //manager.get(request, this, [](int param){}); // Wrong callback signature
+}
+
+void tst_QRestAccessManager::abort()
+{
+ // Test aborting requests
+ QRestAccessManager manager;
+ HttpTestServer server;
+ QTRY_VERIFY(server.isListening());
+ QNetworkRequest request(server.url());
+
+ QSignalSpy finishedSpy(&manager, &QRestAccessManager::requestFinished);
+ int callbackCount = 0;
+ auto callback = [&](QRestReply*) {
+ callbackCount++;
+ };
+
+ // Abort without any requests
+ manager.abortRequests();
+ QTest::qWait(20);
+ QCOMPARE(finishedSpy.size(), 0);
+
+ // Abort immediately after requesting
+ manager.get(request, this, callback);
+ manager.abortRequests();
+ QTRY_COMPARE(callbackCount, 1);
+ QTRY_COMPARE(finishedSpy.size(), 1);
+
+ // Abort after request has been sent out
+ server.setHandler([&](HttpData, HttpData &response) {
+ response.respond = false;
+ manager.abortRequests();
+ });
+ manager.get(request, this, callback);
+ QTRY_COMPARE(callbackCount, 2);
+ QTRY_COMPARE(finishedSpy.size(), 2);
+}
+
+void tst_QRestAccessManager::memberHandler(QRestReply *reply)
+{
+ m_actualReplies.append(reply);
+}
+
+// Class that is destroyed during an active request.
+// Used to test that the callbacks won't be called in these cases
+class Transient : public QObject
+{
+ Q_OBJECT
+public:
+ explicit Transient(tst_QRestAccessManager *test) : QObject(test), m_test(test) {}
+
+ void memberHandler(QRestReply *reply)
+ {
+ m_test->m_actualReplies.append(reply);
+ }
+
+private:
+ tst_QRestAccessManager *m_test = nullptr;
+};
+
+template <typename Functor, std::enable_if_t<
+ QtPrivate::AreFunctionsCompatible<void(*)(QRestReply*), Functor>::value, bool> = true>
+inline constexpr bool isCompatibleCallback(Functor &&) { return true; }
+
+template <typename Functor, std::enable_if_t<
+ !QtPrivate::AreFunctionsCompatible<void(*)(QRestReply*), Functor>::value, bool> = true,
+ typename = void>
+inline constexpr bool isCompatibleCallback(Functor &&) { return false; }
+
+void tst_QRestAccessManager::callbacks()
+{
+ QRestAccessManager manager;
+
+ manager.setDeletesRepliesOnFinished(false); // Don't autodelete so we can compare results later
+ QNetworkRequest request{u"i_dont_exist"_s}; // Will result in ProtocolUnknown error
+ QSignalSpy managerFinishedSpy(&manager, &QRestAccessManager::requestFinished);
+
+ auto lambdaHandler = [this](QRestReply *reply) { m_actualReplies.append(reply); };
+ QRestReply *reply = nullptr;
+ Transient *transient = nullptr;
+ QByteArray data{"some_data"};
+
+ // Compile-time tests for callback signatures
+ static_assert(isCompatibleCallback([](QRestReply*){})); // Correct signature
+ static_assert(isCompatibleCallback(lambdaHandler));
+ static_assert(isCompatibleCallback(&Transient::memberHandler));
+ static_assert(isCompatibleCallback([](){})); // Less parameters are allowed
+
+ static_assert(!isCompatibleCallback([](QString){})); // Wrong parameter type
+ static_assert(!isCompatibleCallback([](QNetworkReply*){})); // Wrong parameter type
+ static_assert(!isCompatibleCallback([](const QString &){})); // Wrong parameter type
+ static_assert(!isCompatibleCallback([](QRestReply*, QString){})); // Too many parameters
+
+ // -- Test without data
+ // Without callback
+ reply = manager.get(request);
+ QCOMPARE(reply->isFinished(), false); // Test this once here
+ m_expectedReplies.append(reply);
+ QObject::connect(reply, &QRestReply::finished, lambdaHandler);
+
+ // With lambda callback, without context object
+ m_expectedReplies.append(manager.get(request, nullptr, lambdaHandler));
+ m_expectedReplies.append(manager.get(request, nullptr,
+ [this](QRestReply *reply){m_actualReplies.append(reply);}));
+ // With lambda callback and context object
+ m_expectedReplies.append(manager.get(request, this, lambdaHandler));
+ m_expectedReplies.append(manager.get(request, this,
+ [this](QRestReply *reply){m_actualReplies.append(reply);}));
+ // With member callback and context object
+ m_expectedReplies.append(manager.get(request, this, &tst_QRestAccessManager::memberHandler));
+ // With context object that is destroyed, there should be no callback or eg. crash.
+ transient = new Transient(this);
+ manager.get(request, transient, &Transient::memberHandler); // Reply not added to expecteds
+ delete transient;
+
+ // Let requests finish
+ QTRY_COMPARE(m_actualReplies.size(), m_expectedReplies.size());
+ QTRY_COMPARE(managerFinishedSpy.size(), m_actualReplies.size());
+ for (auto reply: m_actualReplies) {
+ QVERIFY(!reply->isSuccess());
+ QVERIFY(reply->hasError());
+ QCOMPARE(reply->error(), QNetworkReply::ProtocolUnknownError);
+ QCOMPARE(reply->isFinished(), true);
+ reply->deleteLater();
+ }
+ m_actualReplies.clear();
+ m_expectedReplies.clear();
+ managerFinishedSpy.clear();
+
+ // -- Test with data
+ reply = manager.post(request, data);
+ m_expectedReplies.append(reply);
+ QObject::connect(reply, &QRestReply::finished, lambdaHandler);
+
+ // With lambda callback, without context object
+ m_expectedReplies.append(manager.post(request, data, nullptr, lambdaHandler));
+ m_expectedReplies.append(manager.post(request, data, nullptr,
+ [this](QRestReply *reply){m_actualReplies.append(reply);}));
+ // With lambda callback and context object
+ m_expectedReplies.append(manager.post(request, data, this, lambdaHandler));
+ m_expectedReplies.append(manager.post(request, data, this,
+ [this](QRestReply *reply){m_actualReplies.append(reply);}));
+ // With member callback and context object
+ m_expectedReplies.append(manager.post(request, data,
+ this, &tst_QRestAccessManager::memberHandler));
+ // With context object that is destroyed, there should be no callback or eg. crash
+ transient = new Transient(this);
+ manager.post(request, data, transient, &Transient::memberHandler); // Note: reply not expected
+ delete transient;
+
+ // Let requests finish
+ QTRY_COMPARE(m_actualReplies.size(), m_expectedReplies.size());
+ QTRY_COMPARE(managerFinishedSpy.size(), m_actualReplies.size());
+ for (auto reply: m_actualReplies) {
+ QVERIFY(!reply->isSuccess());
+ QVERIFY(reply->hasError());
+ QCOMPARE(reply->error(), QNetworkReply::ProtocolUnknownError);
+ QCOMPARE(reply->isFinished(), true);
+ reply->deleteLater();
+ }
+ m_actualReplies.clear();
+ m_expectedReplies.clear();
+ managerFinishedSpy.clear();
+
+ // -- Test GET with data separately, as GET provides methods that are usable with and
+ // without data, and fairly easy to get the qrestaccessmanager.h template SFINAE subtly wrong
+ reply = manager.get(request, data);
+ m_expectedReplies.append(reply);
+ QObject::connect(reply, &QRestReply::finished, lambdaHandler);
+ // With lambda callback, without context object
+ m_expectedReplies.append(manager.get(request, data, nullptr, lambdaHandler));
+ m_expectedReplies.append(manager.get(request, data, nullptr,
+ [this](QRestReply *reply){m_actualReplies.append(reply);}));
+ // With lambda callback and context object
+ m_expectedReplies.append(manager.get(request, data, this, lambdaHandler));
+ m_expectedReplies.append(manager.get(request, data, this,
+ [this](QRestReply *reply){m_actualReplies.append(reply);}));
+ // With member callback and context object
+ m_expectedReplies.append(manager.get(request, data,
+ this, &tst_QRestAccessManager::memberHandler));
+ // With context object that is destroyed, there should be no callback or eg. crash
+ transient = new Transient(this);
+ manager.get(request, data, transient, &Transient::memberHandler); // Reply not added
+ delete transient;
+
+ // Let requests finish
+ QTRY_COMPARE(m_actualReplies.size(), m_expectedReplies.size());
+ QTRY_COMPARE(managerFinishedSpy.size(), m_actualReplies.size());
+ for (auto reply: m_actualReplies) {
+ QVERIFY(!reply->isSuccess());
+ QVERIFY(reply->hasError());
+ QCOMPARE(reply->error(), QNetworkReply::ProtocolUnknownError);
+ QCOMPARE(reply->isFinished(), true);
+ reply->deleteLater();
+ }
+ m_actualReplies.clear();
+ m_expectedReplies.clear();
+ managerFinishedSpy.clear();
+}
+
+class RestWorker : public QObject
+{
+ Q_OBJECT
+public:
+ explicit RestWorker(QObject *parent = nullptr) : QObject(parent)
+ {
+ m_manager = new QRestAccessManager(this);
+ }
+ QRestAccessManager *m_manager;
+
+public slots:
+ void issueRestRequest()
+ {
+ QNetworkRequest request{u"i_dont_exist"_s};
+ m_manager->get(request, this, [this](QRestReply *reply){
+ emit result(reply->body());
+ });
+ }
+signals:
+ void result(const QByteArray &data);
+};
+
+void tst_QRestAccessManager::threading()
+{
+ // QRestAccessManager and QRestReply are only allowed to use in the thread they live in.
+
+ // A "sanity test" for checking that there are no problems with running the QRestAM
+ // in another thread.
+ QThread restWorkThread;
+ RestWorker restWorker;
+ restWorker.moveToThread(&restWorkThread);
+
+ QList<QByteArray> results;
+ QObject::connect(&restWorker, &RestWorker::result, this, [&](const QByteArray &data){
+ results.append(data);
+ });
+ restWorkThread.start();
+
+ QMetaObject::invokeMethod(&restWorker, &RestWorker::issueRestRequest);
+ QTRY_COMPARE(results.size(), 1);
+ restWorkThread.quit();
+ restWorkThread.wait();
+}
+
+void tst_QRestAccessManager::destruction()
+{
+ QRestAccessManager *manager = new QRestAccessManager;
+ manager->setDeletesRepliesOnFinished(false); // Don't autodelete so we can compare results later
+ QNetworkRequest request{u"i_dont_exist"_s}; // Will result in ProtocolUnknown error
+ m_expectedReplies.clear();
+ m_actualReplies.clear();
+ auto handler = [this](QRestReply *reply) { m_actualReplies.append(reply); };
+
+ // Delete reply immediately, make sure nothing bad happens and that there is no callback
+ QRestReply *reply = manager->get(request, this, handler);
+ delete reply;
+ QTest::qWait(20); // allow some time for the callback to arrive (it shouldn't)
+ QCOMPARE(m_actualReplies.size(), m_expectedReplies.size()); // Both should be 0
+
+ // Delete access manager immediately after request, make sure nothing bad happens
+ manager->get(request, this, handler);
+ manager->post(request, "data"_ba, this, handler);
+ QTest::ignoreMessage(QtWarningMsg, "Access manager destroyed while 2 requests were still"
+ " in progress");
+ delete manager;
+ QTest::qWait(20);
+ QCOMPARE(m_actualReplies.size(), m_expectedReplies.size()); // Both should be 0
+}
+
+void tst_QRestAccessManager::authentication()
+{
+ // Test the case where server responds with '401' (authentication required).
+ // The QRestAM emits an authenticationRequired signal, which is used to the username/password.
+ // The QRestAM/QNAM underneath then automatically resends the request.
+ QRestAccessManager manager;
+ manager.setDeletesRepliesOnFinished(false);
+ HttpTestServer server;
+ QTRY_VERIFY(server.isListening());
+ QNetworkRequest request(server.url());
+ QRestReply *replyFromServer = nullptr;
+
+ HttpData serverSideRequest;
+ server.setHandler([&](HttpData request, HttpData &response) {
+ if (!request.headers.contains("Authorization"_ba)) {
+ response.status = 401;
+ response.headers.insert("WWW-Authenticate: "_ba, "Basic realm=\"secret_place\""_ba);
+ } else {
+ response.status = 200;
+ }
+ serverSideRequest = request; // store for checking later the 'Authorization' header value
+ });
+
+ QObject::connect(&manager, &QRestAccessManager::authenticationRequired, this,
+ [](QRestReply*, QAuthenticator *authenticator) {
+ authenticator->setUser(u"a_user"_s);
+ authenticator->setPassword(u"a_password"_s);
+ });
+
+ // Issue a GET request without any authorization data.
+ int finishedCount = 0;
+ manager.get(request, this, [&](QRestReply *reply) {
+ finishedCount++;
+ replyFromServer = reply;
+ });
+ QTRY_VERIFY(replyFromServer);
+ // Server and QRestAM/QNAM exchange req/res twice, but finished() should be emitted just once
+ QCOMPARE(finishedCount, 1);
+ QCOMPARE(serverSideRequest.headers["Authorization"_ba], "Basic YV91c2VyOmFfcGFzc3dvcmQ="_ba);
+}
+
+#define VERIFY_HTTP_ERROR_STATUS(STATUS) \
+ serverSideResponse.status = STATUS; \
+ reply = manager.get(request); \
+ QObject::connect(reply, &QRestReply::errorOccurred, this, \
+ [&](){ errorSignalReceived = true; }); \
+ QTRY_VERIFY(reply->isFinished()); \
+ QVERIFY(!errorSignalReceived); \
+ QVERIFY(!reply->hasError()); \
+ QCOMPARE(reply->httpStatus(), serverSideResponse.status); \
+ QCOMPARE(reply->error(), QNetworkReply::NetworkError::NoError); \
+ QVERIFY(!reply->isSuccess()); \
+ reply->deleteLater(); \
+
+void tst_QRestAccessManager::errors()
+{
+ // Tests the distinction between HTTP and other (network/protocol) errors
+ QRestAccessManager manager;
+ manager.setDeletesRepliesOnFinished(false);
+ HttpTestServer server;
+ QTRY_VERIFY(server.isListening());
+ QNetworkRequest request(server.url());
+ QRestReply *reply = nullptr;
+ bool errorSignalReceived = false;
+
+ HttpData serverSideResponse; // The response data the server responds with
+ server.setHandler([&](HttpData, HttpData &response) {
+ response = serverSideResponse;
+ });
+
+ // Test few HTTP statuses in different categories
+ VERIFY_HTTP_ERROR_STATUS(103) // QNetworkReply::NoError
+ VERIFY_HTTP_ERROR_STATUS(301) // QNetworkReply::ProtocolUnknownError
+ VERIFY_HTTP_ERROR_STATUS(302) // QNetworkReply::ProtocolUnknownError
+ VERIFY_HTTP_ERROR_STATUS(400) // QNetworkReply::ProtocolInvalidOperationError
+ VERIFY_HTTP_ERROR_STATUS(401) // QNetworkReply::AuthenticationRequiredEror
+ VERIFY_HTTP_ERROR_STATUS(402) // QNetworkReply::UnknownContentError
+ VERIFY_HTTP_ERROR_STATUS(403) // QNetworkReply::ContentAccessDenied
+ VERIFY_HTTP_ERROR_STATUS(404) // QNetworkReply::ContentNotFoundError
+ VERIFY_HTTP_ERROR_STATUS(405) // QNetworkReply::ContentOperationNotPermittedError
+ VERIFY_HTTP_ERROR_STATUS(406) // QNetworkReply::UnknownContentError
+ VERIFY_HTTP_ERROR_STATUS(407) // QNetworkReply::ProxyAuthenticationRequiredError
+ VERIFY_HTTP_ERROR_STATUS(408) // QNetworkReply::UnknownContentError
+ VERIFY_HTTP_ERROR_STATUS(409) // QNetworkReply::ContentConflictError
+ VERIFY_HTTP_ERROR_STATUS(410) // QNetworkReply::ContentGoneError
+ VERIFY_HTTP_ERROR_STATUS(500) // QNetworkReply::InternalServerError
+ VERIFY_HTTP_ERROR_STATUS(501) // QNetworkReply::OperationNotImplementedError
+ VERIFY_HTTP_ERROR_STATUS(502) // QNetworkReply::UnknownServerError
+ VERIFY_HTTP_ERROR_STATUS(503) // QNetworkReply::ServiceUnavailableError
+ VERIFY_HTTP_ERROR_STATUS(504) // QNetworkReply::UnknownServerError
+ VERIFY_HTTP_ERROR_STATUS(505) // QNetworkReply::UnknownServerError
+
+ // Test that actual network/protocol errors come through
+ reply = manager.get({}); // Empty url
+ QObject::connect(reply, &QRestReply::errorOccurred, this, [&](){ errorSignalReceived = true; });
+ QTRY_VERIFY(reply->isFinished());
+ QTRY_VERIFY(errorSignalReceived);
+ QVERIFY(reply->hasError());
+ QVERIFY(!reply->isSuccess());
+ QCOMPARE(reply->error(), QNetworkReply::ProtocolUnknownError);
+ reply->deleteLater();
+ errorSignalReceived = false;
+
+ reply = manager.get(QNetworkRequest{{"http://non-existent.foo.bar.test"}});
+ QObject::connect(reply, &QRestReply::errorOccurred, this, [&](){ errorSignalReceived = true; });
+ QTRY_VERIFY(reply->isFinished());
+ QTRY_VERIFY(errorSignalReceived);
+ QVERIFY(reply->hasError());
+ QVERIFY(!reply->isSuccess());
+ QCOMPARE(reply->error(), QNetworkReply::HostNotFoundError);
+ reply->deleteLater();
+ errorSignalReceived = false;
+
+ reply = manager.get(request);
+ QObject::connect(reply, &QRestReply::errorOccurred, this, [&](){ errorSignalReceived = true; });
+ reply->abort();
+ QTRY_VERIFY(reply->isFinished());
+ QTRY_VERIFY(errorSignalReceived);
+ QVERIFY(reply->hasError());
+ QVERIFY(!reply->isSuccess());
+ QCOMPARE(reply->error(), QNetworkReply::OperationCanceledError);
+ reply->deleteLater();
+ errorSignalReceived = false;
+}
+
+void tst_QRestAccessManager::body()
+{
+ // Test using QRestReply::body() data accessor
+ QRestAccessManager manager;
+ manager.setDeletesRepliesOnFinished(false);
+ HttpTestServer server;
+ QTRY_VERIFY(server.isListening());
+ QNetworkRequest request(server.url());
+ QRestReply *replyFromServer = nullptr;
+
+ HttpData serverSideRequest; // The request data the server received
+ HttpData serverSideResponse; // The response data the server responds with
+ server.setHandler([&](HttpData request, HttpData &response) {
+ serverSideRequest = request;
+ response = serverSideResponse;
+ });
+
+ serverSideResponse.status = 200;
+ serverSideResponse.body = "some_data"_ba;
+ manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
+ QTRY_VERIFY(replyFromServer);
+ QCOMPARE(replyFromServer->body(), serverSideResponse.body);
+ QCOMPARE(replyFromServer->httpStatus(), serverSideResponse.status);
+ QVERIFY(!replyFromServer->hasError());
+ QVERIFY(replyFromServer->isSuccess());
+ replyFromServer->deleteLater();
+ replyFromServer = nullptr;
+
+ serverSideResponse.body = ""_ba; // Empty
+ manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
+ QTRY_VERIFY(replyFromServer);
+ QCOMPARE(replyFromServer->body(), serverSideResponse.body);
+ replyFromServer->deleteLater();
+ replyFromServer = nullptr;
+
+ serverSideResponse.status = 500;
+ serverSideResponse.body = "some_other_data"_ba;
+ manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
+ QTRY_VERIFY(replyFromServer);
+ QCOMPARE(replyFromServer->body(), serverSideResponse.body);
+ QCOMPARE(replyFromServer->httpStatus(), serverSideResponse.status);
+ QVERIFY(!replyFromServer->hasError());
+ QVERIFY(!replyFromServer->isSuccess());
+ replyFromServer->deleteLater();
+ replyFromServer = nullptr;
+}
+
+void tst_QRestAccessManager::json()
+{
+ // Test using QRestReply::json() and jsonArray() data accessors
+ QRestAccessManager manager;
+ manager.setDeletesRepliesOnFinished(false);
+ HttpTestServer server;
+ QTRY_VERIFY(server.isListening());
+ QNetworkRequest request(server.url());
+ QRestReply *replyFromServer = nullptr;
+ QJsonObject responseJsonObject;
+ QJsonArray responseJsonArray;
+
+ HttpData serverSideRequest; // The request data the server received
+ HttpData serverSideResponse; // The response data the server responds with
+ serverSideResponse.status = 200;
+ server.setHandler([&](HttpData request, HttpData &response) {
+ serverSideRequest = request;
+ response = serverSideResponse;
+ });
+
+ // Test receiving valid json object
+ serverSideResponse.body = "{\"key1\":\"value1\",""\"key2\":\"value2\"}\n"_ba;
+ manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
+ QTRY_VERIFY(replyFromServer);
+ responseJsonObject = *replyFromServer->json();
+ QCOMPARE(responseJsonObject["key1"], "value1");
+ QCOMPARE(responseJsonObject["key2"], "value2");
+ replyFromServer->deleteLater();
+ replyFromServer = nullptr;
+
+ // Test receiving an invalid json object
+ serverSideResponse.body = "foobar"_ba;
+ manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
+ QTRY_VERIFY(replyFromServer);
+ QVERIFY(!replyFromServer->json().has_value()); // std::nullopt returned
+ replyFromServer->deleteLater();
+ replyFromServer = nullptr;
+
+ // Test receiving valid json array
+ serverSideResponse.body = "[\"foo\", \"bar\"]\n"_ba;
+ manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
+ QTRY_VERIFY(replyFromServer);
+ responseJsonArray = *replyFromServer->jsonArray();
+ QCOMPARE(responseJsonArray.size(), 2);
+ QCOMPARE(responseJsonArray[0].toString(), "foo"_L1);
+ QCOMPARE(responseJsonArray[1].toString(), "bar"_L1);
+ replyFromServer->deleteLater();
+ replyFromServer = nullptr;
+
+ // Test receiving an invalid json array
+ serverSideResponse.body = "foobar"_ba;
+ manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
+ QTRY_VERIFY(replyFromServer);
+ QVERIFY(!replyFromServer->jsonArray().has_value()); // std::nullopt returned
+ replyFromServer->deleteLater();
+ replyFromServer = nullptr;
+}
+
+#define VERIFY_TEXT_REPLY_OK \
+ QTRY_VERIFY(replyFromServer); \
+ responseString = replyFromServer->text(); \
+ QCOMPARE(responseString, sourceString); \
+ replyFromServer->deleteLater(); \
+ replyFromServer = nullptr; \
+
+void tst_QRestAccessManager::text()
+{
+ // Test using QRestReply::text() data accessor with various text encodings
+ QRestAccessManager manager;
+ manager.setDeletesRepliesOnFinished(false);
+ HttpTestServer server;
+ QTRY_VERIFY(server.isListening());
+ QNetworkRequest request(server.url());
+ QRestReply *replyFromServer = nullptr;
+ QJsonObject responseJsonObject;
+
+ QStringEncoder encUTF8("UTF-8");
+ QStringEncoder encUTF16("UTF-16");
+ QStringEncoder encUTF32("UTF-32");
+ QString responseString;
+
+ HttpData serverSideRequest; // The request data the server received
+ HttpData serverSideResponse; // The response data the server responds with
+ serverSideResponse.status = 200;
+ server.setHandler([&](HttpData request, HttpData &response) {
+ serverSideRequest = request;
+ response = serverSideResponse;
+ });
+
+ const QString sourceString("this is a string"_L1);
+
+ // Charset parameter of Content-Type header may specify non-UTF-8 character encoding.
+ //
+ // QString is UTF-16, and in the tests below we encode the response data to various
+ // charset encodings (into byte arrays). When we get the response data, the text()
+ // should consider the indicated charset and convert it to an UTF-16 QString => the returned
+ // QString from text() should match with the original (UTF-16) QString.
+
+ // Successful UTF-8
+ serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; charset=UTF-8"_ba);
+ serverSideResponse.body = encUTF8(sourceString);
+ manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
+ VERIFY_TEXT_REPLY_OK;
+
+ // Successful UTF-16
+ serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; charset=UTF-16"_ba);
+ serverSideResponse.body = encUTF16(sourceString);
+ manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
+ VERIFY_TEXT_REPLY_OK;
+
+ // Successful UTF-16, parameter case insensitivity
+ serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; chARset=uTf-16"_ba);
+ serverSideResponse.body = encUTF16(sourceString);
+ manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
+ VERIFY_TEXT_REPLY_OK;
+
+ // Successful UTF-32
+ serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; charset=UTF-32"_ba);
+ serverSideResponse.body = encUTF32(sourceString);
+ manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
+ VERIFY_TEXT_REPLY_OK;
+
+ // Successful UTF-32 with spec-wise allowed extra content in the Content-Type header value
+ serverSideResponse.headers.insert("Content-Type:"_ba,
+ "text/plain; charset = \"UTF-32\";extraparameter=bar"_ba);
+ serverSideResponse.body = encUTF32(sourceString);
+ manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
+ VERIFY_TEXT_REPLY_OK;
+
+ // Unsuccessful UTF-32, wrong encoding indicated (indicated charset UTF-32 but data is UTF-8)
+ serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; charset=UTF-32"_ba);
+ serverSideResponse.body = encUTF8(sourceString);
+ manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
+ QTRY_VERIFY(replyFromServer);
+ responseString = replyFromServer->text();
+ QCOMPARE_NE(responseString, sourceString);
+ replyFromServer->deleteLater();
+ replyFromServer = nullptr;
+
+ // Unsupported encoding, defaults to UTF-8
+ serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; charset=foo"_ba);
+ serverSideResponse.body = encUTF8(sourceString);
+ manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
+ QTRY_VERIFY(replyFromServer);
+ QTest::ignoreMessage(QtWarningMsg, "Charset \"foo\" is not supported, defaulting to UTF-8");
+ responseString = replyFromServer->text();
+ QCOMPARE(responseString, sourceString);
+ replyFromServer->deleteLater();
+ replyFromServer = nullptr;
+}
+
+QTEST_MAIN(tst_QRestAccessManager)
+#include "tst_qrestaccessmanager.moc"