summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/network/access/qnetworkaccessmanager.cpp2
-rw-r--r--src/network/access/qnetworkreplyhttpimpl.cpp51
-rw-r--r--src/network/access/qnetworkreplyhttpimpl_p.h1
-rw-r--r--tests/auto/network/access/qnetworkreply_local/tst_qnetworkreply_local.cpp95
4 files changed, 148 insertions, 1 deletions
diff --git a/src/network/access/qnetworkaccessmanager.cpp b/src/network/access/qnetworkaccessmanager.cpp
index 2d93e217e88..6428653de26 100644
--- a/src/network/access/qnetworkaccessmanager.cpp
+++ b/src/network/access/qnetworkaccessmanager.cpp
@@ -1259,7 +1259,7 @@ QNetworkReply *QNetworkAccessManager::createRequest(QNetworkAccessManager::Opera
auto h = request.headers();
#ifndef Q_OS_WASM // Content-length header is not allowed to be set by user in wasm
if (!h.contains(QHttpHeaders::WellKnownHeader::ContentLength) &&
- outgoingData && !outgoingData->isSequential()) {
+ outgoingData && !outgoingData->isSequential() && outgoingData->size()) {
// request has no Content-Length
// but the data that is outgoing is random-access
h.append(QHttpHeaders::WellKnownHeader::ContentLength,
diff --git a/src/network/access/qnetworkreplyhttpimpl.cpp b/src/network/access/qnetworkreplyhttpimpl.cpp
index bed1eed63d8..61cc2f7038b 100644
--- a/src/network/access/qnetworkreplyhttpimpl.cpp
+++ b/src/network/access/qnetworkreplyhttpimpl.cpp
@@ -625,6 +625,53 @@ QHttpNetworkRequest::Priority QNetworkReplyHttpImplPrivate::convert(QNetworkRequ
Q_UNREACHABLE_RETURN(QHttpNetworkRequest::NormalPriority);
}
+void QNetworkReplyHttpImplPrivate::maybeDropUploadDevice(const QNetworkRequest &newHttpRequest)
+{
+ // Check for 0-length upload device. Following RFC9110, we are discouraged
+ // from sending "content-length: 0" for methods where a content-length would
+ // not normally be expected. E.g. get, connect, head, delete
+ // https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6-5
+ auto contentLength0Allowed = [&]{
+ switch (operation) {
+ case QNetworkAccessManager::CustomOperation: {
+ const QByteArray customVerb = newHttpRequest.attribute(QNetworkRequest::CustomVerbAttribute)
+ .toByteArray();
+ if (customVerb.compare("get", Qt::CaseInsensitive) != 0
+ && customVerb.compare("head", Qt::CaseInsensitive) != 0
+ && customVerb.compare("connect", Qt::CaseInsensitive) != 0
+ && customVerb.compare("delete", Qt::CaseInsensitive) != 0) {
+ return true; // Trust user => content-length 0 is presumably okay!
+ }
+ // else:
+ [[fallthrough]];
+ }
+ case QNetworkAccessManager::HeadOperation:
+ case QNetworkAccessManager::GetOperation:
+ case QNetworkAccessManager::DeleteOperation:
+ // no content-length 0
+ return false;
+ case QNetworkAccessManager::PutOperation:
+ case QNetworkAccessManager::PostOperation:
+ case QNetworkAccessManager::UnknownOperation:
+ // yes content-length 0
+ return true;
+ }
+ Q_UNREACHABLE_RETURN(false);
+ };
+
+ const auto hasEmptyOutgoingPayload = [&]() {
+ if (!outgoingData)
+ return false;
+ if (outgoingDataBuffer)
+ return outgoingDataBuffer->isEmpty();
+ return outgoingData->size() == 0;
+ };
+ if (Q_UNLIKELY(hasEmptyOutgoingPayload()) && !contentLength0Allowed()) {
+ outgoingData = nullptr;
+ outgoingDataBuffer.reset();
+ }
+}
+
void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpRequest)
{
Q_Q(QNetworkReplyHttpImpl);
@@ -697,6 +744,10 @@ void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpReq
httpRequest.setRedirectPolicy(redirectPolicy);
+ // If, for some reason, it turns out we won't use the upload device we drop
+ // it in the following call:
+ maybeDropUploadDevice(newHttpRequest);
+
httpRequest.setPriority(convert(newHttpRequest.priority()));
loadingFromCache = false;
diff --git a/src/network/access/qnetworkreplyhttpimpl_p.h b/src/network/access/qnetworkreplyhttpimpl_p.h
index 2c9faffc321..0d16d02ff53 100644
--- a/src/network/access/qnetworkreplyhttpimpl_p.h
+++ b/src/network/access/qnetworkreplyhttpimpl_p.h
@@ -164,6 +164,7 @@ public:
QString reasonPhrase;
// upload
+ void maybeDropUploadDevice(const QNetworkRequest &newHttpRequest);
QNonContiguousByteDevice* createUploadByteDevice();
std::shared_ptr<QNonContiguousByteDevice> uploadByteDevice;
qint64 uploadByteDevicePosition;
diff --git a/tests/auto/network/access/qnetworkreply_local/tst_qnetworkreply_local.cpp b/tests/auto/network/access/qnetworkreply_local/tst_qnetworkreply_local.cpp
index 0c8a816d426..8bed904c230 100644
--- a/tests/auto/network/access/qnetworkreply_local/tst_qnetworkreply_local.cpp
+++ b/tests/auto/network/access/qnetworkreply_local/tst_qnetworkreply_local.cpp
@@ -10,6 +10,8 @@
#include <QtNetwork/qnetworkreply.h>
#include <QtNetwork/qnetworkaccessmanager.h>
+#include <QtCore/qbuffer.h>
+
#include "minihttpserver.h"
using namespace Qt::StringLiterals;
@@ -26,6 +28,8 @@ private slots:
void get();
void post();
+ void emptyDeviceUpload_data();
+ void emptyDeviceUpload();
#if QT_CONFIG(localserver)
void fullServerName_data();
@@ -121,6 +125,97 @@ void tst_QNetworkReply_local::post()
QCOMPARE(firstRequest.receivedData.last(payload.size() + 4), "\r\n\r\n" + payload);
}
+enum Method {
+ Get,
+ Put,
+ Post,
+ Custom,
+};
+void tst_QNetworkReply_local::emptyDeviceUpload_data()
+{
+ QTest::addColumn<Method>("method");
+ QTest::addColumn<QString>("customVerb");
+ QTest::addColumn<bool>("contentLengthExpected");
+ QTest::addColumn<bool>("sequential");
+ for (auto sequential : { false, true }) {
+ const char *suffix = sequential ? "sequential" : "non-sequential";
+ QTest::addRow("get-%s", suffix) << Get << "" << false << sequential;
+ QTest::addRow("put-%s", suffix) << Put << "" << true << sequential;
+ QTest::addRow("post-%s", suffix) << Post << "" << true << sequential;
+ QTest::addRow("custom-get-%s", suffix) << Custom << "get" << false << sequential;
+ QTest::addRow("custom-post-%s", suffix) << Custom << "post" << true << sequential;
+ QTest::addRow("custom-connect-%s", suffix) << Custom << "cOnNeCt" << false << sequential;
+ }
+}
+
+class EmptySequentialDevice : public QIODevice {
+public:
+ EmptySequentialDevice() = default;
+ bool isSequential() const override { return true; }
+
+protected:
+ qint64 readData(char *buf, qint64 len) override
+ {
+ Q_UNUSED(buf);
+ Q_UNUSED(len);
+ return -1;
+ }
+ qint64 writeData(const char *buf, qint64 len) override
+ {
+ Q_UNUSED(buf);
+ Q_UNUSED(len);
+ return -1;
+ }
+};
+
+void tst_QNetworkReply_local::emptyDeviceUpload()
+{
+ QFETCH(const Method, method);
+ QFETCH(const QString, customVerb);
+ QFETCH(const bool, contentLengthExpected);
+ QFETCH(const bool, sequential);
+ std::unique_ptr<MiniHttpServerV2> server = getServerForCurrentScheme();
+ const QUrl url = getUrlForCurrentScheme(server.get());
+
+ QNetworkAccessManager manager;
+
+ QBuffer emptyDevice;
+ emptyDevice.open(QIODevice::ReadOnly);
+
+ EmptySequentialDevice emptySequentialDevice;
+ emptySequentialDevice.open(QIODevice::ReadOnly);
+
+ QIODevice *device = sequential ? static_cast<QIODevice *>(&emptySequentialDevice)
+ : static_cast<QIODevice *>(&emptyDevice);
+ auto reply = [&]() -> std::unique_ptr<QNetworkReply> {
+ using unique_ptr = std::unique_ptr<QNetworkReply>;
+ switch (method) {
+ case Get:
+ return unique_ptr{ manager.get(QNetworkRequest(url), device) };
+ case Put:
+ return unique_ptr{ manager.put(QNetworkRequest(url), device) };
+ case Post:
+ return unique_ptr{ manager.post(QNetworkRequest(url), device) };
+ case Custom:
+ return unique_ptr{ manager.sendCustomRequest(QNetworkRequest(url), customVerb.toUtf8(),
+ device) };
+ }
+ Q_UNREACHABLE_RETURN({});
+ }();
+
+ QTRY_VERIFY(reply->isFinished());
+
+ auto printErrorOnFail = qScopeGuard([reply = reply.get()]() {
+ qWarning() << "Error in the reply:" << reply->errorString();
+ });
+ QCOMPARE(reply->readAll(), QByteArray("Hello World!"));
+ QCOMPARE(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), 200);
+ const QList<MiniHttpServerV2::State> peerStates = server->peerStates();
+ QCOMPARE(peerStates.size(), 1);
+ QCOMPARE(peerStates[0].foundContentLength, contentLengthExpected);
+ printErrorOnFail.dismiss();
+}
+
#if QT_CONFIG(localserver)
void tst_QNetworkReply_local::fullServerName_data()
{