//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // HTTP/1.1 wire format — RFC 9112 syntax on top of the RFC 9110 semantics // already modelled by :HTTP (HTTPRequest / HTTPResponse are shared with the // HTTP/3 stack, so a route handler written for one works unchanged on the // other). // // Scope: // - request / response serialisation, with the framing headers // (host, content-length, transfer-encoding, connection) owned by the // serialiser rather than the caller // - an incremental parser that consumes arbitrary socket chunks and // surfaces one message at a time, so a single connection can carry a // keep-alive series (and pipelined requests) // - both body framings that matter in practice: content-length and // `Transfer-Encoding: chunked` (plus read-to-EOF for responses that // use neither, RFC 9112 §6.3 item 8) // // Deliberately rejected rather than guessed at, because getting these // wrong is how request smuggling happens (RFC 9112 §11.2): // - Content-Length together with Transfer-Encoding // - conflicting duplicate Content-Length values // - whitespace between a field name and its colon // - CR/LF inside a field value we are asked to serialise // // This partition has no transport dependency; ClientHTTP1 and ListenerHTTP1 // pair it with :ClientTCP / :ListenerTCP. export module Crafter.Network:HTTP1; import std; import :HTTP; namespace Crafter::HTTP1 { // ---------------- Versions ---------------- export inline constexpr std::string_view kVersion10 = "HTTP/1.0"; export inline constexpr std::string_view kVersion11 = "HTTP/1.1"; // ---------------- Errors ---------------- // Thrown for any input we refuse to interpret. Callers turn this into a // 400 (server side) or propagate it out of Send() (client side). export class HTTP1ProtocolError : public std::runtime_error { public: using std::runtime_error::runtime_error; }; // ---------------- Lexical helpers (RFC 9110 §5.6) ---------------- inline bool IsTChar(unsigned char c) { constexpr std::string_view extra = "!#$%&'*+-.^_`|~"; return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || extra.find(static_cast(c)) != std::string_view::npos; } inline bool IsToken(std::string_view s) { return !s.empty() && std::ranges::all_of(s, [](char c) { return IsTChar(static_cast(c)); }); } inline std::string ToLowerAscii(std::string_view s) { std::string out(s); std::ranges::transform(out, out.begin(), [](unsigned char c) { return static_cast(c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c); }); return out; } inline bool EqualsIgnoreCase(std::string_view a, std::string_view b) { return std::ranges::equal(a, b, [](unsigned char x, unsigned char y) { auto lower = [](unsigned char c) { return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; }; return lower(x) == lower(y); }); } // Optional whitespace — SP / HTAB only (RFC 9110 §5.6.3). inline std::string_view TrimOWS(std::string_view s) { while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) s.remove_prefix(1); while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) s.remove_suffix(1); return s; } // Split a comma-separated field value into trimmed tokens (#rule). export inline std::vector SplitCommaList(std::string_view value) { std::vector parts; while (!value.empty()) { auto comma = value.find(','); parts.push_back(TrimOWS(value.substr(0, comma))); if (comma == std::string_view::npos) break; value.remove_prefix(comma + 1); } std::erase_if(parts, [](std::string_view p) { return p.empty(); }); return parts; } // Whether a comma-separated field value contains `token`, compared // case-insensitively — the shape of `Connection`, `Expect`, // `Transfer-Encoding` and friends. export inline bool HasToken(std::string_view fieldValue, std::string_view token) { return std::ranges::any_of(SplitCommaList(fieldValue), [token](std::string_view candidate) { return EqualsIgnoreCase(candidate, token); }); } // ---------------- Status reason phrases ---------------- // HTTP/1.1 keeps a reason phrase in the status line. It carries no // meaning (RFC 9112 §4) but some ancient clients log it, and an empty // one trips up a few proxies, so emit the registered text where we know // it and a generic class name otherwise. export inline std::string_view ReasonPhrase(std::string_view status) { static const std::unordered_map known = { {"100", "Continue"}, {"101", "Switching Protocols"}, {"103", "Early Hints"}, {"200", "OK"}, {"201", "Created"}, {"202", "Accepted"}, {"204", "No Content"}, {"206", "Partial Content"}, {"301", "Moved Permanently"}, {"302", "Found"}, {"303", "See Other"}, {"304", "Not Modified"}, {"307", "Temporary Redirect"}, {"308", "Permanent Redirect"}, {"400", "Bad Request"}, {"401", "Unauthorized"}, {"403", "Forbidden"}, {"404", "Not Found"}, {"405", "Method Not Allowed"}, {"408", "Request Timeout"}, {"409", "Conflict"}, {"411", "Length Required"}, {"413", "Content Too Large"}, {"414", "URI Too Long"}, {"415", "Unsupported Media Type"}, {"426", "Upgrade Required"}, {"429", "Too Many Requests"}, {"431", "Request Header Fields Too Large"}, {"500", "Internal Server Error"}, {"501", "Not Implemented"}, {"502", "Bad Gateway"}, {"503", "Service Unavailable"}, {"504", "Gateway Timeout"}, {"505", "HTTP Version Not Supported"}, }; if (auto it = known.find(status); it != known.end()) return it->second; if (status.size() == 3) { switch (status[0]) { case '1': return "Informational"; case '2': return "Success"; case '3': return "Redirection"; case '4': return "Client Error"; case '5': return "Server Error"; default: break; } } return "Unknown"; } // IMF-fixdate, the only date format an HTTP/1.1 sender may generate // (RFC 9110 §5.6.7). Always UTC. export inline std::string FormatHTTPDate(std::chrono::system_clock::time_point when) { return std::format("{:%a, %d %b %Y %H:%M:%S} GMT", std::chrono::floor(when)); } // ---------------- Serialisation ---------------- export struct RequestOptions { // false emits `connection: close`, telling the peer we will not // reuse the connection after this exchange. bool keepAlive = true; }; export struct ResponseOptions { bool keepAlive = true; // HTTP/1.0 peers have no keep-alive by default, so reuse has to be // stated explicitly for them (RFC 9112 §9.3). bool explicitKeepAlive = false; // Response to HEAD: send the header section, including the // content-length the body *would* have had, but no body. bool omitBody = false; }; // Headers whose value is derived from the message rather than copied // from the caller's map — emitting the caller's copy as well would // produce a duplicate, and for content-length a smuggling vector. inline bool IsFramingHeader(std::string_view lowerName) { return lowerName == "host" || lowerName == "content-length" || lowerName == "transfer-encoding" || lowerName == "connection"; } inline void ValidateFieldName(std::string_view name) { if (!IsToken(name)) { throw HTTP1ProtocolError("invalid header field name: '" + std::string(name) + "'"); } } // A CR or LF smuggled through a field value would split the message. inline void ValidateFieldValue(std::string_view name, std::string_view value) { if (value.find_first_of("\r\n") != std::string_view::npos) { throw HTTP1ProtocolError("header '" + std::string(name) + "' contains CR/LF"); } } inline void AppendHeaders(std::string& out, const std::unordered_map& headers) { for (const auto& [name, value] : headers) { // HTTP/3 pseudo-headers have no HTTP/1.1 equivalent — they are // already represented by the start line. if (name.empty() || name.front() == ':') continue; std::string lower = ToLowerAscii(name); if (IsFramingHeader(lower)) continue; ValidateFieldName(lower); ValidateFieldValue(lower, value); out += lower; out += ": "; out += value; out += "\r\n"; } } // Serialise a request in origin-form. `authority` becomes the mandatory // Host header (RFC 9112 §3.2); ClientHTTP1 fills it in from the // connection when the caller left it empty. export inline std::string SerializeRequest(const HTTPRequest& request, RequestOptions options = {}) { const std::string method = request.method.empty() ? std::string("GET") : request.method; const std::string target = request.path.empty() ? std::string("/") : request.path; if (!IsToken(method)) throw HTTP1ProtocolError("invalid request method: '" + method + "'"); if (target.find_first_of(" \r\n") != std::string::npos) { throw HTTP1ProtocolError("invalid request target: '" + target + "'"); } ValidateFieldValue("host", request.authority); std::string out; out.reserve(256 + request.headers.size() * 32 + request.body.size()); out += method; out += ' '; out += target; out += " HTTP/1.1\r\n"; out += "host: "; out += request.authority; out += "\r\n"; AppendHeaders(out, request.headers); // A body always gets an explicit length; so does a method that is // normally expected to carry one, since some servers answer 411 for // a bodyless POST that omits it. if (!request.body.empty() || method == "POST" || method == "PUT" || method == "PATCH") { out += "content-length: "; out += std::to_string(request.body.size()); out += "\r\n"; } if (!options.keepAlive) out += "connection: close\r\n"; out += "\r\n"; out += request.body; return out; } export inline std::string SerializeResponse(const HTTPResponse& response, ResponseOptions options = {}) { std::string status = response.status.empty() ? std::string("200") : response.status; if (status.size() != 3 || !std::ranges::all_of(status, [](char c) { return c >= '0' && c <= '9'; })) { throw HTTP1ProtocolError("status must be three digits, got '" + status + "'"); } std::string out; out.reserve(256 + response.headers.size() * 32 + response.body.size()); out += "HTTP/1.1 "; out += status; out += ' '; out += ReasonPhrase(status); out += "\r\n"; AppendHeaders(out, response.headers); // 1xx / 204 / 304 must not carry a content-length at all — sending // one makes some clients wait for a body that never arrives. const bool bodyless = status[0] == '1' || status == "204" || status == "304"; if (!bodyless) { out += "content-length: "; out += std::to_string(response.body.size()); out += "\r\n"; } if (!options.keepAlive) out += "connection: close\r\n"; else if (options.explicitKeepAlive) out += "connection: keep-alive\r\n"; out += "\r\n"; if (!options.omitBody && !bodyless) out += response.body; return out; } // The interim response a server owes a client that sent // `Expect: 100-continue` before it will send its body (RFC 9110 §10.1.1). export inline std::string SerializeContinue() { return "HTTP/1.1 100 Continue\r\n\r\n"; } // ---------------- Incremental parser ---------------- export enum class MessageKind { Request, Response }; // Bounds on what we are willing to buffer before declaring the peer // hostile. maxBody applies to the decoded body, so it also caps a // chunked stream that never ends. export struct MessageLimits { std::size_t maxStartLine = 8 * 1024; std::size_t maxHeaderSection = 64 * 1024; std::uint64_t maxBody = 512ull * 1024 * 1024; }; // Feed socket bytes in with Feed(); call Finish() when the peer closes // its send side. Complete() then reports whether a whole message is // available, and Take*() moves it out. Reset() rearms for the next // message on the same connection, keeping any bytes that already // belonged to it (HTTP/1.1 pipelining). export class MessageParser { public: explicit MessageParser(MessageKind kind, MessageLimits limits = {}) : kind(kind), limits(limits) {} // Responses are framed partly by the request that provoked them: a // HEAD response has no body no matter what its content-length says. // Tell the parser before feeding it bytes. void SetRequestMethod(std::string_view requestMethod) { headRequest = EqualsIgnoreCase(requestMethod, "HEAD"); } void Feed(const char* data, std::size_t size) { if (size != 0) buffer.append(data, size); Advance(); } void Feed(std::span bytes) { Feed(bytes.data(), bytes.size()); } // The peer closed. Completes a read-to-EOF body; anything else // that is mid-message is a truncation error. void Finish() { eof = true; Advance(); } bool HeadersComplete() const noexcept { return headersDone; } bool Complete() const noexcept { return state == State::Done; } // True while the peer is waiting for `100 Continue` before it sends // the body. Clear it with ContinueSent() once the interim response // is on the wire. bool ExpectsContinue() const noexcept { return expectContinue && !Complete(); } void ContinueSent() noexcept { expectContinue = false; } // Whether the connection may carry another message after this one. // Meaningful once HeadersComplete(). bool KeepAlive() const noexcept { return keepAlive; } // No partial message buffered — a clean point to stop reading. bool AtMessageBoundary() const noexcept { return state == State::StartLine && pos == buffer.size() && !headersDone; } std::string_view Method() const noexcept { return method; } std::string_view Version() const noexcept { return version; } std::string_view Status() const noexcept { return status; } HTTPRequest TakeRequest() { if (state != State::Done) throw HTTP1ProtocolError("request is not complete"); HTTPRequest request; request.method = std::move(method); request.path = std::move(path); request.scheme = scheme.empty() ? std::string("http") : std::move(scheme); // Absolute-form targets carry their own authority and win over // Host (RFC 9112 §3.2.2); otherwise Host supplies it. Either way // it lives in the named field, matching the HTTP/3 shape, so it // is removed from the header map. if (authority.empty()) { if (auto it = headers.find("host"); it != headers.end()) authority = it->second; } headers.erase("host"); request.authority = std::move(authority); request.headers = std::move(headers); request.body = std::move(body); return request; } HTTPResponse TakeResponse() { if (state != State::Done) throw HTTP1ProtocolError("response is not complete"); HTTPResponse response; response.status = std::move(status); response.headers = std::move(headers); response.body = std::move(body); return response; } // Rearm for the next message. Any bytes past the one just taken are // retained and parsed immediately, so a pipelined request can be // Complete() as soon as this returns. void Reset() { buffer.erase(0, pos); pos = 0; state = State::StartLine; method.clear(); path.clear(); scheme.clear(); authority.clear(); version.clear(); status.clear(); lastHeaderName.clear(); headers.clear(); body.clear(); headerBytes = 0; bodyRemaining = 0; chunkRemaining = 0; bodyMode = BodyMode::None; headersDone = false; expectContinue = false; keepAlive = true; headRequest = false; Advance(); } private: enum class State { StartLine, Headers, Body, ChunkSize, ChunkData, ChunkTrailingCRLF, Trailers, Done }; enum class BodyMode { None, Length, Chunked, UntilClose }; // Read one CRLF-terminated line. A bare LF is accepted as well: it // is not legal to generate, but recipients may recognise it // (RFC 9112 §2.2) and some embedded clients still emit it. std::optional TryReadLine(std::size_t maxLength) { const std::size_t nl = buffer.find('\n', pos); if (nl == std::string::npos) { if (buffer.size() - pos > maxLength) { throw HTTP1ProtocolError("line exceeds " + std::to_string(maxLength) + " bytes"); } if (eof && buffer.size() != pos) { throw HTTP1ProtocolError("connection closed mid-message"); } return std::nullopt; } std::size_t end = nl; if (end > pos && buffer[end - 1] == '\r') --end; if (end - pos > maxLength) { throw HTTP1ProtocolError("line exceeds " + std::to_string(maxLength) + " bytes"); } std::string_view line(buffer.data() + pos, end - pos); pos = nl + 1; return line; } void ParseRequestLine(std::string_view line) { const auto first = line.find(' '); if (first == std::string_view::npos) throw HTTP1ProtocolError("malformed request line"); const auto second = line.find(' ', first + 1); if (second == std::string_view::npos) { // HTTP/0.9 simple-request. Not supported, and accepting it // would leave us unable to answer with headers. throw HTTP1ProtocolError("malformed request line (missing version)"); } method = std::string(line.substr(0, first)); const std::string_view target = line.substr(first + 1, second - first - 1); version = std::string(line.substr(second + 1)); if (!IsToken(method)) throw HTTP1ProtocolError("invalid method token"); if (target.empty()) throw HTTP1ProtocolError("empty request target"); CheckVersion(); if (target.front() == '/') { path = std::string(target); // origin-form } else if (target == "*") { path = "*"; // asterisk-form (OPTIONS) } else if (const auto sep = target.find("://"); sep != std::string_view::npos) { // absolute-form, as sent to a proxy. scheme = ToLowerAscii(target.substr(0, sep)); const std::string_view rest = target.substr(sep + 3); const auto slash = rest.find('/'); authority = std::string(rest.substr(0, slash)); path = slash == std::string_view::npos ? std::string("/") : std::string(rest.substr(slash)); } else { // authority-form — only legal for CONNECT, which this // listener does not implement. authority = std::string(target); path = std::string(target); } } void ParseStatusLine(std::string_view line) { const auto first = line.find(' '); if (first == std::string_view::npos) throw HTTP1ProtocolError("malformed status line"); version = std::string(line.substr(0, first)); CheckVersion(); std::string_view rest = TrimOWS(line.substr(first + 1)); const auto space = rest.find(' '); status = std::string(space == std::string_view::npos ? rest : rest.substr(0, space)); if (status.size() != 3 || !std::ranges::all_of(status, [](char c) { return c >= '0' && c <= '9'; })) { throw HTTP1ProtocolError("malformed status code '" + status + "'"); } // The reason phrase is discarded: it carries no semantics and // HTTPResponse has nowhere to put it. } void CheckVersion() { if (version != kVersion11 && version != kVersion10) { throw HTTP1ProtocolError("unsupported HTTP version '" + version + "'"); } } void AddHeader(std::string name, std::string_view value) { auto [it, inserted] = headers.try_emplace(std::move(name), std::string(value)); if (!inserted) { // Repeated field lines are equivalent to one comma-joined // value (RFC 9110 §5.3) — except content-length, where // disagreeing values are a smuggling attempt. if (it->first == "content-length" && it->second != value) { throw HTTP1ProtocolError("conflicting content-length values"); } if (it->first != "content-length") { it->second += ", "; it->second += value; } } } void ParseHeaderLine(std::string_view line) { headerBytes += line.size() + 2; if (headerBytes > limits.maxHeaderSection) { throw HTTP1ProtocolError("header section exceeds " + std::to_string(limits.maxHeaderSection) + " bytes"); } if (line.front() == ' ' || line.front() == '\t') { // obs-fold. RFC 9112 §5.2 allows replacing it with a space // rather than rejecting the message. if (lastHeaderName.empty()) throw HTTP1ProtocolError("obs-fold before any header"); auto it = headers.find(lastHeaderName); if (it != headers.end()) { it->second += ' '; it->second += TrimOWS(line); } return; } const auto colon = line.find(':'); if (colon == std::string_view::npos) throw HTTP1ProtocolError("header line has no colon"); const std::string_view rawName = line.substr(0, colon); // "No whitespace is allowed between the field name and colon" — // a recipient MUST reject such a message (RFC 9112 §5.1). if (!IsToken(rawName)) throw HTTP1ProtocolError("invalid header field name"); lastHeaderName = ToLowerAscii(rawName); AddHeader(lastHeaderName, TrimOWS(line.substr(colon + 1))); } std::optional Header(std::string_view name) const { auto it = headers.find(std::string(name)); if (it == headers.end()) return std::nullopt; return std::string_view(it->second); } void ResolveKeepAlive() { const auto connection = Header("connection"); const bool close = connection && HasToken(*connection, "close"); const bool explicitKeep = connection && HasToken(*connection, "keep-alive"); keepAlive = version == kVersion11 ? !close : (explicitKeep && !close); } void ResolveFraming() { const auto transferEncoding = Header("transfer-encoding"); const auto contentLength = Header("content-length"); if (transferEncoding && contentLength) { // Ambiguous framing — the classic smuggling primitive. throw HTTP1ProtocolError("both content-length and transfer-encoding present"); } if (transferEncoding) { auto codings = SplitCommaList(*transferEncoding); if (codings.empty() || !EqualsIgnoreCase(codings.back(), "chunked")) { throw HTTP1ProtocolError("unsupported transfer-encoding '" + std::string(*transferEncoding) + "'"); } if (codings.size() > 1) { throw HTTP1ProtocolError("stacked transfer-codings are not supported"); } bodyMode = BodyMode::Chunked; return; } if (contentLength) { if (contentLength->empty() || !std::ranges::all_of(*contentLength, [](char c) { return c >= '0' && c <= '9'; })) { throw HTTP1ProtocolError("malformed content-length '" + std::string(*contentLength) + "'"); } std::uint64_t length = 0; const auto result = std::from_chars(contentLength->data(), contentLength->data() + contentLength->size(), length); if (result.ec != std::errc{}) throw HTTP1ProtocolError("content-length out of range"); if (length > limits.maxBody) { throw HTTP1ProtocolError("body exceeds " + std::to_string(limits.maxBody) + " bytes"); } bodyRemaining = length; bodyMode = length == 0 ? BodyMode::None : BodyMode::Length; return; } // No explicit framing. A request has no body; a response runs // to connection close (RFC 9112 §6.3). if (kind == MessageKind::Request) { bodyMode = BodyMode::None; } else { bodyMode = BodyMode::UntilClose; keepAlive = false; } } // Returns false when this was an interim (1xx) response and parsing // must restart on the next status line. bool FinishHeaders() { ResolveKeepAlive(); if (kind == MessageKind::Response) { if (status[0] == '1') { // Interim response (100 Continue, 103 Early Hints, …). // It has no body and is not the answer to the request, // so drop it and keep reading. headers.clear(); lastHeaderName.clear(); status.clear(); version.clear(); headerBytes = 0; state = State::StartLine; return false; } if (status == "204" || status == "304" || headRequest) { bodyMode = BodyMode::None; headersDone = true; state = State::Done; return true; } } else if (auto expect = Header("expect")) { expectContinue = HasToken(*expect, "100-continue"); } ResolveFraming(); headersDone = true; switch (bodyMode) { case BodyMode::None: state = State::Done; break; case BodyMode::Length: state = State::Body; break; case BodyMode::Chunked: state = State::ChunkSize; break; case BodyMode::UntilClose: state = State::Body; break; } return true; } void AppendBody(const char* data, std::size_t size) { if (body.size() + size > limits.maxBody) { throw HTTP1ProtocolError("body exceeds " + std::to_string(limits.maxBody) + " bytes"); } body.append(data, size); } // Drive the state machine as far as the buffered bytes allow, then // drop what has been consumed so a long-lived connection (or a // large body arriving in many chunks) doesn't grow the buffer // without bound. void Advance() { AdvanceState(); if (pos != 0 && pos == buffer.size()) { buffer.clear(); pos = 0; } } void AdvanceState() { for (;;) { switch (state) { case State::StartLine: { auto line = TryReadLine(limits.maxStartLine); if (!line) return; // A server should tolerate stray empty lines left // over from a previous message (RFC 9112 §2.2). if (line->empty()) continue; if (kind == MessageKind::Request) ParseRequestLine(*line); else ParseStatusLine(*line); state = State::Headers; continue; } case State::Headers: { auto line = TryReadLine(limits.maxHeaderSection); if (!line) return; if (line->empty()) { FinishHeaders(); continue; } ParseHeaderLine(*line); continue; } case State::Body: { const std::size_t available = buffer.size() - pos; if (bodyMode == BodyMode::UntilClose) { AppendBody(buffer.data() + pos, available); pos = buffer.size(); if (!eof) return; state = State::Done; continue; } const std::size_t take = static_cast( std::min(available, bodyRemaining)); AppendBody(buffer.data() + pos, take); pos += take; bodyRemaining -= take; if (bodyRemaining != 0) { if (eof) throw HTTP1ProtocolError("connection closed mid-body"); return; } state = State::Done; continue; } case State::ChunkSize: { auto line = TryReadLine(limits.maxStartLine); if (!line) return; // Chunk extensions after ';' are legal and ignorable. std::string_view digits = TrimOWS(line->substr(0, line->find(';'))); if (digits.empty()) throw HTTP1ProtocolError("empty chunk size"); std::uint64_t size = 0; const auto result = std::from_chars(digits.data(), digits.data() + digits.size(), size, 16); if (result.ec != std::errc{} || result.ptr != digits.data() + digits.size()) { throw HTTP1ProtocolError("malformed chunk size '" + std::string(digits) + "'"); } if (size == 0) { state = State::Trailers; continue; } chunkRemaining = size; state = State::ChunkData; continue; } case State::ChunkData: { const std::size_t available = buffer.size() - pos; if (available == 0) { if (eof) throw HTTP1ProtocolError("connection closed mid-chunk"); return; } const std::size_t take = static_cast( std::min(available, chunkRemaining)); AppendBody(buffer.data() + pos, take); pos += take; chunkRemaining -= take; if (chunkRemaining != 0) { if (eof) throw HTTP1ProtocolError("connection closed mid-chunk"); return; } state = State::ChunkTrailingCRLF; continue; } case State::ChunkTrailingCRLF: { auto line = TryReadLine(2); if (!line) return; if (!line->empty()) throw HTTP1ProtocolError("chunk not terminated by CRLF"); state = State::ChunkSize; continue; } case State::Trailers: { auto line = TryReadLine(limits.maxHeaderSection); if (!line) return; if (line->empty()) { state = State::Done; continue; } // Trailer fields are merged into the header map; // callers see one flat view of the message. ParseHeaderLine(*line); continue; } case State::Done: return; } } } MessageKind kind; MessageLimits limits; State state = State::StartLine; BodyMode bodyMode = BodyMode::None; std::string buffer; std::size_t pos = 0; bool eof = false; std::string method; std::string path; std::string scheme; std::string authority; std::string version; std::string status; std::string lastHeaderName; std::unordered_map headers; std::string body; std::size_t headerBytes = 0; std::uint64_t bodyRemaining = 0; std::uint64_t chunkRemaining = 0; bool headersDone = false; bool expectContinue = false; bool keepAlive = true; bool headRequest = false; }; }