feat(http1): add an HTTP/1.1 client and listener
HTTP/3-only is not a deployable position yet: plenty of clients, proxies and CI tooling still speak nothing but HTTP/1.1. This adds that path using the request/response types the HTTP/3 stack already uses, so a route handler or call site moves between the two protocols by changing the class name. - :HTTP1 — transport-free wire format. Serialisation with the framing headers owned by the serialiser, and an incremental parser that takes arbitrary socket chunks and yields one message at a time: keep-alive, pipelining, content-length and chunked bodies (with trailers), read-to-EOF responses, interim 1xx skipping, HEAD/204/304 framing and Expect: 100-continue. Ambiguous framing is rejected rather than guessed at (content-length with transfer-encoding, disagreeing content-lengths, whitespace before a colon), and CR/LF in a value we are asked to serialise is refused. - ClientHTTP1 — persistent connection, redialling once when a pooled connection turns out to have been closed by the peer, which is the race HTTP/1.1 keep-alive cannot avoid. Nothing is replayed after a response byte has arrived. - ListenerHTTP1 — one thread per connection (keep-alive connections are idle most of their life and would pin every ThreadPool thread), automatic Date, HEAD, 100-continue, handler-requested close, idle and request timeouts, and 400/404/500 responses. Routes fall back to the query-stripped path so `/thing?x=1` reaches the handler for `/thing`. No TLS: this is `http://` only. Encrypted traffic still goes over HTTP/3, or through a TLS-terminating proxy. Tests: codec unit tests including the malformed inputs above, a client/server round-trip, keep-alive and stale-connection recovery, a 10 MiB body both ways, and interop both directions against curl and python3's http.server (skipped when those are not installed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b758419007
commit
337ce32eca
12 changed files with 2175 additions and 4 deletions
69
interfaces/Crafter.Network-ClientHTTP1.cppm
Normal file
69
interfaces/Crafter.Network-ClientHTTP1.cppm
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
export module Crafter.Network:ClientHTTP1;
|
||||
import std;
|
||||
import :HTTP;
|
||||
import :HTTP1;
|
||||
|
||||
#ifndef CRAFTER_NETWORK_BROWSER
|
||||
namespace Crafter {
|
||||
// HTTP/1.1 client over plain TCP, 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.
|
||||
//
|
||||
// No TLS. This talks `http://`; for an encrypted transport use
|
||||
// ClientHTTP (HTTP/3 over QUIC), or put a TLS-terminating proxy in
|
||||
// front of the HTTP/1.1 endpoint.
|
||||
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);
|
||||
|
||||
~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 (the transport is plaintext).
|
||||
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;
|
||||
|
||||
// Drop the pooled connection; the next Send() dials again.
|
||||
void Disconnect();
|
||||
|
||||
// Limits applied to responses. Set before the first Send().
|
||||
HTTP1::MessageLimits limits;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
};
|
||||
}
|
||||
#endif
|
||||
Loading…
Reference in a new issue