diff options
| author | Stefan Boberg <[email protected]> | 2025-09-29 10:36:32 +0200 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2025-09-29 10:36:32 +0200 |
| commit | 2f0efec7ab0430f4f4858db87b7eecfbccc0f47c (patch) | |
| tree | 80ce35992a220260cf070fac739626f555de738a /src/zenhttp | |
| parent | fixed race condition in zen::logging::Get (#519) (diff) | |
| download | zen-2f0efec7ab0430f4f4858db87b7eecfbccc0f47c.tar.xz zen-2f0efec7ab0430f4f4858db87b7eecfbccc0f47c.zip | |
make cpr a HttpClient implementation detail (#517)
these changes remove cpr from anything which is not `HttpClient` internals.
The goal is to eventually replace cpr with a more direct curl interface to eliminate cpr since it's proven problematic due to their development practices which frequently breaks APIs and prevents us from updating vcpkg. But this PR is limited to refactoring existing cpr code to use `HttpClient` instead.
Diffstat (limited to 'src/zenhttp')
| -rw-r--r-- | src/zenhttp/auth/oidc.cpp | 40 | ||||
| -rw-r--r-- | src/zenhttp/httpclient.cpp | 70 | ||||
| -rw-r--r-- | src/zenhttp/httpclientauth.cpp | 18 | ||||
| -rw-r--r-- | src/zenhttp/include/zenhttp/cprutils.h | 86 | ||||
| -rw-r--r-- | src/zenhttp/include/zenhttp/formatters.h | 71 | ||||
| -rw-r--r-- | src/zenhttp/include/zenhttp/httpclient.h | 28 |
6 files changed, 210 insertions, 103 deletions
diff --git a/src/zenhttp/auth/oidc.cpp b/src/zenhttp/auth/oidc.cpp index 318110c7d..38e7586ad 100644 --- a/src/zenhttp/auth/oidc.cpp +++ b/src/zenhttp/auth/oidc.cpp @@ -1,9 +1,9 @@ // Copyright Epic Games, Inc. All Rights Reserved. #include "zenhttp/auth/oidc.h" +#include <zenhttp/httpclient.h> ZEN_THIRD_PARTY_INCLUDES_START -#include <cpr/cpr.h> #include <fmt/format.h> #include <json11.hpp> ZEN_THIRD_PARTY_INCLUDES_END @@ -41,27 +41,21 @@ OidcClient::OidcClient(const OidcClient::Options& Options) OidcClient::InitResult OidcClient::Initialize() { - ExtendableStringBuilder<256> Uri; - Uri << m_BaseUrl << "/.well-known/openid-configuration"sv; + HttpClient Http{m_BaseUrl}; + HttpClient::Response Response = Http.Get("/.well-known/openid-configuration"sv); - cpr::Session Session; - - Session.SetOption(cpr::Url{Uri.c_str()}); - - cpr::Response Response = Session.Get(); - - if (Response.error) + if (!Response) { - return {.Reason = std::move(Response.error.message)}; + return {.Reason = Response.ErrorMessage("")}; } - if (Response.status_code != 200) + if (Response.StatusCode != HttpResponseCode::OK) { - return {.Reason = std::move(Response.reason)}; + return {.Reason = std::string{ToString(Response.StatusCode)}}; } std::string JsonError; - json11::Json Json = json11::Json::parse(Response.text, JsonError); + json11::Json Json = json11::Json::parse(std::string{Response.AsText()}, JsonError); if (JsonError.empty() == false) { @@ -89,26 +83,24 @@ OidcClient::RefreshToken(std::string_view RefreshToken) { const std::string Body = fmt::format("grant_type=refresh_token&refresh_token={}&client_id={}", RefreshToken, m_ClientId); - cpr::Session Session; + HttpClient Http{m_Config.TokenEndpoint}; - Session.SetOption(cpr::Url{m_Config.TokenEndpoint.c_str()}); - Session.SetOption(cpr::Header{{"Content-Type", "application/x-www-form-urlencoded"}}); - Session.SetBody(cpr::Body{Body.data(), Body.size()}); + HttpClient::KeyValueMap Headers{{"Content-Type", "application/x-www-form-urlencoded"}}; - cpr::Response Response = Session.Post(); + HttpClient::Response Response = Http.Post("", IoBufferBuilder::MakeFromMemory(MemoryView{Body.data(), Body.size()}), Headers); - if (Response.error) + if (!Response) { - return {.Reason = std::move(Response.error.message)}; + return {.Reason = std::string{Response.ErrorMessage("")}}; } - if (Response.status_code != 200) + if (Response.StatusCode != HttpResponseCode::OK) { - return {.Reason = fmt::format("{} ({})", Response.reason, Response.text)}; + return {.Reason = fmt::format("{} ({})", ToString(Response.StatusCode), Response.AsText())}; } std::string JsonError; - json11::Json Json = json11::Json::parse(Response.text, JsonError); + json11::Json Json = json11::Json::parse(std::string{Response.AsText()}, JsonError); if (JsonError.empty() == false) { diff --git a/src/zenhttp/httpclient.cpp b/src/zenhttp/httpclient.cpp index 9ee8cc05a..5981d449a 100644 --- a/src/zenhttp/httpclient.cpp +++ b/src/zenhttp/httpclient.cpp @@ -1,5 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. +#include <zenhttp/cprutils.h> #include <zenhttp/formatters.h> #include <zenhttp/httpclient.h> #include <zenhttp/httpserver.h> @@ -27,7 +28,8 @@ #endif // ZEN_WITH_TESTS ZEN_THIRD_PARTY_INCLUDES_START -#include <cpr/cpr.h> +#include <cpr/body.h> +#include <cpr/session.h> ZEN_THIRD_PARTY_INCLUDES_END #if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC @@ -1660,6 +1662,72 @@ HttpClient::Response::ThrowError(std::string_view ErrorPrefix) ////////////////////////////////////////////////////////////////////////// +HttpClientError::ResponseClass +HttpClientError::GetResponseClass() const +{ + if ((cpr::ErrorCode)m_Error != cpr::ErrorCode::OK) + { + switch ((cpr::ErrorCode)m_Error) + { + case cpr::ErrorCode::CONNECTION_FAILURE: + return ResponseClass::kHttpCantConnectError; + case cpr::ErrorCode::HOST_RESOLUTION_FAILURE: + case cpr::ErrorCode::PROXY_RESOLUTION_FAILURE: + return ResponseClass::kHttpNoHost; + case cpr::ErrorCode::INTERNAL_ERROR: + case cpr::ErrorCode::NETWORK_RECEIVE_ERROR: + case cpr::ErrorCode::NETWORK_SEND_FAILURE: + case cpr::ErrorCode::OPERATION_TIMEDOUT: + return ResponseClass::kHttpTimeout; + case cpr::ErrorCode::SSL_CONNECT_ERROR: + case cpr::ErrorCode::SSL_LOCAL_CERTIFICATE_ERROR: + case cpr::ErrorCode::SSL_REMOTE_CERTIFICATE_ERROR: + case cpr::ErrorCode::SSL_CACERT_ERROR: + case cpr::ErrorCode::GENERIC_SSL_ERROR: + return ResponseClass::kHttpSLLError; + default: + return ResponseClass::kHttpOtherClientError; + } + } + else if (IsHttpSuccessCode(m_ResponseCode)) + { + return ResponseClass::kSuccess; + } + else + { + switch (m_ResponseCode) + { + case HttpResponseCode::Unauthorized: + return ResponseClass::kHttpUnauthorized; + case HttpResponseCode::NotFound: + return ResponseClass::kHttpNotFound; + case HttpResponseCode::Forbidden: + return ResponseClass::kHttpForbidden; + case HttpResponseCode::Conflict: + return ResponseClass::kHttpConflict; + case HttpResponseCode::InternalServerError: + return ResponseClass::kHttpInternalServerError; + case HttpResponseCode::ServiceUnavailable: + return ResponseClass::kHttpServiceUnavailable; + case HttpResponseCode::BadGateway: + return ResponseClass::kHttpBadGateway; + case HttpResponseCode::GatewayTimeout: + return ResponseClass::kHttpGatewayTimeout; + default: + if (m_ResponseCode >= HttpResponseCode::InternalServerError) + { + return ResponseClass::kHttpOtherServerError; + } + else + { + return ResponseClass::kHttpOtherClientError; + } + } + } +} + +////////////////////////////////////////////////////////////////////////// + #if ZEN_WITH_TESTS namespace testutil { diff --git a/src/zenhttp/httpclientauth.cpp b/src/zenhttp/httpclientauth.cpp index 4bbc6405b..4438fc137 100644 --- a/src/zenhttp/httpclientauth.cpp +++ b/src/zenhttp/httpclientauth.cpp @@ -9,11 +9,11 @@ #include <zencore/timer.h> #include <zencore/uid.h> #include <zenhttp/auth/authmgr.h> +#include <zenhttp/httpclient.h> #include <ctime> ZEN_THIRD_PARTY_INCLUDES_START -#include <cpr/cpr.h> #include <fmt/format.h> #include <json11.hpp> ZEN_THIRD_PARTY_INCLUDES_END @@ -47,18 +47,22 @@ namespace zen { namespace httpclientauth { OAuthParams.ClientId, OAuthParams.ClientSecret); - cpr::Response Response = cpr::Post(cpr::Url{OAuthParams.Url}, - cpr::Header{{"Content-Type", "application/x-www-form-urlencoded"}}, - cpr::Body{std::move(Body)}); + HttpClient Http{OAuthParams.Url}; - if (Response.error || Response.status_code != 200) + IoBuffer Payload{IoBuffer::Wrap, Body.data(), Body.size()}; + + // TODO: ensure this gets the right Content-Type passed along + + HttpClient::Response Response = Http.Post("", Payload, {{"Content-Type", "application/x-www-form-urlencoded"}}); + + if (!Response || Response.StatusCode != HttpResponseCode::OK) { - ZEN_WARN("Failed fetching OAuth access token {}. Reason: '{}'", OAuthParams.Url, Response.reason); + ZEN_WARN("Failed fetching OAuth access token {}. Reason: '{}'", OAuthParams.Url, Response.ErrorMessage("")); return HttpClientAccessToken{}; } std::string JsonError; - json11::Json Json = json11::Json::parse(Response.text, JsonError); + json11::Json Json = json11::Json::parse(std::string{Response.AsText()}, JsonError); if (JsonError.empty() == false) { diff --git a/src/zenhttp/include/zenhttp/cprutils.h b/src/zenhttp/include/zenhttp/cprutils.h new file mode 100644 index 000000000..a3b870c0f --- /dev/null +++ b/src/zenhttp/include/zenhttp/cprutils.h @@ -0,0 +1,86 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <zencore/compactbinary.h> +#include <zencore/compactbinaryvalidation.h> +#include <zencore/iobuffer.h> +#include <zencore/string.h> +#include <zenhttp/formatters.h> +#include <zenhttp/httpclient.h> +#include <zenhttp/httpcommon.h> + +ZEN_THIRD_PARTY_INCLUDES_START +#include <cpr/response.h> +#include <fmt/format.h> +ZEN_THIRD_PARTY_INCLUDES_END + +template<> +struct fmt::formatter<cpr::Response> +{ + constexpr auto parse(format_parse_context& Ctx) -> decltype(Ctx.begin()) { return Ctx.end(); } + + template<typename FormatContext> + auto format(const cpr::Response& Response, FormatContext& Ctx) const -> decltype(Ctx.out()) + { + using namespace std::literals; + + zen::NiceTimeSpanMs NiceResponseTime(uint64_t(Response.elapsed * 1000)); + + if (zen::IsHttpSuccessCode(Response.status_code)) + { + return fmt::format_to(Ctx.out(), + "Url: {}, Status: {}, Error: '{}' ({}), Bytes: {}/{} (Up/Down), Elapsed: {}", + Response.url.str(), + Response.status_code, + Response.error.message, + int(Response.error.code), + Response.uploaded_bytes, + Response.downloaded_bytes, + NiceResponseTime.c_str()); + } + else + { + const auto It = Response.header.find("Content-Type"); + const std::string_view ContentType = It != Response.header.end() ? It->second : "<None>"sv; + + if (ContentType == "application/x-ue-cb"sv) + { + zen::IoBuffer Body(zen::IoBuffer::Wrap, Response.text.data(), Response.text.size()); + zen::CbObjectView Obj(Body.Data()); + zen::ExtendableStringBuilder<256> Sb; + std::string_view Json = Obj.ToJson(Sb).ToView(); + + return fmt::format_to( + Ctx.out(), + "Url: {}, Status: {}, Error: '{}' ({}). Bytes: {}/{} (Up/Down), Elapsed: {}, Response: '{}', Reason: '{}'", + Response.url.str(), + Response.status_code, + Response.error.message, + int(Response.error.code), + Response.uploaded_bytes, + Response.downloaded_bytes, + NiceResponseTime.c_str(), + Json, + Response.reason); + } + else + { + zen::BodyLogFormatter Body(Response.text); + + return fmt::format_to( + Ctx.out(), + "Url: {}, Status: {}, Error: '{}' ({}), Bytes: {}/{} (Up/Down), Elapsed: {}, Response: '{}', Reason: '{}'", + Response.url.str(), + Response.status_code, + Response.error.message, + int(Response.error.code), + Response.uploaded_bytes, + Response.downloaded_bytes, + NiceResponseTime.c_str(), + Body.GetText(), + Response.reason); + } + } + } +}; diff --git a/src/zenhttp/include/zenhttp/formatters.h b/src/zenhttp/include/zenhttp/formatters.h index 05a23d675..0af31fa30 100644 --- a/src/zenhttp/include/zenhttp/formatters.h +++ b/src/zenhttp/include/zenhttp/formatters.h @@ -10,7 +10,6 @@ #include <zenhttp/httpcommon.h> ZEN_THIRD_PARTY_INCLUDES_START -#include <cpr/cpr.h> #include <fmt/format.h> ZEN_THIRD_PARTY_INCLUDES_END @@ -59,76 +58,6 @@ public: } // namespace zen template<> -struct fmt::formatter<cpr::Response> -{ - constexpr auto parse(format_parse_context& Ctx) -> decltype(Ctx.begin()) { return Ctx.end(); } - - template<typename FormatContext> - auto format(const cpr::Response& Response, FormatContext& Ctx) const -> decltype(Ctx.out()) - { - using namespace std::literals; - - zen::NiceTimeSpanMs NiceResponseTime(uint64_t(Response.elapsed * 1000)); - - if (zen::IsHttpSuccessCode(Response.status_code)) - { - return fmt::format_to(Ctx.out(), - "Url: {}, Status: {}, Error: '{}' ({}), Bytes: {}/{} (Up/Down), Elapsed: {}", - Response.url.str(), - Response.status_code, - Response.error.message, - int(Response.error.code), - Response.uploaded_bytes, - Response.downloaded_bytes, - NiceResponseTime.c_str()); - } - else - { - const auto It = Response.header.find("Content-Type"); - const std::string_view ContentType = It != Response.header.end() ? It->second : "<None>"sv; - - if (ContentType == "application/x-ue-cb"sv) - { - zen::IoBuffer Body(zen::IoBuffer::Wrap, Response.text.data(), Response.text.size()); - zen::CbObjectView Obj(Body.Data()); - zen::ExtendableStringBuilder<256> Sb; - std::string_view Json = Obj.ToJson(Sb).ToView(); - - return fmt::format_to( - Ctx.out(), - "Url: {}, Status: {}, Error: '{}' ({}). Bytes: {}/{} (Up/Down), Elapsed: {}, Response: '{}', Reason: '{}'", - Response.url.str(), - Response.status_code, - Response.error.message, - int(Response.error.code), - Response.uploaded_bytes, - Response.downloaded_bytes, - NiceResponseTime.c_str(), - Json, - Response.reason); - } - else - { - zen::BodyLogFormatter Body(Response.text); - - return fmt::format_to( - Ctx.out(), - "Url: {}, Status: {}, Error: '{}' ({}), Bytes: {}/{} (Up/Down), Elapsed: {}, Response: '{}', Reason: '{}'", - Response.url.str(), - Response.status_code, - Response.error.message, - int(Response.error.code), - Response.uploaded_bytes, - Response.downloaded_bytes, - NiceResponseTime.c_str(), - Body.GetText(), - Response.reason); - } - } - } -}; - -template<> struct fmt::formatter<zen::HttpClient::Response> { constexpr auto parse(format_parse_context& Ctx) -> decltype(Ctx.begin()) { return Ctx.end(); } diff --git a/src/zenhttp/include/zenhttp/httpclient.h b/src/zenhttp/include/zenhttp/httpclient.h index 50bd5b53a..ec06aa229 100644 --- a/src/zenhttp/include/zenhttp/httpclient.h +++ b/src/zenhttp/include/zenhttp/httpclient.h @@ -76,6 +76,34 @@ public: { } + inline int GetInternalErrorCode() const { return m_Error; } + inline HttpResponseCode GetHttpResponseCode() const { return m_ResponseCode; } + + enum class ResponseClass : std::int8_t + { + kSuccess = 0, + + kHttpOtherClientError = 80, + kHttpCantConnectError = 81, // CONNECTION_FAILURE + kHttpNotFound = 66, // NotFound(404) + kHttpUnauthorized = 77, // Unauthorized(401), + kHttpSLLError = + 82, // SSL_CONNECT_ERROR, SSL_LOCAL_CERTIFICATE_ERROR, SSL_REMOTE_CERTIFICATE_ERROR, SSL_CACERT_ERROR, GENERIC_SSL_ERROR + kHttpForbidden = 83, // Forbidden(403) + kHttpTimeout = 84, // NETWORK_RECEIVE_ERROR, NETWORK_SEND_FAILURE, OPERATION_TIMEDOUT, RequestTimeout(408) + kHttpConflict = 85, // Conflict(409) + kHttpNoHost = 86, // HOST_RESOLUTION_FAILURE, PROXY_RESOLUTION_FAILURE + + kHttpOtherServerError = 90, + kHttpInternalServerError = 91, // InternalServerError(500) + kHttpServiceUnavailable = 69, // ServiceUnavailable(503) + kHttpBadGateway = 92, // BadGateway(502) + kHttpGatewayTimeout = 93, // GatewayTimeout(504) + }; + + ResponseClass GetResponseClass() const; + +private: const int m_Error = 0; const HttpResponseCode m_ResponseCode = HttpResponseCode::ImATeapot; }; |