- 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>
152 lines
7.2 KiB
C++
152 lines
7.2 KiB
C++
//SPDX-License-Identifier: LGPL-3.0-only
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
export module Crafter.Network:ClientHTTP1;
|
|
import std;
|
|
import :HTTP;
|
|
import :HTTP1;
|
|
import :TLS;
|
|
|
|
#ifndef CRAFTER_NETWORK_BROWSER
|
|
namespace Crafter {
|
|
// HTTP/1.1 client over TCP, with or without TLS, for peers that cannot
|
|
// speak HTTP/3. The request/response types are the ones the HTTP/3 client
|
|
// uses, so swapping ClientHTTP for ClientHTTP1 is a one-line change at the
|
|
// call site.
|
|
//
|
|
// The connection is persistent: the first Send() dials, and later calls
|
|
// reuse the socket unless the peer asked for it to be closed
|
|
// (`Connection: close`, or an HTTP/1.0 response without
|
|
// `Connection: keep-alive`). A reused connection that turns out to have
|
|
// been closed by the peer in the meantime — the unavoidable race in
|
|
// HTTP/1.1 keep-alive — is redialled once and the request replayed;
|
|
// a freshly dialled connection is never replayed on, so a genuinely
|
|
// broken server surfaces as an exception rather than a retry loop.
|
|
//
|
|
// Thread-affinity matches ClientHTTP: one ClientHTTP1 serves one caller
|
|
// at a time; distinct instances are independent.
|
|
//
|
|
// `http://` or `https://` is chosen by the constructor: pass
|
|
// TLSClientCredentials and every byte goes through libssl (see :TLS),
|
|
// leave them out and the transport is plaintext. TLS changes nothing
|
|
// above the transport — the same keep-alive, replay and framing rules
|
|
// apply, and `authority` still defaults to host:port with the scheme's
|
|
// default port elided (443 under TLS, 80 without).
|
|
export class ClientHTTP1 {
|
|
public:
|
|
std::string host;
|
|
std::uint16_t port;
|
|
|
|
ClientHTTP1(const char* host, std::uint16_t port);
|
|
ClientHTTP1(std::string host, std::uint16_t port);
|
|
|
|
// https://. The credentials verify the server's certificate chain and
|
|
// its name against `host` by default; see TLSClientCredentials for
|
|
// self-signed peers, private trust anchors and client certificates.
|
|
// Throws TLSException if the certificate is rejected.
|
|
ClientHTTP1(const char* host, std::uint16_t port, TLSClientCredentials credentials);
|
|
ClientHTTP1(std::string host, std::uint16_t port, TLSClientCredentials credentials);
|
|
|
|
~ClientHTTP1();
|
|
ClientHTTP1(const ClientHTTP1&) = delete;
|
|
ClientHTTP1(ClientHTTP1&&) noexcept;
|
|
|
|
// Send a request and read the full response. `authority` defaults to
|
|
// the host:port this client was constructed with; `scheme` is ignored
|
|
// — HTTP/1.1 request targets are origin-form and the transport was
|
|
// already decided by the constructor.
|
|
HTTPResponse Send(const HTTPRequest& request);
|
|
|
|
// Send a request and deliver the response (or the error text) via
|
|
// callback, on Crafter.Thread's ThreadPool.
|
|
void SendAsync(const HTTPRequest& request,
|
|
std::function<void(HTTPResponse)> onSuccess,
|
|
std::function<void(std::string)> onError);
|
|
|
|
// Whether a pooled connection is currently open. Mostly useful for
|
|
// tests asserting that keep-alive actually kept the socket.
|
|
bool Connected() const noexcept;
|
|
|
|
// Whether this client speaks https://.
|
|
bool Secure() const noexcept;
|
|
|
|
// ALPN protocol the last connection negotiated, empty for plaintext
|
|
// or when the server offered no ALPN. For an https:// client with the
|
|
// default credentials this is "http/1.1".
|
|
std::string_view Protocol() const noexcept;
|
|
|
|
// Drop the pooled connection; the next Send() dials again.
|
|
void Disconnect();
|
|
|
|
// Limits applied to responses. Set before the first Send().
|
|
HTTP1::MessageLimits limits;
|
|
// How long to wait for the next piece of a response before giving
|
|
// up on a server that accepted the connection and then went quiet.
|
|
std::chrono::milliseconds timeout{30000};
|
|
// How long the TLS handshake may take, on an https:// client. Separate
|
|
// from `timeout` because it covers a multi-round-trip exchange before
|
|
// any request has been written.
|
|
std::chrono::milliseconds handshakeTimeout{15000};
|
|
// How long the TCP connect may take, for each address the host
|
|
// resolves to (see ClientTCP). Without it a host that drops the SYNs
|
|
// held Send() for the kernel's retries, about two minutes, whatever
|
|
// `timeout` said.
|
|
std::chrono::milliseconds connectTimeout{15000};
|
|
|
|
private:
|
|
struct Impl;
|
|
std::unique_ptr<Impl> impl;
|
|
};
|
|
|
|
// What FetchHTTP1 needs beyond the URL.
|
|
export struct FetchOptionsHTTP1 {
|
|
std::string method = "GET";
|
|
// Lowercase names, as on HTTPRequest. Host comes from the URL.
|
|
std::unordered_map<std::string, std::string> headers;
|
|
std::string body;
|
|
// As on ClientHTTP1, for every request of the chain.
|
|
std::chrono::milliseconds connectTimeout{15000};
|
|
std::chrono::milliseconds handshakeTimeout{15000};
|
|
std::chrono::milliseconds timeout{30000};
|
|
HTTP1::MessageLimits limits;
|
|
// For every https:// request of the chain.
|
|
TLSClientCredentials credentials;
|
|
// Python urllib's limits: a chain stops at a redirect to a URL it has
|
|
// already been sent to `maxRepeats` times, or once it has been sent
|
|
// to `maxRedirects` different URLs.
|
|
std::uint32_t maxRedirects = 10;
|
|
std::uint32_t maxRepeats = 4;
|
|
};
|
|
|
|
export struct FetchResultHTTP1 {
|
|
// The last response of the chain. A redirect only when it had nothing
|
|
// to follow — no Location, a method urllib does not redirect, a
|
|
// scheme other than http(s) — or when the limits stopped the chain.
|
|
HTTPResponse response;
|
|
// The URL that answered with `response`.
|
|
std::string url;
|
|
// Redirects followed to get there.
|
|
std::uint32_t redirects = 0;
|
|
// The limits stopped the chain; `response` is the redirect that would
|
|
// have gone past them.
|
|
bool limited = false;
|
|
};
|
|
|
|
// One request to an http:// or https:// URL, following redirects the way
|
|
// Python's urllib does, so a port of urllib code keeps its behaviour:
|
|
// 301, 302, 303, 307 and 308 are followed for GET and HEAD, the first
|
|
// three also for POST; the redirected request is a GET (a HEAD stays a
|
|
// HEAD) without the body and its content headers. A relative Location is
|
|
// resolved against the URL that sent it, as urljoin resolves it; a
|
|
// redirect may switch between http:// and https://. Each redirect's body
|
|
// is read before the next request goes out. Proxies are not consulted.
|
|
//
|
|
// The URL is `scheme://[user@]host[:port][/path][?query][#fragment]`,
|
|
// with an IPv6 host in brackets; the fragment is not sent and the user
|
|
// part is dropped. Throws std::invalid_argument for a URL that is not
|
|
// http(s) or has no host or a bad port, and whatever a request throws
|
|
// (SocketErrorException, SocketTimeoutException, TLSException,
|
|
// std::runtime_error) otherwise.
|
|
export FetchResultHTTP1 FetchHTTP1(std::string_view url, const FetchOptionsHTTP1& options = {});
|
|
}
|
|
#endif
|