// Copyright Epic Games, Inc. All Rights Reserved. #include #include #include namespace zen { std::string_view S3ExtractXmlValue(std::string_view Xml, std::string_view Tag) { std::string OpenTag = fmt::format("<{}>", Tag); std::string CloseTag = fmt::format("", Tag); size_t Start = Xml.find(OpenTag); if (Start == std::string_view::npos) { return {}; } Start += OpenTag.size(); size_t End = Xml.find(CloseTag, Start); if (End == std::string_view::npos) { return {}; } return Xml.substr(Start, End - Start); } void S3DecodeXmlEntities(std::string_view Input, StringBuilderBase& Out) { if (Input.find('&') == std::string_view::npos) { Out.Append(Input); return; } for (size_t i = 0; i < Input.size(); ++i) { if (Input[i] == '&') { std::string_view Remaining = Input.substr(i); if (Remaining.starts_with("&")) { Out.Append('&'); i += 4; } else if (Remaining.starts_with("<")) { Out.Append('<'); i += 3; } else if (Remaining.starts_with(">")) { Out.Append('>'); i += 3; } else if (Remaining.starts_with(""")) { Out.Append('"'); i += 5; } else if (Remaining.starts_with("'")) { Out.Append('\''); i += 5; } else { Out.Append(Input[i]); } } else { Out.Append(Input[i]); } } } std::string S3DecodeXmlEntities(std::string_view Input) { if (Input.find('&') == std::string_view::npos) { return std::string(Input); } ExtendableStringBuilder<256> Sb; S3DecodeXmlEntities(Input, Sb); return Sb.ToString(); } std::string S3BuildRequestPath(std::string_view Path, std::string_view CanonicalQS) { if (CanonicalQS.empty()) { return std::string(Path); } return fmt::format("{}?{}", Path, CanonicalQS); } const std::string* S3FindResponseHeader(const HttpClient::KeyValueMap& Headers, std::string_view Name) { for (const auto& [K, V] : *Headers) { if (StrCaseCompare(K, Name) == 0) { return &V; } } return nullptr; } bool S3ExtractError(std::string_view Body, std::string_view& OutCode, std::string_view& OutMessage) { if (Body.find("") == std::string_view::npos) { return false; } OutCode = S3ExtractXmlValue(Body, "Code"); OutMessage = S3ExtractXmlValue(Body, "Message"); // Treat malformed bodies (Error tag present but no parseable Code/Message) // as a parse miss; callers format ": - " and an // empty render is indistinguishable from "no error". S3IsThrottled with // empty ErrorCode + S3ErrorMessage's Response.ErrorMessage fallback path // covers status-only triage. return !OutCode.empty() || !OutMessage.empty(); } bool S3IsThrottled(const HttpClient::Response& Response, std::string_view ErrorCode) { const int Status = static_cast(Response.StatusCode); if (Status == 503 || Status == 429) { return true; } if (ErrorCode == "SlowDown" || ErrorCode == "ServiceUnavailable" || ErrorCode == "ThrottlingException" || ErrorCode == "RequestLimitExceeded" || ErrorCode == "TooManyRequests") { return true; } return false; } std::string S3ErrorMessage(std::string_view Prefix, const HttpClient::Response& Response) { if (!Response.Error.has_value() && Response.ResponsePayload) { std::string_view Body(reinterpret_cast(Response.ResponsePayload.GetData()), Response.ResponsePayload.GetSize()); std::string_view Code; std::string_view Message; if (S3ExtractError(Body, Code, Message)) { ExtendableStringBuilder<256> Decoded; S3DecodeXmlEntities(Message, Decoded); if (S3IsThrottled(Response, Code)) { ZEN_WARN("S3 THROTTLED [{}] status={} code='{}' message='{}'", Prefix, static_cast(Response.StatusCode), Code, Decoded.ToView()); } return fmt::format("{}: HTTP status ({}) {} - {}", Prefix, static_cast(Response.StatusCode), Code, Decoded.ToView()); } } if (S3IsThrottled(Response, {})) { ZEN_WARN("S3 THROTTLED [{}] status={} (no XML body)", Prefix, static_cast(Response.StatusCode)); } return Response.ErrorMessage(Prefix); } } // namespace zen