108 lines
6 KiB
C++
108 lines
6 KiB
C++
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
module;
|
||
|
|
#include <netdb.h>
|
||
|
|
|
||
|
|
module Crafter.Review:Http_impl;
|
||
|
|
import :Http;
|
||
|
|
import :Json;
|
||
|
|
import :Text;
|
||
|
|
import Crafter.Network;
|
||
|
|
import std;
|
||
|
|
|
||
|
|
namespace Crafter::Review::Http {
|
||
|
|
namespace {
|
||
|
|
std::string Wrapped(std::string_view reason) {
|
||
|
|
return std::format("<urlopen error {}>", reason);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ClientTCP's resolve, socket() and connect failures, which happen
|
||
|
|
// while the request goes out, apart from a send's or a receive's.
|
||
|
|
bool WhileSending(const SocketErrorException& failure) {
|
||
|
|
const std::string_view text = failure.what();
|
||
|
|
return failure.kind == SocketErrorKind::Resolver || text.starts_with("Could not connect") || text.starts_with("Could not open socket");
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string SocketText(const SocketErrorException& failure) {
|
||
|
|
if (failure.kind == SocketErrorKind::Resolver) return std::format("[Errno {}] {}", failure.code, gai_strerror(failure.code));
|
||
|
|
return Dyn::ErrnoText(failure.code);
|
||
|
|
}
|
||
|
|
|
||
|
|
constexpr std::array<std::pair<std::int32_t, std::string_view>, 62> Phrases = {{
|
||
|
|
{100, "Continue"}, {101, "Switching Protocols"}, {102, "Processing"}, {103, "Early Hints"}, {200, "OK"}, {201, "Created"}, {202, "Accepted"},
|
||
|
|
{203, "Non-Authoritative Information"}, {204, "No Content"}, {205, "Reset Content"}, {206, "Partial Content"}, {207, "Multi-Status"},
|
||
|
|
{208, "Already Reported"}, {226, "IM Used"}, {300, "Multiple Choices"}, {301, "Moved Permanently"}, {302, "Found"}, {303, "See Other"},
|
||
|
|
{304, "Not Modified"}, {305, "Use Proxy"}, {307, "Temporary Redirect"}, {308, "Permanent Redirect"}, {400, "Bad Request"}, {401, "Unauthorized"},
|
||
|
|
{402, "Payment Required"}, {403, "Forbidden"}, {404, "Not Found"}, {405, "Method Not Allowed"}, {406, "Not Acceptable"},
|
||
|
|
{407, "Proxy Authentication Required"}, {408, "Request Timeout"}, {409, "Conflict"}, {410, "Gone"}, {411, "Length Required"},
|
||
|
|
{412, "Precondition Failed"}, {413, "Content Too Large"}, {414, "URI Too Long"}, {415, "Unsupported Media Type"}, {416, "Range Not Satisfiable"},
|
||
|
|
{417, "Expectation Failed"}, {418, "I'm a Teapot"}, {421, "Misdirected Request"}, {422, "Unprocessable Content"}, {423, "Locked"},
|
||
|
|
{424, "Failed Dependency"}, {425, "Too Early"}, {426, "Upgrade Required"}, {428, "Precondition Required"}, {429, "Too Many Requests"},
|
||
|
|
{431, "Request Header Fields Too Large"}, {451, "Unavailable For Legal Reasons"}, {500, "Internal Server Error"}, {501, "Not Implemented"},
|
||
|
|
{502, "Bad Gateway"}, {503, "Service Unavailable"}, {504, "Gateway Timeout"}, {505, "HTTP Version Not Supported"}, {506, "Variant Also Negotiates"},
|
||
|
|
{507, "Insufficient Storage"}, {508, "Loop Detected"}, {510, "Not Extended"}, {511, "Network Authentication Required"},
|
||
|
|
}};
|
||
|
|
}
|
||
|
|
|
||
|
|
HttpError::HttpError(std::int32_t code, const std::string& message) : std::runtime_error(std::format("HTTP Error {}: {}", code, message)), code(code) {}
|
||
|
|
|
||
|
|
HttpError::HttpError(std::int32_t code) : HttpError(code, std::string(Reason(code))) {}
|
||
|
|
|
||
|
|
std::int32_t HttpError::Code() const {
|
||
|
|
return code;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string_view Reason(std::int32_t code) {
|
||
|
|
for (const auto& [value, phrase] : Phrases) {
|
||
|
|
if (value == code) return phrase;
|
||
|
|
}
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
Response UrlOpen(std::string_view url, const std::map<std::string, std::string>& headers, double timeout) {
|
||
|
|
FetchOptionsHTTP1 options;
|
||
|
|
options.headers = {{"accept-encoding", "identity"}, {"connection", "close"}, {"user-agent", "crafter-review"}};
|
||
|
|
for (const auto& [name, value] : headers) options.headers[Dyn::Lower(name)] = value;
|
||
|
|
// The timeout bounds each blocking step: the connect, the handshake,
|
||
|
|
// every read.
|
||
|
|
const std::chrono::milliseconds budget(static_cast<std::int64_t>(timeout * 1000));
|
||
|
|
options.connectTimeout = budget;
|
||
|
|
options.handshakeTimeout = budget;
|
||
|
|
options.timeout = budget;
|
||
|
|
FetchResultHTTP1 result;
|
||
|
|
try {
|
||
|
|
result = FetchHTTP1(url, options);
|
||
|
|
} catch (const SocketTimeoutException&) {
|
||
|
|
throw Error(Wrapped("timed out"));
|
||
|
|
} catch (const SocketErrorException& failure) {
|
||
|
|
throw Error(WhileSending(failure) ? Wrapped(SocketText(failure)) : SocketText(failure));
|
||
|
|
} catch (const SocketClosedException&) {
|
||
|
|
throw Error("Remote end closed connection without response");
|
||
|
|
} catch (const TLSException& failure) {
|
||
|
|
throw Error(Wrapped(failure.what()));
|
||
|
|
} catch (const std::invalid_argument& failure) {
|
||
|
|
throw Error(failure.what());
|
||
|
|
} catch (const std::runtime_error& failure) {
|
||
|
|
const std::string_view text = failure.what();
|
||
|
|
if (text.starts_with("timed out")) throw Error("timed out");
|
||
|
|
if (text.starts_with("connection closed")) throw Error("Remote end closed connection without response");
|
||
|
|
throw Error(failure.what());
|
||
|
|
} catch (const std::exception& failure) {
|
||
|
|
throw Error(failure.what());
|
||
|
|
}
|
||
|
|
const std::optional<std::int64_t> status = Dyn::ParseInt(result.response.status);
|
||
|
|
const auto code = static_cast<std::int32_t>(status.value_or(0));
|
||
|
|
if (result.limited) {
|
||
|
|
throw HttpError(code, std::format("The HTTP server returned a redirect error that would lead to an infinite loop.\nThe last 30x error message was:\n{}", Reason(code)));
|
||
|
|
}
|
||
|
|
if (code < 200 || code >= 300) throw HttpError(code);
|
||
|
|
return {code, std::move(result.response.body)};
|
||
|
|
}
|
||
|
|
|
||
|
|
Json JsonBody(const Response& response) {
|
||
|
|
if (const std::optional<std::string> error = Dyn::Utf8DecodeError(response.body)) throw Dyn::Error("UnicodeDecodeError", *error);
|
||
|
|
if (const std::optional<std::string> error = Dyn::JsonDecodeError(response.body)) throw Dyn::Error("JSONDecodeError", *error);
|
||
|
|
return ParseLoose(response.body);
|
||
|
|
}
|
||
|
|
}
|