Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
339 changes: 339 additions & 0 deletions doc/design/parser_redesign.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
# Parser redesign

## Summary

This document describes a redesign of the library’s message parser. As in the existing design, the parser uses a single block of memory allocated at construction time and never exceeds it. That memory is reused across messages, and headers are parsed in place. The parser remains a strictly sans-I/O state machine, while stream handling moves to a separate `message_reader`. Message bodies can be retrieved without copying, including by reading directly from the stream into caller-supplied memory. Chunked trailers are now accessible, and per-message buffer compaction is optimized so that pipelined messages can be read and parsed in place without an extra `memmove`.

The design is not speculative: it is implemented and tested in the Burl project (<https://github.com/cppalliance/burl>).

## The parser interface

```cpp
class parser
{
public:
/// Settings which apply for the life of
/// the parser.
struct config
{
/// The limits enforced while parsing
/// a header.
header_limits hdr_limits;

/// The space reserved for buffering
/// received octets.
std::size_t in_buffer = 64 * 1024;

/// The space reserved for decoded
/// output.
std::size_t dec_buffer = 8 * 1024;

/// The maximum body size.
std::uint64_t body_limit = std::uint64_t(-1);

/// Whether to decode the body according
/// to its Content-Encoding.
bool decode = true;
};

/// Return true if the header has been parsed.
bool
got_header() const noexcept;

/// Return true if the entire message
/// has arrived.
bool
got_body() const noexcept;

/// Return true if octets are buffered
/// past the message.
bool
has_buffered_data() const noexcept;

/// Return the unconsumed octets in the
/// buffer (e.g. Upgrade: websocket).
std::array<capy::const_buffer, 2>
buffered_data() const noexcept;

/// Prepare for a new stream, discarding
/// all state and buffered octets.
void
reset() noexcept;

/// Set the maximum body size, overriding
/// config::body_limit.
void
set_body_limit(std::uint64_t n) noexcept;

/// Return the buffer region for
/// receiving octets.
std::array<capy::mutable_buffer, 2>
prepare() noexcept;

/// Report octets received into the
/// region returned by prepare.
void
commit(std::size_t n) noexcept;

/// Report the end of the stream.
void
commit_eof() noexcept;

/// Return how many body octets may be
/// read straight into caller memory.
std::size_t
direct_capacity() const noexcept;

/// Report octets received directly
/// into caller memory.
void
commit_direct(std::size_t n) noexcept;

/// Parse the header, returning as soon
/// as it is complete.
void
parse_header(std::error_code& ec);

/// Flatten the body in place, de-chunking
/// chunked bodies, and return a view of it.
std::string_view
flatten_body(std::error_code& ec);

/// Copy body octets, or the decoder's
/// output, into caller memory.
template<capy::MutableBufferSequence MB>
std::size_t
read_some(
MB const& buffers,
std::error_code& ec);

/// Assign `dest` with descriptors into the
/// parser's own buffers.
std::span<capy::const_buffer>
pull(
std::span<capy::const_buffer> dest,
std::error_code& ec);

/// Release body octets returned by pull.
void
consume(std::size_t n) noexcept;

/// Append the trailer fields of a
/// chunked payload to `f`.
void
parse_trailer(
fields_base& f,
std::error_code& ec);
};

class request_parser
: public parser
{
public:
/// Constructor.
request_parser() = default;

/// Constructor.
explicit
request_parser(config const& cfg);

/// Prepare for a new message; octets
/// already received for it are retained.
void
start();

/// Return the parsed header.
request_head_base const&
get() const;
};

class response_parser
: public parser
{
public:
/// Constructor.
response_parser() = default;

/// Constructor.
explicit
response_parser(config const& cfg);

/// Prepare for a new message; `head`
/// states it answers a HEAD request.
void
start(bool head = false);

/// Return the parsed header.
response_head_base const&
get() const;
};
```

## Direct reads from the stream into a user-provided buffer

The new `direct_capacity` and `commit_direct` interfaces allow users to query the parser state and avoid unnecessary copies when the body type allows it and all internally buffered data has already been consumed. This ensures that direct reads do not interleave with data already buffered by the parser.

This feature is particularly useful for operations such as file downloads, where the body is not encoded and its size is known in advance, so chunked encoding is not required. In these cases, the body can be read directly into the user's buffer, eliminating an otherwise unnecessary copy.

The following is a possible implementation of a `read_some` algorithm that takes advantage of direct reads:

```cpp
template<
capy::ReadStream S,
capy::MutableBufferSequence MB>
capy::io_task<std::size_t>
read_some_(
S& stream,
parser& pr,
MB buffers)
{
for(;;)
{
// Try to read the internally buffered data first
std::error_code ec;
std::size_t n = pr.read_some(buffers, ec);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of inconsistent error handling, read_some here is taking an outparam std::error_code, where most of the API returns it through io_result. I understand this function is synchronous. Corosio has taken the approach of using io_result to handle this situation as it is more consistent.

@ashtum ashtum Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, io_result would be more consistence with capy.

if(ec != http::condition::need_more_input)
co_return { ec, n };

// Check if the parser state permits a direct read
if(auto const limit = pr.direct_capacity(); limit != 0)
{
// Read directly from the stream, with the buffer
// limited to the size determined by the parser
std::tie(ec, n) = co_await stream.read_some(
capy::buffer_slice(buffers, 0, limit));
pr.commit_direct(n);
if(ec == capy::cond::eof)
{
pr.commit_eof();
if(n == 0)
continue;
ec = {};
}
co_return { ec, n };
}

// Refill the parser and try again
ec = co_await refill(stream, pr);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this refill migrated from Burl's message_reader::refill_? If so it returns io_task<> so it should be std::tie(ec) = co_await refill(...). It might be worth showing refill in the doc.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It serves as a filler to shorten the snippet here. Yes, std::tie(ec) would make it correct.

if(ec)
co_return { ec, 0 };
}
}
```

## Trailer fields are no longer discarded

Unlike the existing parser, the new parser does not discard trailer fields. Instead, it allows users to append them to a separate, user-provided field container. Although these fields could theoretically be read in place, like the header fields, we chose not to do so because it would add complexity and require additional parser state for a rarely used feature.

## No need for a parser configuration service

The parser reads its configuration only during construction. At that point, the configured buffer sizes are determined and the single allocation is performed. The limit parameters are then stored locally: `header_limits` is passed directly to the `head_parser`, which keeps its own copy, while `body_limit` is stored as a member of the parser itself. Since the configuration is never consulted again after construction, there is no need to retain a shared reference or introduce a separate configuration service.

## Quality of the implementation

### Per-message buffer compaction is optimized

In the new implementation, the parser starts parsing the next header at the address where its octets already lie inside the circular buffer. Octets move only when actually required:

- if the header cannot complete below the buffer's ceiling, the buffered octets slide to the front once and parsing resumes via `head_parser::rebase`;
- after the header completes with the body still in flight, the region slides to the front once to maximize contiguous receive space.

As a result, a sequence of pipelined messages that fit in the buffer (the common fast path) parses with no relocation at all.

### Chunked messages are read without compaction

The new implementation walks the chunked messages and assigns their bodies to the user-provided buffer span. As a result, chunked message bodies can be read in place without any copying or memory movement.

## The `message_reader` interface

```cpp
template<capy::ReadStream S>
class message_reader

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very interesting. It is essentially, a message read strream?

@ashtum ashtum Aug 23, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This provides an interface that satisfies both the capy::ReadSource and http::BufferSource requirements and can be used interchangeably depending on the user's needs.

When the user already owns the buffer—for example, when reading a body into an instance of std::string they can use the capy::ReadSource interface to avoid extra copies:

message_reader reader(&stream, &pr);
std::string body;

body.resize(
    pr.get().content_length().value());

auto [ec, n] = co_await reader.read(
    capy::mutable_buffer(body.data(), body.size()));

Note: even with an installed decoder, the parser will decode directly into the user-provided buffer.

When the user wants to perform an operation that requires an external buffer, they can benefit from the parser's internal buffer. For example, streaming the body into a file:

message_reader reader(&stream, &pr);
corosio::stream_file f(co_await capy::this_coro::executor);

f.open(dest, fb::write_only | fb::create | fb::exclusive);

capy::const_buffer bufs[8];
std::uint64_t total = 0;
for (;;)
{
    auto [ec, data] = co_await reader.pull(bufs);
    if (ec == capy::cond::eof)
        break;
    if (ec)
        co_return { ec };

    auto [wec, n] = co_await f.write_some(data);
    total += n;
    reader.consume(n);
    if (wec)
        co_return { wec };
}

The mixed mode allows implementations such as multipart_form to be both simple and efficient. Depending on the contents of each field whether it is read from a file or is a binary or text blob in memory the implementer can chose the appropriate interface.

A similar facility exists in the serializer side (message_writer).

{
public:
/// Constructor; the stream and parser
/// must outlive the reader.
message_reader(S* stream, parser* pr) noexcept;

/// Parse the header, reading from the
/// stream until it is complete.
capy::io_task<>
read_header();

/// Read the complete body into the
/// parser's buffer and return a view of it.
capy::io_task<std::string_view>
read_body();

/// Read body octets into `buffers`.
template<capy::MutableBufferSequence MB>
capy::io_task<std::size_t>
read_some(MB buffers);

/// Read until `buffers` is full or the
/// body is complete.
template<capy::MutableBufferSequence MB>
capy::io_task<std::size_t>
read(MB buffers);

/// Assign `dest` with descriptors into the
/// parser's own buffers.
capy::io_task<std::span<capy::const_buffer>>
pull(std::span<capy::const_buffer> dest);

/// Release body octets returned by pull.
void
consume(std::size_t n) noexcept;
};
```

The rationale for the existence of `message_reader` is to satisfy the `capy::ReadStream`, `http::ReadSource` and `http::BufferSource` concepts, so it composes with generic stream algorithms.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should explicitly state the end of body contract for message_reader/parser in the design doc, e.g. the differences between eof and incomplete.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reference document is more detailed and explains the eof condition in each interface: https://develop.burl.cpp.al/burl/reference/boost/burl/parser.html
But I think the Errors section in the doc should also explain the end of the body conditon.


## Open questions and possibilities regarding `message_reader`

While the current design has proven itself in the Burl project, there are still some open questions worth discussing in this section.

### Should we use `capy::any_read_stream` instead of templating on the stream type?

Using a `capy::any_read_stream` instance would make `message_reader` a concrete type. If we consider `message_reader` to be a type that is exposed to users, for example as part of the Beast2 request handler interface, then using a concrete type may make more sense.

There is also the possibility of providing both a templated implementation and a concrete alias:

```cpp
template<typename Stream>
class basic_message_reader;

using message_reader = basic_message_reader<capy::any_read_stream>;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any_read_stream is a handle, and then we have a constructor that takes Stream* s creating two levels of indirection and two pointers. There is something awkward about it, is there a better way to structure this? Possibly this alias needs to change?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc doesn’t mean that basic_message_reader should be exactly like the current message_reader implementation. It’s more a question of whether we should offer such a concrete type at all.

```

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we follow the design principles of Corosio then we would give the user the building blocks and allow them to opt-in to any of the three levels of abstraction, by uttering the correct type.


### Should we drop `http::BufferSource` in favor of `message_reader`?

If it turns out that the sole purpose of `http::BufferSource` and `http::any_buffer_source` is to provide types that `message_reader` satisfies, then we might not need them anymore.

### Should we add `request_reader` and `response_reader`?

If we provide request- and response-specific versions, the same instance could provide access to the corresponding parser as well as the header section:

```cpp
class request_reader
: public message_reader
{
public:
/// Return the parser.
request_parser&
get_parser();

/// Parse and return the header.
capy::io_task<request_head_base const&>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a reference here is problematic and a footgun. co_return { ec, {} } would compile but bind to an object that is already destroyed by the time the caller sees it.

read_header();
};
```

## Related links

Reference documentation for the Burl implementation:

- [`parser`](https://develop.burl.cpp.al/burl/reference/boost/burl/parser.html)
- [`request_parser`](https://develop.burl.cpp.al/burl/reference/boost/burl/request_parser.html)
- [`response_parser`](https://develop.burl.cpp.al/burl/reference/boost/burl/response_parser.html)
- [`message_reader`](https://develop.burl.cpp.al/burl/reference/boost/burl/message_reader.html)
Loading