117 lines
7.5 KiB
C++
117 lines
7.5 KiB
C++
#pragma once
|
||
#include <QNetworkReply>
|
||
#include <QString>
|
||
|
||
// Хелперы для информативного логирования сетевых ошибок. Используются
|
||
// и в NetworkService, и в DeviceScreener — категория сразу показывает
|
||
// характер сбоя: timeout / refused / DNS / SSL / HTTP-уровень.
|
||
|
||
namespace NetworkErrorUtils {
|
||
|
||
// Удаляет схему+хост из URL, оставляет только путь (для логов/UI без секретов).
|
||
inline QString sanitizePath(const QString &fullUrl) {
|
||
const int schemeEnd = fullUrl.indexOf(QStringLiteral("://"));
|
||
if (schemeEnd < 0) return fullUrl;
|
||
const int pathStart = fullUrl.indexOf(QLatin1Char('/'), schemeEnd + 3);
|
||
return pathStart >= 0 ? fullUrl.mid(pathStart) : QStringLiteral("/");
|
||
}
|
||
|
||
// Категория ошибки в виде короткого ярлыка. См. QNetworkReply::NetworkError.
|
||
inline QString networkErrorCategory(QNetworkReply::NetworkError err) {
|
||
switch (err) {
|
||
case QNetworkReply::NoError: return QStringLiteral("NO_ERROR");
|
||
case QNetworkReply::ConnectionRefusedError: return QStringLiteral("CONNECTION_REFUSED");
|
||
case QNetworkReply::RemoteHostClosedError: return QStringLiteral("REMOTE_CLOSED");
|
||
case QNetworkReply::HostNotFoundError: return QStringLiteral("DNS_FAIL");
|
||
case QNetworkReply::TimeoutError: return QStringLiteral("QT_TIMEOUT");
|
||
case QNetworkReply::OperationCanceledError: return QStringLiteral("CANCELLED");
|
||
case QNetworkReply::SslHandshakeFailedError: return QStringLiteral("SSL_HANDSHAKE");
|
||
case QNetworkReply::TemporaryNetworkFailureError: return QStringLiteral("TEMP_NET_FAIL");
|
||
case QNetworkReply::NetworkSessionFailedError: return QStringLiteral("NO_NETWORK_SESSION");
|
||
case QNetworkReply::BackgroundRequestNotAllowedError: return QStringLiteral("BG_NOT_ALLOWED");
|
||
case QNetworkReply::TooManyRedirectsError: return QStringLiteral("TOO_MANY_REDIRECTS");
|
||
case QNetworkReply::InsecureRedirectError: return QStringLiteral("INSECURE_REDIRECT");
|
||
case QNetworkReply::UnknownNetworkError: return QStringLiteral("UNKNOWN_NETWORK");
|
||
case QNetworkReply::ProxyConnectionRefusedError: return QStringLiteral("PROXY_REFUSED");
|
||
case QNetworkReply::ProxyConnectionClosedError: return QStringLiteral("PROXY_CLOSED");
|
||
case QNetworkReply::ProxyNotFoundError: return QStringLiteral("PROXY_NOT_FOUND");
|
||
case QNetworkReply::ProxyTimeoutError: return QStringLiteral("PROXY_TIMEOUT");
|
||
case QNetworkReply::ProxyAuthenticationRequiredError: return QStringLiteral("PROXY_AUTH_REQ");
|
||
case QNetworkReply::ContentAccessDenied: return QStringLiteral("HTTP_403");
|
||
case QNetworkReply::ContentOperationNotPermittedError:return QStringLiteral("OP_NOT_PERMITTED");
|
||
case QNetworkReply::ContentNotFoundError: return QStringLiteral("HTTP_404");
|
||
case QNetworkReply::AuthenticationRequiredError: return QStringLiteral("HTTP_401");
|
||
case QNetworkReply::ContentReSendError: return QStringLiteral("RESEND_REQUIRED");
|
||
case QNetworkReply::ContentConflictError: return QStringLiteral("HTTP_409");
|
||
case QNetworkReply::ContentGoneError: return QStringLiteral("HTTP_410");
|
||
case QNetworkReply::InternalServerError: return QStringLiteral("HTTP_500");
|
||
case QNetworkReply::OperationNotImplementedError: return QStringLiteral("HTTP_501");
|
||
case QNetworkReply::ServiceUnavailableError: return QStringLiteral("HTTP_503");
|
||
case QNetworkReply::UnknownServerError: return QStringLiteral("UNKNOWN_SERVER");
|
||
case QNetworkReply::ProtocolUnknownError: return QStringLiteral("PROTO_UNKNOWN");
|
||
case QNetworkReply::ProtocolInvalidOperationError: return QStringLiteral("PROTO_INVALID_OP");
|
||
case QNetworkReply::UnknownContentError: return QStringLiteral("UNKNOWN_CONTENT");
|
||
case QNetworkReply::ProtocolFailure: return QStringLiteral("PROTO_FAILURE");
|
||
default: return QStringLiteral("err_%1").arg(static_cast<int>(err));
|
||
}
|
||
}
|
||
|
||
// Транзиентная ошибка CONNECT-фазы: сбой произошёл ДО того, как байты запроса
|
||
// ушли на сервер (соединение не установилось), поэтому повтор безопасен даже для
|
||
// неидемпотентных POST — дубля быть не может. Сюда же ETIMEDOUT-connect, который
|
||
// Qt отдаёт как UnknownNetworkError "Connection timed out".
|
||
inline bool isConnectPhaseTransient(QNetworkReply::NetworkError err) {
|
||
switch (err) {
|
||
case QNetworkReply::ConnectionRefusedError:
|
||
case QNetworkReply::HostNotFoundError:
|
||
case QNetworkReply::ProxyConnectionRefusedError:
|
||
case QNetworkReply::ProxyNotFoundError:
|
||
case QNetworkReply::ProxyTimeoutError:
|
||
case QNetworkReply::UnknownNetworkError:
|
||
case QNetworkReply::TemporaryNetworkFailureError:
|
||
case QNetworkReply::NetworkSessionFailedError:
|
||
return true;
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Более широкий набор транзиентных ошибок — безопасен ТОЛЬКО для идемпотентных
|
||
// запросов (GET/PATCH/DELETE, аплоады), т.к. сбой мог случиться уже ПОСЛЕ отправки
|
||
// тела (сервер мог его обработать) → повтор неидемпотентного POST дал бы дубль.
|
||
inline bool isIdempotentTransient(QNetworkReply::NetworkError err) {
|
||
if (isConnectPhaseTransient(err)) return true;
|
||
switch (err) {
|
||
case QNetworkReply::RemoteHostClosedError:
|
||
case QNetworkReply::TimeoutError:
|
||
case QNetworkReply::ProxyConnectionClosedError:
|
||
case QNetworkReply::ServiceUnavailableError:
|
||
return true;
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Развёрнутое описание сетевой ошибки: HTTP-код + категория или просто
|
||
// категория + Qt errorString. Эндпоинт — без хоста.
|
||
inline QString describeNetError(QNetworkReply *reply, const QString &path) {
|
||
const int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||
const QString endpoint = sanitizePath(path);
|
||
const QString category = networkErrorCategory(reply->error());
|
||
if (httpCode > 0) {
|
||
return QStringLiteral("HTTP %1 [%2] %3").arg(httpCode).arg(category, endpoint);
|
||
}
|
||
return QStringLiteral("[%1] %2 (qt: %3)").arg(category, endpoint, reply->errorString());
|
||
}
|
||
|
||
// Случай нашего QTimer-таймаута (reply ещё не завершился). reply->error()
|
||
// обычно ещё NoError — помечаем категорию явно как CLIENT_TIMEOUT_30s.
|
||
inline QString describeClientTimeout(QNetworkReply *reply, const QString &path) {
|
||
const QString endpoint = sanitizePath(path);
|
||
const QString qtCategory = networkErrorCategory(reply->error());
|
||
return QStringLiteral("[CLIENT_TIMEOUT_30s] %1 (qt-state=%2 errorString=\"%3\")")
|
||
.arg(endpoint, qtCategory, reply->errorString());
|
||
}
|
||
|
||
} // namespace NetworkErrorUtils
|