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
|
|
@ -106,7 +106,7 @@ Conventions:
|
||||||
- An integer beyond 64 bits becomes a float.
|
- An integer beyond 64 bits becomes a float.
|
||||||
|
|
||||||
No file the drivers write holds these in practice, and a file that fails to parse is treated as a damaged one.
|
No file the drivers write holds these in practice, and a file that fails to parse is treated as a damaged one.
|
||||||
- **HTTP.** `FetchHTTP1` ignores `*_proxy` variables. The OpenRouter requests send `User-Agent: crafter-review`; `app_ready` sends `review-gates`. Reason phrases are the standard ones, not the server's.
|
- **HTTP.** `ClientHTTP1::Fetch` ignores `*_proxy` variables. The OpenRouter requests send `User-Agent: crafter-review`; `app_ready` sends `review-gates`. Reason phrases are the standard ones, not the server's.
|
||||||
- **Seat placement** (SeatPool, Seats, Drivers):
|
- **Seat placement** (SeatPool, Seats, Drivers):
|
||||||
- An account whose 5-hour limit Anthropic has removed reports the weekly window alone. Its burst window (`Seats::BurstWindow`) is the weekly one: it is ranked and admitted by the weekly window with the passes in flight projected on it (0.04 points per pass at xhigh, 0.125 otherwise), under the 95 % cutoff.
|
- An account whose 5-hour limit Anthropic has removed reports the weekly window alone. Its burst window (`Seats::BurstWindow`) is the weekly one: it is ranked and admitted by the weekly window with the passes in flight projected on it (0.04 points per pass at xhigh, 0.125 otherwise), under the 95 % cutoff.
|
||||||
- Every OAuth account and the local login takes at most 128 passes at once, every driver's counted.
|
- Every OAuth account and the local login takes at most 128 passes at once, every driver's counted.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
module;
|
|
||||||
#include <netdb.h>
|
|
||||||
|
|
||||||
module Crafter.Review:Http_impl;
|
module Crafter.Review:Http_impl;
|
||||||
import :Http;
|
import :Http;
|
||||||
import :Json;
|
import :Json;
|
||||||
|
|
@ -20,13 +17,14 @@ namespace Crafter::Review::Http {
|
||||||
// ClientTCP's resolve, socket() and connect failures, which happen
|
// ClientTCP's resolve, socket() and connect failures, which happen
|
||||||
// while the request goes out, apart from a send's or a receive's.
|
// while the request goes out, apart from a send's or a receive's.
|
||||||
bool WhileSending(const SocketErrorException& failure) {
|
bool WhileSending(const SocketErrorException& failure) {
|
||||||
const std::string_view text = failure.what();
|
return failure.phase == SocketPhase::Resolve || failure.phase == SocketPhase::Open || failure.phase == SocketPhase::Connect;
|
||||||
return failure.kind == SocketErrorKind::Resolver || text.starts_with("Could not connect") || text.starts_with("Could not open socket");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "[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) {
|
std::string SocketText(const SocketErrorException& failure) {
|
||||||
if (failure.kind == SocketErrorKind::Resolver) return std::format("[Errno {}] {}", failure.code, gai_strerror(failure.code));
|
if (failure.code().category() == ResolverCategory()) return std::format("[Errno {}] {}", failure.code().value(), failure.code().message());
|
||||||
return Dyn::ErrnoText(failure.code);
|
return Dyn::ErrnoText(failure.code().value());
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr std::array<std::pair<std::int32_t, std::string_view>, 62> Phrases = {{
|
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) {
|
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"}};
|
options.headers = {{"accept-encoding", "identity"}, {"connection", "close"}, {"user-agent", "crafter-review"}};
|
||||||
for (const auto& [name, value] : headers) options.headers[Dyn::Lower(name)] = value;
|
for (const auto& [name, value] : headers) options.headers[Dyn::Lower(name)] = value;
|
||||||
// The timeout bounds each blocking step: the connect, the handshake,
|
// The timeout bounds each blocking step: the connect, the handshake,
|
||||||
|
|
@ -70,9 +68,9 @@ namespace Crafter::Review::Http {
|
||||||
options.connectTimeout = budget;
|
options.connectTimeout = budget;
|
||||||
options.handshakeTimeout = budget;
|
options.handshakeTimeout = budget;
|
||||||
options.timeout = budget;
|
options.timeout = budget;
|
||||||
FetchResultHTTP1 result;
|
FetchResult result;
|
||||||
try {
|
try {
|
||||||
result = FetchHTTP1(url, options);
|
result = ClientHTTP1::Fetch(url, options);
|
||||||
} catch (const SocketTimeoutException&) {
|
} catch (const SocketTimeoutException&) {
|
||||||
throw Error(Wrapped("timed out"));
|
throw Error(Wrapped("timed out"));
|
||||||
} catch (const SocketErrorException& failure) {
|
} 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 std::optional<std::int64_t> status = Dyn::ParseInt(result.response.status);
|
||||||
const auto code = static_cast<std::int32_t>(status.value_or(0));
|
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)));
|
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);
|
if (code < 200 || code >= 300) throw HttpError(code);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
module;
|
module;
|
||||||
#include <netdb.h>
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
@ -141,8 +140,10 @@ namespace Crafter::Review::PgWire {
|
||||||
std::string ConnectErrorText(const std::exception& error) {
|
std::string ConnectErrorText(const std::exception& error) {
|
||||||
if (dynamic_cast<const SocketTimeoutException*>(&error)) return "timed out";
|
if (dynamic_cast<const SocketTimeoutException*>(&error)) return "timed out";
|
||||||
if (const auto* failure = dynamic_cast<const SocketErrorException*>(&error)) {
|
if (const auto* failure = dynamic_cast<const SocketErrorException*>(&error)) {
|
||||||
if (failure->kind == SocketErrorKind::Resolver) return std::format("[Errno {}] {}", failure->code, gai_strerror(failure->code));
|
// "[Errno N] text": the resolver's own text for a resolver
|
||||||
return Dyn::ErrnoText(failure->code);
|
// code, the C library's for an errno.
|
||||||
|
if (failure->code().category() == ResolverCategory()) return std::format("[Errno {}] {}", failure->code().value(), failure->code().message());
|
||||||
|
return Dyn::ErrnoText(failure->code().value());
|
||||||
}
|
}
|
||||||
return error.what();
|
return error.what();
|
||||||
}
|
}
|
||||||
|
|
@ -487,7 +488,7 @@ namespace Crafter::Review::PgWire {
|
||||||
} catch (const SocketTimeoutException&) {
|
} catch (const SocketTimeoutException&) {
|
||||||
throw SocketError("timed out", true);
|
throw SocketError("timed out", true);
|
||||||
} catch (const SocketErrorException& error) {
|
} catch (const SocketErrorException& error) {
|
||||||
throw SocketError(Dyn::ErrnoText(error.code), false);
|
throw SocketError(Dyn::ErrnoText(error.code().value()), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const std::size_t take = std::min(count, buffer.size() - position);
|
const std::size_t take = std::min(count, buffer.size() - position);
|
||||||
|
|
@ -510,7 +511,7 @@ namespace Crafter::Review::PgWire {
|
||||||
try {
|
try {
|
||||||
socket->Send(data.data(), static_cast<std::uint32_t>(data.size()));
|
socket->Send(data.data(), static_cast<std::uint32_t>(data.size()));
|
||||||
} catch (const SocketErrorException& error) {
|
} catch (const SocketErrorException& error) {
|
||||||
throw DbError(std::format("the connection broke ({})", Dyn::ErrnoText(error.code)));
|
throw DbError(std::format("the connection broke ({})", Dyn::ErrnoText(error.code().value())));
|
||||||
} catch (const SocketClosedException&) {
|
} catch (const SocketClosedException&) {
|
||||||
throw DbError(std::format("the connection broke ({})", Dyn::ErrnoText(32)));
|
throw DbError(std::format("the connection broke ({})", Dyn::ErrnoText(32)));
|
||||||
}
|
}
|
||||||
|
|
@ -1170,7 +1171,7 @@ namespace Crafter::Review::PgWire {
|
||||||
out("-- the driver's SQL broker stopped answering mid-call (timed out); nothing of this call was committed");
|
out("-- the driver's SQL broker stopped answering mid-call (timed out); nothing of this call was committed");
|
||||||
return ExitSql;
|
return ExitSql;
|
||||||
} catch (const SocketErrorException& error) {
|
} catch (const SocketErrorException& error) {
|
||||||
out(std::format("-- the driver's SQL broker stopped answering mid-call ({}); nothing of this call was committed", Dyn::ErrnoText(error.code)));
|
out(std::format("-- the driver's SQL broker stopped answering mid-call ({}); nothing of this call was committed", Dyn::ErrnoText(error.code().value())));
|
||||||
return ExitSql;
|
return ExitSql;
|
||||||
} catch (const std::exception& error) {
|
} catch (const std::exception& error) {
|
||||||
out(std::format("-- the driver's SQL broker stopped answering mid-call ({}); nothing of this call was committed", error.what()));
|
out(std::format("-- the driver's SQL broker stopped answering mid-call ({}); nothing of this call was committed", error.what()));
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import std;
|
||||||
import :Json;
|
import :Json;
|
||||||
|
|
||||||
// The HTTP the driver needs: app_ready's preflight, the OpenRouter catalogue
|
// The HTTP the driver needs: app_ready's preflight, the OpenRouter catalogue
|
||||||
// and key probe. On Crafter.Network's FetchHTTP1, which follows redirects; on
|
// and key probe. On Crafter.Network's ClientHTTP1::Fetch, which follows redirects; on
|
||||||
// top of it a status outside 2xx thrown as HttpError, and every other failure
|
// top of it a status outside 2xx thrown as HttpError, and every other failure
|
||||||
// as Error with a fixed text, because those texts reach the run log and
|
// as Error with a fixed text, because those texts reach the run log and
|
||||||
// app_ready's detail. Proxies are not consulted (AGENTS.md).
|
// app_ready's detail. Proxies are not consulted (AGENTS.md).
|
||||||
|
|
|
||||||
|
|
@ -317,7 +317,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
Configuration* network = useLocal
|
Configuration* network = useLocal
|
||||||
? LocalProject({ .projectFile = "../Crafter.Network/project.cpp", .args = depArgs })
|
? LocalProject({ .projectFile = "../Crafter.Network/project.cpp", .args = depArgs })
|
||||||
: GitProject({
|
: GitProject({
|
||||||
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Network.git", .commit = "f68321685006be65f6bc0436fd0175611f1e0a55" },
|
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Network.git", .commit = "7ee938c98e8c76290eef41b03e49ca5eed27a174" },
|
||||||
.args = depArgs,
|
.args = depArgs,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue