feat(http): the HTTP layer on ClientHTTP1::Fetch and the typed socket error
Crafter.Network master now carries, in their final shape, the pieces the HTTP layer and the SQL broker were written against on a side branch: ClientHTTP1::Fetch follows redirects by RFC 9110 §15.4 on a URL type of its own, and SocketErrorException is a std::system_error with the phase that failed. The broker and the HTTP layer read the errno or the resolver code from code() and the phase from the field, instead of matching message prefixes, and the pin moves to that commit, so the build no longer needs --local against a checkout of the side branch. The messages the run log, the preflight detail and the SQL helper show are unchanged, but for the wording of a refused URL.
This commit is contained in:
parent
101471d81a
commit
51bdd6298a
5 changed files with 19 additions and 20 deletions
|
|
@ -1,9 +1,6 @@
|
|||
// 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;
|
||||
|
|
@ -20,13 +17,14 @@ namespace Crafter::Review::Http {
|
|||
// 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");
|
||||
return failure.phase == SocketPhase::Resolve || failure.phase == SocketPhase::Open || failure.phase == SocketPhase::Connect;
|
||||
}
|
||||
|
||||
// "[Errno N] text", with the resolver's own text for a resolver
|
||||
// code and the C library's for an errno.
|
||||
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);
|
||||
if (failure.code().category() == ResolverCategory()) return std::format("[Errno {}] {}", failure.code().value(), failure.code().message());
|
||||
return Dyn::ErrnoText(failure.code().value());
|
||||
}
|
||||
|
||||
constexpr std::array<std::pair<std::int32_t, std::string_view>, 62> Phrases = {{
|
||||
|
|
@ -61,7 +59,7 @@ namespace Crafter::Review::Http {
|
|||
}
|
||||
|
||||
Response UrlOpen(std::string_view url, const std::map<std::string, std::string>& headers, double timeout) {
|
||||
FetchOptionsHTTP1 options;
|
||||
FetchOptions 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,
|
||||
|
|
@ -70,9 +68,9 @@ namespace Crafter::Review::Http {
|
|||
options.connectTimeout = budget;
|
||||
options.handshakeTimeout = budget;
|
||||
options.timeout = budget;
|
||||
FetchResultHTTP1 result;
|
||||
FetchResult result;
|
||||
try {
|
||||
result = FetchHTTP1(url, options);
|
||||
result = ClientHTTP1::Fetch(url, options);
|
||||
} catch (const SocketTimeoutException&) {
|
||||
throw Error(Wrapped("timed out"));
|
||||
} catch (const SocketErrorException& failure) {
|
||||
|
|
@ -93,7 +91,7 @@ namespace Crafter::Review::Http {
|
|||
}
|
||||
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) {
|
||||
if (result.stop == FetchStop::Loop || result.stop == FetchStop::Limit) {
|
||||
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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue