QHttpServerResponder: refactor and extend the API

Currently QHttpServerResponder is not flexible enough:
 - Does not allow to rewrite headers
 - Does not allow to write headers without storing them internaly first
 - Does not provide API to make your own HTTP response

This patch will help to implement QHttpServerResponse
setHeaders/addHeaders

Change-Id: If9e21f7f7a58629bfedad0f10cab67d67fce0a89
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
This commit is contained in:
Mikhail Svetkin 2019-07-18 21:30:10 +02:00
parent b63b0862c9
commit 055d36692b
5 changed files with 312 additions and 126 deletions

View File

@ -55,8 +55,12 @@ static const std::map<QHttpServerResponder::StatusCode, QByteArray> statusString
#undef XX #undef XX
}; };
static const QByteArray contentTypeString(QByteArrayLiteral("Content-Type")); static const QByteArray contentTypeHeader(QByteArrayLiteral("Content-Type"));
static const QByteArray contentLengthString(QByteArrayLiteral("Content-Length")); static const QByteArray contentLengthHeader(QByteArrayLiteral("Content-Length"));
static const QByteArray contentTypeEmpty(QByteArrayLiteral("application/x-empty"));
static const QByteArray contentTypeJson(QByteArrayLiteral("text/json"));
template <qint64 BUFFERSIZE = 512> template <qint64 BUFFERSIZE = 512>
struct IOChunkedTransfer struct IOChunkedTransfer
@ -165,8 +169,8 @@ QHttpServerResponder::~QHttpServerResponder()
{} {}
/*! /*!
Answers a request with an HTTP status code \a status and a Answers a request with an HTTP status code \a status and
MIME type \a mimeType. The I/O device \a data provides the body HTTP headers \a headers. The I/O device \a data provides the body
of the response. If \a data is sequential, the body of the of the response. If \a data is sequential, the body of the
message is sent in chunks: otherwise, the function assumes all message is sent in chunks: otherwise, the function assumes all
the content is available and sends it all at once but the read the content is available and sends it all at once but the read
@ -175,7 +179,7 @@ QHttpServerResponder::~QHttpServerResponder()
\note This function takes the ownership of \a data. \note This function takes the ownership of \a data.
*/ */
void QHttpServerResponder::write(QIODevice *data, void QHttpServerResponder::write(QIODevice *data,
const QByteArray &mimeType, HeaderList headers,
StatusCode status) StatusCode status)
{ {
Q_D(QHttpServerResponder); Q_D(QHttpServerResponder);
@ -202,14 +206,14 @@ void QHttpServerResponder::write(QIODevice *data,
return; return;
} }
d->writeStatusLine(status); writeStatusLine(status);
if (!input->isSequential()) // Non-sequential QIODevice should know its data size if (!input->isSequential()) // Non-sequential QIODevice should know its data size
d->addHeader(contentLengthString, QByteArray::number(input->size())); writeHeader(contentLengthHeader, QByteArray::number(input->size()));
d->addHeader(contentTypeString, mimeType); for (auto &&header : headers)
writeHeader(header.first, header.second);
d->writeHeaders();
d->socket->write("\r\n"); d->socket->write("\r\n");
if (input->atEnd()) { if (input->atEnd()) {
@ -221,6 +225,73 @@ void QHttpServerResponder::write(QIODevice *data,
new IOChunkedTransfer<>(input.take(), d->socket); new IOChunkedTransfer<>(input.take(), d->socket);
} }
/*!
Answers a request with an HTTP status code \a status and a
MIME type \a mimeType. The I/O device \a data provides the body
of the response. If \a data is sequential, the body of the
message is sent in chunks: otherwise, the function assumes all
the content is available and sends it all at once but the read
is done in chunks.
\note This function takes the ownership of \a data.
*/
void QHttpServerResponder::write(QIODevice *data,
const QByteArray &mimeType,
StatusCode status)
{
write(data, {{ contentTypeHeader, mimeType }}, status);
}
/*!
Answers a request with an HTTP status code \a status, JSON
document \a document and HTTP headers \a headers.
Note: This function sets HTTP Content-Type header as "application/json".
*/
void QHttpServerResponder::write(const QJsonDocument &document,
HeaderList headers,
StatusCode status)
{
const QByteArray &json = document.toJson();
writeStatusLine(status);
writeHeader(contentTypeHeader, contentTypeJson);
writeHeader(contentLengthHeader, QByteArray::number(json.size()));
writeHeaders(std::move(headers));
writeBody(document.toJson());
}
/*!
Answers a request with an HTTP status code \a status, and JSON
document \a document.
Note: This function sets HTTP Content-Type header as "application/json".
*/
void QHttpServerResponder::write(const QJsonDocument &document,
StatusCode status)
{
write(document, {}, status);
}
/*!
Answers a request with an HTTP status code \a status,
HTTP Headers \a headers and a body \a data.
Note: This function sets HTTP Content-Length header.
*/
void QHttpServerResponder::write(const QByteArray &data,
HeaderList headers,
StatusCode status)
{
Q_D(QHttpServerResponder);
writeStatusLine(status);
for (auto &&header : headers)
writeHeader(header.first, header.second);
writeHeader(contentLengthHeader, QByteArray::number(data.size()));
writeBody(data);
}
/*! /*!
Answers a request with an HTTP status code \a status, a Answers a request with an HTTP status code \a status, a
MIME type \a mimeType and a body \a data. MIME type \a mimeType and a body \a data.
@ -229,29 +300,102 @@ void QHttpServerResponder::write(const QByteArray &data,
const QByteArray &mimeType, const QByteArray &mimeType,
StatusCode status) StatusCode status)
{ {
Q_D(QHttpServerResponder); write(data, {{ contentTypeHeader, mimeType }}, status);
d->writeStatusLine(status);
addHeaders(contentTypeString, mimeType,
contentLengthString, QByteArray::number(data.size()));
d->writeHeaders();
d->writeBody(data);
}
/*!
Answers a request with an HTTP status code \a status, and JSON
document \a document.
*/
void QHttpServerResponder::write(const QJsonDocument &document, StatusCode status)
{
write(document.toJson(), QByteArrayLiteral("text/json"), status);
} }
/*! /*!
Answers a request with an HTTP status code \a status. Answers a request with an HTTP status code \a status.
Note: This function sets HTTP Content-Type header as "application/x-empty".
*/ */
void QHttpServerResponder::write(StatusCode status) void QHttpServerResponder::write(StatusCode status)
{ {
write(QByteArray(), QByteArrayLiteral("application/x-empty"), status); write(QByteArray(), contentTypeEmpty, status);
}
/*!
Answers a request with an HTTP status code \a status and
HTTP Headers \a headers.
*/
void QHttpServerResponder::write(HeaderList headers, StatusCode status)
{
write(QByteArray(), std::move(headers), status);
}
/*!
This function writes HTTP status line with an HTTP status code \a status
and an HTTP version \a version.
*/
void QHttpServerResponder::writeStatusLine(StatusCode status,
const QPair<quint8, quint8> &version)
{
Q_D(const QHttpServerResponder);
Q_ASSERT(d->socket->isOpen());
d->socket->write("HTTP/");
d->socket->write(QByteArray::number(version.first));
d->socket->write(".");
d->socket->write(QByteArray::number(version.second));
d->socket->write(" ");
d->socket->write(QByteArray::number(quint32(status)));
d->socket->write(" ");
d->socket->write(statusString.at(status));
d->socket->write("\r\n");
}
/*!
This function writes an HTTP header \a header
with \a value.
*/
void QHttpServerResponder::writeHeader(const QByteArray &header,
const QByteArray &value)
{
Q_D(const QHttpServerResponder);
Q_ASSERT(d->socket->isOpen());
d->socket->write(header);
d->socket->write(": ");
d->socket->write(value);
d->socket->write("\r\n");
}
/*!
This function writes HTTP headers \a headers.
*/
void QHttpServerResponder::writeHeaders(HeaderList headers)
{
for (auto &&header : headers)
writeHeader(header.first, header.second);
}
/*!
This function writes HTTP body \a body with size \a size.
*/
void QHttpServerResponder::writeBody(const char *body, qint64 size)
{
Q_D(QHttpServerResponder);
Q_ASSERT(d->socket->isOpen());
if (!d->bodyStarted) {
d->socket->write("\r\n");
d->bodyStarted = true;
}
d->socket->write(body, size);
}
/*!
This function writes HTTP body \a body.
*/
void QHttpServerResponder::writeBody(const char *body)
{
writeBody(body, qstrlen(body));
}
/*!
This function writes HTTP body \a body.
*/
void QHttpServerResponder::writeBody(const QByteArray &body)
{
writeBody(body.constData(), body.size());
} }
/*! /*!
@ -263,47 +407,4 @@ QTcpSocket *QHttpServerResponder::socket() const
return d->socket; return d->socket;
} }
bool QHttpServerResponder::addHeader(const QByteArray &key, const QByteArray &value)
{
Q_D(QHttpServerResponder);
return d->addHeader(key, value);
}
void QHttpServerResponderPrivate::writeStatusLine(StatusCode status,
const QPair<quint8, quint8> &version) const
{
Q_ASSERT(socket->isOpen());
socket->write("HTTP/");
socket->write(QByteArray::number(version.first));
socket->write(".");
socket->write(QByteArray::number(version.second));
socket->write(" ");
socket->write(QByteArray::number(quint32(status)));
socket->write(" ");
socket->write(statusString.at(status));
socket->write("\r\n");
}
void QHttpServerResponderPrivate::writeHeader(const QByteArray &header,
const QByteArray &value) const
{
socket->write(header);
socket->write(": ");
socket->write(value);
socket->write("\r\n");
}
void QHttpServerResponderPrivate::writeHeaders() const
{
for (const auto &pair : qAsConst(headers()))
writeHeader(pair.first, pair.second);
}
void QHttpServerResponderPrivate::writeBody(const QByteArray &body) const
{
Q_ASSERT(socket->isOpen());
socket->write("\r\n");
socket->write(body);
}
QT_END_NAMESPACE QT_END_NAMESPACE

View File

@ -33,12 +33,16 @@
#include <QtHttpServer/qthttpserverglobal.h> #include <QtHttpServer/qthttpserverglobal.h>
#include <QtCore/qdebug.h> #include <QtCore/qdebug.h>
#include <QtCore/qpair.h>
#include <QtCore/qglobal.h> #include <QtCore/qglobal.h>
#include <QtCore/qstring.h> #include <QtCore/qstring.h>
#include <QtCore/qscopedpointer.h> #include <QtCore/qscopedpointer.h>
#include <QtCore/qmetatype.h> #include <QtCore/qmetatype.h>
#include <QtCore/qmimetype.h> #include <QtCore/qmimetype.h>
#include <utility>
#include <initializer_list>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
class QTcpSocket; class QTcpSocket;
@ -126,39 +130,53 @@ public:
NetworkConnectTimeoutError = 599, NetworkConnectTimeoutError = 599,
}; };
using HeaderList = std::initializer_list<std::pair<QByteArray, QByteArray>>;
QHttpServerResponder(QHttpServerResponder &&other); QHttpServerResponder(QHttpServerResponder &&other);
~QHttpServerResponder(); ~QHttpServerResponder();
void write(QIODevice *data, const QByteArray &mimeType, StatusCode status = StatusCode::Ok); void write(QIODevice *data,
HeaderList headers,
StatusCode status = StatusCode::Ok);
void write(QIODevice *data,
const QByteArray &mimeType,
StatusCode status = StatusCode::Ok);
void write(const QJsonDocument &document,
HeaderList headers,
StatusCode status = StatusCode::Ok);
void write(const QJsonDocument &document,
StatusCode status = StatusCode::Ok);
void write(const QByteArray &data,
HeaderList headers,
StatusCode status = StatusCode::Ok);
void write(const QByteArray &data, void write(const QByteArray &data,
const QByteArray &mimeType, const QByteArray &mimeType,
StatusCode status = StatusCode::Ok); StatusCode status = StatusCode::Ok);
void write(const QJsonDocument &document, StatusCode status = StatusCode::Ok);
void write(HeaderList headers, StatusCode status = StatusCode::Ok);
void write(StatusCode status = StatusCode::Ok); void write(StatusCode status = StatusCode::Ok);
void writeStatusLine(StatusCode status = StatusCode::Ok,
const QPair<quint8, quint8> &version = qMakePair(1u, 1u));
void writeHeader(const QByteArray &key, const QByteArray &value);
void writeHeaders(HeaderList headers);
void writeBody(const char *body, qint64 size);
void writeBody(const char *body);
void writeBody(const QByteArray &body);
QTcpSocket *socket() const; QTcpSocket *socket() const;
bool addHeader(const QByteArray &key, const QByteArray &value);
template <typename... Args>
inline void addHeaders(const QPair<QByteArray, QByteArray> &first, Args &&... others)
{
addHeader(first.first, first.second);
addHeaders(std::forward<Args>(others)...);
}
template <typename... Args>
inline void addHeaders(const QByteArray &key, const QByteArray &value, Args &&... others)
{
addHeader(key, value);
addHeaders(std::forward<Args>(others)...);
}
private: private:
QHttpServerResponder(const QHttpServerRequest &request, QTcpSocket *socket); QHttpServerResponder(const QHttpServerRequest &request, QTcpSocket *socket);
inline void addHeaders() {}
QScopedPointer<QHttpServerResponderPrivate> d_ptr; QScopedPointer<QHttpServerResponderPrivate> d_ptr;
}; };

View File

@ -55,33 +55,9 @@ QT_BEGIN_NAMESPACE
class QHttpServerResponderPrivate class QHttpServerResponderPrivate
{ {
using StatusCode = QHttpServerResponder::StatusCode;
public: public:
QHttpServerResponderPrivate(const QHttpServerRequest &request, QTcpSocket *const socket) : QHttpServerResponderPrivate(const QHttpServerRequest &request, QTcpSocket *const socket)
request(request), : request(request), socket(socket) {}
socket(socket)
{
const auto server = QStringLiteral("%1/%2(%3)").arg(
QCoreApplication::instance()->applicationName(),
QCoreApplication::instance()->applicationVersion(),
QSysInfo::prettyProductName());
addHeader(QByteArrayLiteral("Server"), server.toUtf8());
}
inline bool addHeader(const QByteArray &key, const QByteArray &value)
{
const auto hash = qHash(key.toLower());
if (m_headers.contains(hash))
return false;
m_headers.insert(hash, qMakePair(key, value));
return true;
}
void writeStatusLine(StatusCode status = StatusCode::Ok,
const QPair<quint8, quint8> &version = qMakePair(1u, 1u)) const;
void writeHeaders() const;
void writeBody(const QByteArray &body) const;
const QHttpServerRequest &request; const QHttpServerRequest &request;
#if defined(QT_DEBUG) #if defined(QT_DEBUG)
@ -89,14 +65,7 @@ public:
#else #else
QTcpSocket *const socket; QTcpSocket *const socket;
#endif #endif
bool bodyStarted{false};
QMap<uint, QPair<QByteArray, QByteArray>> m_headers;
private:
void writeHeader(const QByteArray &header, const QByteArray &value) const;
public:
const decltype(m_headers) &headers() const { return m_headers; }
}; };
QT_END_NAMESPACE QT_END_NAMESPACE

View File

@ -215,6 +215,24 @@ void tst_QHttpServer::initTestCase()
}; };
}); });
httpserver.route("/chunked/", [] (QHttpServerResponder &&responder) {
responder.writeStatusLine(QHttpServerResponder::StatusCode::Ok);
responder.writeHeaders({
{"Content-Type", "text/plain"},
{"Transfer-Encoding", "chunked"} });
auto writeChunk = [&responder] (const char *message) {
responder.writeBody(QByteArray::number(qstrlen(message), 16));
responder.writeBody("\r\n");
responder.writeBody(message);
responder.writeBody("\r\n");
};
writeChunk("part 1 of the message, ");
writeChunk("part 2 of the message");
writeChunk("");
});
urlBase = QStringLiteral("http://localhost:%1%2").arg(httpserver.listen()); urlBase = QStringLiteral("http://localhost:%1%2").arg(httpserver.listen());
} }
@ -391,6 +409,12 @@ void tst_QHttpServer::routeGet_data()
<< 200 << 200
<< "application/json" << "application/json"
<< "[1,\"2\",{\"name\":\"test\"}]"; << "[1,\"2\",{\"name\":\"test\"}]";
QTest::addRow("chunked")
<< "/chunked/"
<< 200
<< "text/plain"
<< "part 1 of the message, part 2 of the message";
} }
void tst_QHttpServer::routeGet() void tst_QHttpServer::routeGet()

View File

@ -41,6 +41,9 @@
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
static const QByteArray headerServerString(QByteArrayLiteral("Server"));
static const QByteArray headerServerValue(QByteArrayLiteral("Test server"));
class tst_QHttpServerResponder : public QObject class tst_QHttpServerResponder : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -56,9 +59,13 @@ private slots:
void defaultStatusCodeJson(); void defaultStatusCodeJson();
void writeStatusCode_data(); void writeStatusCode_data();
void writeStatusCode(); void writeStatusCode();
void writeStatusCodeExtraHeader();
void writeJson(); void writeJson();
void writeJsonExtraHeader();
void writeFile_data(); void writeFile_data();
void writeFile(); void writeFile();
void writeFileExtraHeader();
void writeByteArrayExtraHeader();
}; };
#define qWaitForFinished(REPLY) QVERIFY(QSignalSpy(REPLY, &QNetworkReply::finished).wait()) #define qWaitForFinished(REPLY) QVERIFY(QSignalSpy(REPLY, &QNetworkReply::finished).wait())
@ -131,10 +138,18 @@ void tst_QHttpServerResponder::writeStatusCode()
QCOMPARE(reply->error(), networkError); QCOMPARE(reply->error(), networkError);
QCOMPARE(reply->header(QNetworkRequest::ContentTypeHeader), QCOMPARE(reply->header(QNetworkRequest::ContentTypeHeader),
QByteArrayLiteral("application/x-empty")); QByteArrayLiteral("application/x-empty"));
QCOMPARE(reply->header(QNetworkRequest::ServerHeader), QStringLiteral("%1/%2(%3)") }
.arg(QCoreApplication::instance()->applicationName())
.arg(QCoreApplication::instance()->applicationVersion()) void tst_QHttpServerResponder::writeStatusCodeExtraHeader()
.arg(QSysInfo::prettyProductName()).toUtf8()); {
HttpServer server([=](QHttpServerResponder responder) {
responder.write({{ headerServerString, headerServerValue }});
});
auto reply = networkAccessManager->get(QNetworkRequest(server.url));
qWaitForFinished(reply);
QCOMPARE(reply->bytesAvailable(), 0);
QCOMPARE(reply->error(), QNetworkReply::NoError);
QCOMPARE(reply->header(QNetworkRequest::ServerHeader), headerServerValue);
} }
void tst_QHttpServerResponder::writeJson() void tst_QHttpServerResponder::writeJson()
@ -148,6 +163,20 @@ void tst_QHttpServerResponder::writeJson()
QCOMPARE(QJsonDocument::fromJson(reply->readAll()), json); QCOMPARE(QJsonDocument::fromJson(reply->readAll()), json);
} }
void tst_QHttpServerResponder::writeJsonExtraHeader()
{
const auto json = QJsonDocument::fromJson(QByteArrayLiteral(R"JSON({ "key" : "value" })JSON"));
HttpServer server([json](QHttpServerResponder responder) {
responder.write(json, {{ headerServerString, headerServerValue }});
});
auto reply = networkAccessManager->get(QNetworkRequest(server.url));
qWaitForFinished(reply);
QCOMPARE(reply->error(), QNetworkReply::NoError);
QCOMPARE(reply->header(QNetworkRequest::ContentTypeHeader), QByteArrayLiteral("text/json"));
QCOMPARE(reply->header(QNetworkRequest::ServerHeader), headerServerValue);
QCOMPARE(QJsonDocument::fromJson(reply->readAll()), json);
}
void tst_QHttpServerResponder::writeFile_data() void tst_QHttpServerResponder::writeFile_data()
{ {
QTest::addColumn<QIODevice *>("iodevice"); QTest::addColumn<QIODevice *>("iodevice");
@ -196,6 +225,51 @@ void tst_QHttpServerResponder::writeFile()
QCOMPARE(spyDestroyIoDevice.count(), 1); QCOMPARE(spyDestroyIoDevice.count(), 1);
} }
void tst_QHttpServerResponder::writeFileExtraHeader()
{
auto file = new QFile(QFINDTESTDATA("index.html"), this);
QSignalSpy spyDestroyIoDevice(file, &QObject::destroyed);
const QByteArray contentType("text/html");
HttpServer server([=](QHttpServerResponder responder) {
responder.write(
file,
{
{ "Content-Type", contentType },
{ headerServerString, headerServerValue }
});
});
auto reply = networkAccessManager->get(QNetworkRequest(server.url));
QTRY_VERIFY(reply->isFinished());
QCOMPARE(reply->header(QNetworkRequest::ContentTypeHeader), contentType);
QCOMPARE(reply->header(QNetworkRequest::ServerHeader), headerServerValue);
QCOMPARE(reply->readAll().trimmed(), "<html></html>");
QCOMPARE(spyDestroyIoDevice.count(), 1);
}
void tst_QHttpServerResponder::writeByteArrayExtraHeader()
{
const QByteArray data("test data");
const QByteArray contentType("text/plain");
HttpServer server([=](QHttpServerResponder responder) {
responder.write(
data,
{
{ "Content-Type", contentType },
{ headerServerString, headerServerValue }
});
});
auto reply = networkAccessManager->get(QNetworkRequest(server.url));
QTRY_VERIFY(reply->isFinished());
QCOMPARE(reply->header(QNetworkRequest::ContentTypeHeader), contentType);
QCOMPARE(reply->header(QNetworkRequest::ServerHeader), headerServerValue);
QCOMPARE(reply->readAll(), data);
}
QT_END_NAMESPACE QT_END_NAMESPACE
QTEST_MAIN(tst_QHttpServerResponder) QTEST_MAIN(tst_QHttpServerResponder)