- ClientHTTP1 gains connectTimeout (default 15 s), applied to each address the host resolves to. `timeout` only ever bounded the waits for a response, so a host that drops the SYNs held Send() for the kernel's retries, about two minutes, whatever the caller had asked for. - The default Host header puts an IPv6 literal in brackets. - FetchHTTP1(url, options) makes one request to an http:// or https:// URL and follows redirects as Python's urllib does, so urllib code ported over keeps its behaviour: 301/302/303/307/308 for GET and HEAD, 301/302/303 for POST (re-sent as a GET without its body), a relative Location resolved as urljoin resolves it and percent-encoded as urllib quotes it, http and https hops mixed, each redirect's body read before the next request, and the chain stopped by urllib's limits (4 repeats of one URL, 10 different URLs) with the last redirect returned and `limited` set. Proxies are not consulted. New: ShouldTimeoutSilentHTTP1, ShouldFollowRedirectsHTTP1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
3.9 KiB
C++
109 lines
3.9 KiB
C++
//SPDX-License-Identifier: LGPL-3.0-only
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// ClientHTTP1 bounds the TCP connect (connectTimeout) as well as every wait
|
|
// for a response (timeout). Before, the connect had no bound of its own: a
|
|
// host that drops the SYNs held Send() for the kernel's retries, about two
|
|
// minutes, whatever `timeout` said — a readiness probe with a ten-second
|
|
// budget could hang its caller for two minutes.
|
|
//
|
|
// Two servers on loopback. One accepts — the kernel completes the handshake
|
|
// from its backlog — and never answers: the read timeout. The other has a
|
|
// full accept queue, so Linux drops the SYNs of further connects: the connect
|
|
// timeout.
|
|
|
|
#include <arpa/inet.h>
|
|
#include <netinet/in.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
#include <cstdlib>
|
|
|
|
import Crafter.Network;
|
|
import std;
|
|
using namespace Crafter;
|
|
using namespace std::chrono_literals;
|
|
|
|
namespace {
|
|
|
|
int failures = 0;
|
|
|
|
void Check(bool ok, std::string_view what) {
|
|
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
|
if (!ok) ++failures;
|
|
}
|
|
|
|
// A socket listening on 127.0.0.1, on a port the kernel picks.
|
|
struct LoopbackListener {
|
|
int fd = -1;
|
|
std::uint16_t port = 0;
|
|
|
|
explicit LoopbackListener(int backlog) {
|
|
fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
sockaddr_in address {};
|
|
address.sin_family = AF_INET;
|
|
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address));
|
|
listen(fd, backlog);
|
|
socklen_t length = sizeof(address);
|
|
getsockname(fd, reinterpret_cast<sockaddr*>(&address), &length);
|
|
port = ntohs(address.sin_port);
|
|
}
|
|
~LoopbackListener() { close(fd); }
|
|
};
|
|
|
|
long long Milliseconds(std::chrono::steady_clock::duration duration) {
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
{
|
|
LoopbackListener silent(8);
|
|
ClientHTTP1 client("127.0.0.1", silent.port);
|
|
client.timeout = 300ms;
|
|
const auto start = std::chrono::steady_clock::now();
|
|
bool failed = false;
|
|
try {
|
|
client.Send(CreateRequestHTTP("GET", "/", "127.0.0.1"));
|
|
} catch (const std::exception&) {
|
|
failed = true;
|
|
}
|
|
const auto waited = std::chrono::steady_clock::now() - start;
|
|
Check(failed, "a server that accepts and never answers fails the request");
|
|
Check(waited >= 280ms && waited < 5s, std::format("after about `timeout` ({} ms)", Milliseconds(waited)));
|
|
}
|
|
|
|
{
|
|
// Connects queue up until the accept queue is full, which with a
|
|
// backlog of 0 is after one or two; the next one goes unanswered.
|
|
LoopbackListener full(0);
|
|
std::vector<ClientTCP> queued;
|
|
bool queueFull = false;
|
|
for (int i = 0; i < 8 && !queueFull; ++i) {
|
|
try {
|
|
queued.emplace_back("127.0.0.1", full.port, 200ms);
|
|
} catch (const SocketTimeoutException&) {
|
|
queueFull = true;
|
|
}
|
|
}
|
|
Check(queueFull, std::format("the accept queue fills up (after {} connects)", queued.size()));
|
|
|
|
ClientHTTP1 client("127.0.0.1", full.port);
|
|
client.connectTimeout = 300ms;
|
|
const auto start = std::chrono::steady_clock::now();
|
|
bool timedOut = false;
|
|
try {
|
|
client.Send(CreateRequestHTTP("GET", "/", "127.0.0.1"));
|
|
} catch (const SocketTimeoutException&) {
|
|
timedOut = true;
|
|
} catch (const std::exception&) {
|
|
}
|
|
const auto waited = std::chrono::steady_clock::now() - start;
|
|
Check(timedOut, "a connect nobody answers throws SocketTimeoutException");
|
|
Check(waited >= 280ms && waited < 5s, std::format("after about `connectTimeout` ({} ms)", Milliseconds(waited)));
|
|
}
|
|
|
|
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
|
|
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
}
|