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>
bb10
Juha Vuolle 2023-06-12 11:23:19 +03:00
parent f587ba1036
commit e560adef21
13 changed files with 2687 additions and 0 deletions

View File

@ -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
)

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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)

View File

@ -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
)

View File

@ -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;
}

View File

@ -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

View File

@ -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"