WebAPI: Respond with more detailed info

* WebAPI: return error message when endpoint not found
* WebAPI: send appropriate status code when logging in
* WebAPI: return more info when adding torrents

PR #23202.
Closes #375.
Closes #10688.
Closes #10747.
Closes #11464.
This commit is contained in:
Thomas (Tom) Piccirello
2025-09-12 07:53:48 -07:00
committed by GitHub
parent 69b2d7a53e
commit 7ddbf58a3b
8 changed files with 76 additions and 17 deletions

View File

@@ -1,5 +1,13 @@
# WebAPI Changelog
## 2.14.0
* [#23202](https://github.com/qbittorrent/qBittorrent/pull/23202)
* WebAPI responds with the error message "Endpoint does not exist" when the endpoint does not exist, to better differentiate from unrelated Not Found (i.e. 404) responses
* `auth/login` endpoint responds to invalid credentials with a 401
* `torrents/add` endpoint responds with `success_count`, `pending_count`, `failure_count`, and `added_torrent_ids`
* When `pending_count` is non-zero, response code 202 is used
* When all torrents fail to be added, response code 409 is used
## 2.13.1
* [#23163](https://github.com/qbittorrent/qBittorrent/pull/23163)
* `torrents/add` endpoint now supports downloading from a search plugin via the `downloader` parameter

View File

@@ -59,7 +59,7 @@ APIResult APIController::run(const QString &action, const StringMap &params, con
const QByteArray methodName = action.toLatin1() + "Action";
if (!QMetaObject::invokeMethod(this, methodName.constData()))
throw APIError(APIErrorType::NotFound);
throw APIError(APIErrorType::NotFound, tr("Endpoint does not exist"));
return m_result;
}

View File

@@ -32,11 +32,12 @@
enum class APIErrorType
{
AccessDenied,
BadParams,
BadData,
Conflict,
NotFound,
AccessDenied,
Conflict
Unauthorized
};
class APIError : public RuntimeError

View File

@@ -59,7 +59,7 @@ void AuthController::loginAction()
{
if (m_sessionManager->session())
{
setResult(u"Ok."_s);
setStatus(APIStatus::Ok);
return;
}
@@ -84,17 +84,17 @@ void AuthController::loginAction()
m_clientFailedLogins.remove(clientAddr);
m_sessionManager->sessionStart();
setResult(u"Ok."_s);
setStatus(APIStatus::Ok);
LogMsg(tr("WebAPI login success. IP: %1").arg(clientAddr));
}
else
{
if (Preferences::instance()->getWebUIMaxAuthFailCount() > 0)
increaseFailedAttempts();
setResult(u"Fails."_s);
LogMsg(tr("WebAPI login failure. Reason: invalid credentials, attempt count: %1, IP: %2, username: %3")
.arg(QString::number(failedAttemptsCount()), clientAddr, usernameFromWeb)
, Log::WARNING);
throw APIError(APIErrorType::Unauthorized);
}
}

View File

@@ -1098,7 +1098,10 @@ void TorrentsController::addAction()
};
bool partialSuccess = false;
int pending = 0;
int failure = 0;
QList<BitTorrent::TorrentID> addedTorrentIDs;
addedTorrentIDs.reserve(urls.size() + torrents.size());
for (QString url : urls)
{
url = url.trimmed();
@@ -1123,7 +1126,14 @@ void TorrentsController::addAction()
addTorrentParams.filePriorities = filePriorities;
}
partialSuccess |= BitTorrent::Session::instance()->addTorrent(torrentDescr, addTorrentParams);
if (BitTorrent::Session::instance()->addTorrent(torrentDescr, addTorrentParams))
{
addedTorrentIDs.append(torrentID);
}
else
{
++failure;
}
}
else if (!downloaderParam.isEmpty())
{
@@ -1137,14 +1147,29 @@ void TorrentsController::addAction()
app()->addTorrentManager()->addTorrent(torrentFilePath, addTorrentParams);
});
connect(downloadHandler, &SearchDownloadHandler::downloadFinished, downloadHandler, &SearchDownloadHandler::deleteLater);
partialSuccess = true;
++pending;
}
else
{
if (!filePriorities.isEmpty())
throw APIError(APIErrorType::BadParams, tr("`filePriorities` may only be specified when metadata has already been fetched"));
partialSuccess |= app()->addTorrentManager()->addTorrent(url, addTorrentParams);
if (app()->addTorrentManager()->addTorrent(url, addTorrentParams))
{
if (infoHash.isValid())
{
addedTorrentIDs.append(torrentID);
}
else
{
++pending;
}
}
else
{
++failure;
}
}
m_torrentSourceCache.remove(url);
m_torrentMetadataCache.remove(torrentID);
@@ -1155,7 +1180,15 @@ void TorrentsController::addAction()
{
if (const auto loadResult = BitTorrent::TorrentDescriptor::load(it.value()))
{
partialSuccess |= BitTorrent::Session::instance()->addTorrent(loadResult.value(), addTorrentParams);
const BitTorrent::TorrentDescriptor &torrentDescr = loadResult.value();
if (BitTorrent::Session::instance()->addTorrent(torrentDescr, addTorrentParams))
{
addedTorrentIDs.append(torrentDescr.infoHash().toTorrentID());
}
else
{
++failure;
}
}
else
{
@@ -1163,10 +1196,25 @@ void TorrentsController::addAction()
}
}
if (partialSuccess)
setResult(u"Ok."_s);
if (!addedTorrentIDs.isEmpty() || (pending > 0))
{
QJsonArray ids;
for (const BitTorrent::TorrentID &torrentID : addedTorrentIDs)
ids.append(torrentID.toString());
setResult(QJsonObject {
{u"success_count"_s, addedTorrentIDs.size()},
{u"failure_count"_s, failure},
{u"pending_count"_s, pending},
{u"added_torrent_ids"_s, ids},
});
if (pending > 0)
setStatus(APIStatus::Async);
}
else
setResult(u"Fails."_s);
{
throw APIError(APIErrorType::Conflict);
}
}
void TorrentsController::addTrackersAction()

View File

@@ -418,6 +418,8 @@ void WebApplication::doProcessRequest()
throw ConflictHTTPError(error.message());
case APIErrorType::NotFound:
throw NotFoundHTTPError(error.message());
case APIErrorType::Unauthorized:
throw UnauthorizedHTTPError(error.message());
default:
Q_UNREACHABLE();
break;

View File

@@ -53,7 +53,7 @@
#include "base/utils/version.h"
#include "api/isessionmanager.h"
inline const Utils::Version<3, 2> API_VERSION {2, 13, 1};
inline const Utils::Version<3, 2> API_VERSION {2, 14, 0};
class APIController;
class AuthController;

View File

@@ -45,12 +45,12 @@ const submitLoginForm = (event) => {
})
})
.then(async (response) => {
const responseText = await response.text();
if (response.ok && (responseText === "Ok.")) {
if (response.ok) {
location.replace(location); // redirect
location.reload(true);
}
else {
const responseText = await response.text();
errorMsgElement.textContent = `QBT_TR(Invalid Username or Password.)QBT_TR[CONTEXT=Login]\nQBT_TR(Server response:)QBT_TR[CONTEXT=Login] ${responseText}`;
}
},