4
0
mirror of https://github.com/QuasarApp/qthttpserver.git synced 2025-05-12 01:19:44 +00:00

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

@ -55,8 +55,12 @@ static const std::map<QHttpServerResponder::StatusCode, QByteArray> statusString
#undef XX
};
static const QByteArray contentTypeString(QByteArrayLiteral("Content-Type"));
static const QByteArray contentLengthString(QByteArrayLiteral("Content-Length"));
static const QByteArray contentTypeHeader(QByteArrayLiteral("Content-Type"));
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>
struct IOChunkedTransfer
@ -165,8 +169,8 @@ QHttpServerResponder::~QHttpServerResponder()
{}
/*!
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
Answers a request with an HTTP status code \a status and
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
message is sent in chunks: otherwise, the function assumes all
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.
*/
void QHttpServerResponder::write(QIODevice *data,
const QByteArray &mimeType,
HeaderList headers,
StatusCode status)
{
Q_D(QHttpServerResponder);
@ -202,14 +206,14 @@ void QHttpServerResponder::write(QIODevice *data,
return;
}
d->writeStatusLine(status);
writeStatusLine(status);
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");
if (input->atEnd()) {
@ -221,6 +225,73 @@ void QHttpServerResponder::write(QIODevice *data,
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
MIME type \a mimeType and a body \a data.
@ -229,29 +300,102 @@ void QHttpServerResponder::write(const QByteArray &data,
const QByteArray &mimeType,
StatusCode status)
{
Q_D(QHttpServerResponder);
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);
write(data, {{ contentTypeHeader, mimeType }}, 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)
{
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;
}
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

@ -33,12 +33,16 @@
#include <QtHttpServer/qthttpserverglobal.h>
#include <QtCore/qdebug.h>
#include <QtCore/qpair.h>
#include <QtCore/qglobal.h>
#include <QtCore/qstring.h>
#include <QtCore/qscopedpointer.h>
#include <QtCore/qmetatype.h>
#include <QtCore/qmimetype.h>
#include <utility>
#include <initializer_list>
QT_BEGIN_NAMESPACE
class QTcpSocket;
@ -126,39 +130,53 @@ public:
NetworkConnectTimeoutError = 599,
};
using HeaderList = std::initializer_list<std::pair<QByteArray, QByteArray>>;
QHttpServerResponder(QHttpServerResponder &&other);
~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,
const QByteArray &mimeType,
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 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;
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:
QHttpServerResponder(const QHttpServerRequest &request, QTcpSocket *socket);
inline void addHeaders() {}
QScopedPointer<QHttpServerResponderPrivate> d_ptr;
};

@ -55,33 +55,9 @@ QT_BEGIN_NAMESPACE
class QHttpServerResponderPrivate
{
using StatusCode = QHttpServerResponder::StatusCode;
public:
QHttpServerResponderPrivate(const QHttpServerRequest &request, QTcpSocket *const socket) :
request(request),
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;
QHttpServerResponderPrivate(const QHttpServerRequest &request, QTcpSocket *const socket)
: request(request), socket(socket) {}
const QHttpServerRequest &request;
#if defined(QT_DEBUG)
@ -89,14 +65,7 @@ public:
#else
QTcpSocket *const socket;
#endif
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; }
bool bodyStarted{false};
};
QT_END_NAMESPACE

@ -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());
}
@ -391,6 +409,12 @@ void tst_QHttpServer::routeGet_data()
<< 200
<< "application/json"
<< "[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()

@ -41,6 +41,9 @@
QT_BEGIN_NAMESPACE
static const QByteArray headerServerString(QByteArrayLiteral("Server"));
static const QByteArray headerServerValue(QByteArrayLiteral("Test server"));
class tst_QHttpServerResponder : public QObject
{
Q_OBJECT
@ -56,9 +59,13 @@ private slots:
void defaultStatusCodeJson();
void writeStatusCode_data();
void writeStatusCode();
void writeStatusCodeExtraHeader();
void writeJson();
void writeJsonExtraHeader();
void writeFile_data();
void writeFile();
void writeFileExtraHeader();
void writeByteArrayExtraHeader();
};
#define qWaitForFinished(REPLY) QVERIFY(QSignalSpy(REPLY, &QNetworkReply::finished).wait())
@ -131,10 +138,18 @@ void tst_QHttpServerResponder::writeStatusCode()
QCOMPARE(reply->error(), networkError);
QCOMPARE(reply->header(QNetworkRequest::ContentTypeHeader),
QByteArrayLiteral("application/x-empty"));
QCOMPARE(reply->header(QNetworkRequest::ServerHeader), QStringLiteral("%1/%2(%3)")
.arg(QCoreApplication::instance()->applicationName())
.arg(QCoreApplication::instance()->applicationVersion())
.arg(QSysInfo::prettyProductName()).toUtf8());
}
void tst_QHttpServerResponder::writeStatusCodeExtraHeader()
{
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()
@ -148,6 +163,20 @@ void tst_QHttpServerResponder::writeJson()
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()
{
QTest::addColumn<QIODevice *>("iodevice");
@ -196,6 +225,51 @@ void tst_QHttpServerResponder::writeFile()
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
QTEST_MAIN(tst_QHttpServerResponder)