//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; import :URL; #ifndef CRAFTER_NETWORK_BROWSER namespace Crafter { // What ClientHTTP1::Fetch needs beyond the URL. export struct FetchOptions { std::string method = "GET"; // Lowercase names, as on HTTPRequest. Host comes from the URL. std::unordered_map 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; // How many redirects are followed at most; the one after comes back // unfollowed. std::uint32_t maxRedirects = 10; }; // Why a Fetch ended on a redirect it did not follow. export enum class FetchStop { // It did not: the chain ended on a final response. None, // The redirect carried no Location, or one that is no URL. NoLocation, // The Location names a scheme other than http or https. OtherScheme, // The Location is a URL the chain has already been sent to. Loop, // maxRedirects redirects had been followed already. Limit, }; export struct FetchResult { // The last response of the chain: the final response, or the // redirect that was not followed, with `stop` saying why. HTTPResponse response; // The URL that answered with `response`. URL url; // Redirects followed to get there. std::uint32_t redirects = 0; FetchStop stop = FetchStop::None; }; // 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 onSuccess, std::function 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(); // One request to an http:// or https:// URL, following redirects by // RFC 9110 §15.4: 301, 302, 303, 307 and 308, for every method. A // 303 turns the request into a GET (a HEAD stays a HEAD) without its // body and content headers; 301 and 302 do the same for a POST // alone; 307 and 308 keep the method and the body. The Location is // resolved against the URL that sent it (URL::Resolve), and a // redirect may move between http:// and https://, `credentials` // serving every https:// hop. Authorization, Proxy-Authorization and // Cookie are not carried to another origin. Each hop is a client of // its own, since one ClientHTTP1 is one origin: the redirect's body // is read and its connection closed before the next request goes // out. A redirect without a usable Location, to another scheme, to // a URL already visited, or past maxRedirects comes back as the // response with `stop` set. Proxies are not consulted. // // Throws std::invalid_argument for a URL that is not http(s) or that // URL::Parse refuses, and whatever a request throws otherwise // (SocketErrorException, SocketTimeoutException, TLSException, // HTTP1::HTTP1ProtocolError, std::runtime_error). static FetchResult Fetch(std::string_view url, const FetchOptions& options = {}); // 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; }; } #endif