From 0a5126c387c4e404e8648e4fa9f9165d7d9df83b Mon Sep 17 00:00:00 2001 From: Mohammad Nejati Date: Fri, 5 Jun 2026 11:30:14 +0000 Subject: [PATCH 1/3] Add parser::read(stream) --- CMakeLists.txt | 1 - CMakePresets.json | 10 -- include/boost/http/parser.hpp | 56 ++++++++++ src/error.cpp | 2 +- src/parser.cpp | 8 ++ test/unit/parser.cpp | 192 ++++++++++++++++++++++++++++++++++ 6 files changed, 257 insertions(+), 12 deletions(-) delete mode 100644 CMakePresets.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 27e60ab7..ffcfffcc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,7 +60,6 @@ set(BOOST_HTTP_DEPENDENCIES Boost::core Boost::json Boost::mp11 - Boost::static_assert Boost::system Boost::throw_exception Boost::type_traits diff --git a/CMakePresets.json b/CMakePresets.json deleted file mode 100644 index 6a3792d9..00000000 --- a/CMakePresets.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "version": 6, - "cmakeMinimumRequired": { - "major": 3, - "minor": 20, - "patch": 0 - }, - "configurePresets": [], - "buildPresets": [] -} diff --git a/include/boost/http/parser.hpp b/include/boost/http/parser.hpp index 76858a72..866e06c7 100644 --- a/include/boost/http/parser.hpp +++ b/include/boost/http/parser.hpp @@ -340,6 +340,31 @@ class parser capy::io_task<> read_header(Stream& stream); + /** Asynchronously read a complete HTTP message. + + Reads from the stream until the message is fully + parsed or an error occurs. The body is accumulated + in the parser's internal buffer and can be retrieved + via @ref body after completion. + + If the parser's internal buffer fills before the + message is complete, the operation completes with + @ref error::in_place_overflow. + + @par Preconditions + @li @ref reset has been called + @li @ref start has been called + + @param stream The stream to read from. + + @return An awaitable yielding `(error_code)`. + + @see @ref body, @ref read_header. + */ + template + capy::io_task<> + read(Stream& stream); + /** Asynchronously read body data into buffers. Reads from the stream and copies body data into @@ -517,6 +542,37 @@ read_header(Stream& stream) } } +template +capy::io_task<> +parser:: +read(Stream& stream) +{ + system::error_code ec; + for(;;) + { + parse(ec); + + if(is_complete()) + co_return {}; + + if(ec && ec != condition::need_more_input) + co_return {ec}; + + if(ec == condition::need_more_input) + { + auto mbs = prepare(); + + auto [read_ec, n] = co_await stream.read_some(mbs); + if(read_ec == capy::cond::eof) + commit_eof(); + else if(!read_ec) + commit(n); + else + co_return {read_ec}; + } + } +} + template capy::io_task parser:: diff --git a/src/error.cpp b/src/error.cpp index 589b0755..35d3bdc2 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -43,7 +43,7 @@ message( case error::expect_100_continue: return "expect 100 continue"; case error::end_of_message: return "end of message"; case error::end_of_stream: return "end of stream"; - case error::in_place_overflow: return "in place overflow"; + case error::in_place_overflow: return "in-place overflow"; case error::need_data: return "need data"; case error::bad_connection: return "bad Connection"; diff --git a/src/parser.cpp b/src/parser.cpp index 3db5a4cd..af60a7cc 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -1273,6 +1273,10 @@ class parser::impl (is_plain() ? cb0_ : cb1_).data(), body_avail_); return detail::make_span(cbp_); + case state::reset: + if(got_header_) + return {}; + BOOST_FALLTHROUGH; default: detail::throw_logic_error(); } @@ -1291,6 +1295,10 @@ class parser::impl (is_plain() ? cb0_ : cb1_).consume(n); body_avail_ -= n; return; + case state::reset: + if(got_header_) + return; + BOOST_FALLTHROUGH; default: detail::throw_logic_error(); } diff --git a/test/unit/parser.cpp b/test/unit/parser.cpp index 4ca54b12..f14db6c6 100644 --- a/test/unit/parser.cpp +++ b/test/unit/parser.cpp @@ -1974,6 +1974,192 @@ struct parser_coro_test BOOST_TEST(r.success); } + void + testSourceForLargeBody() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> + { + std::string const expected(40000, 'x'); + + capy::test::read_stream rs(f); + rs.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 40000\r\n" + "\r\n"); + rs.provide(expected); + + response_parser pr(res_cfg_); + pr.reset(); + pr.start(); + + auto source = pr.source_for(rs); + + std::string body; + capy::const_buffer arr[16]; + + for(;;) + { + auto [ec, bufs] = co_await source.pull(arr); + if(ec == capy::cond::eof) + break; + BOOST_TEST(ec != error::in_place_overflow); + if(ec) + co_return; + std::size_t n = 0; + for(auto const& buf : bufs) + { + body.append( + static_cast(buf.data()), + buf.size()); + n += buf.size(); + } + source.consume(n); + } + + BOOST_TEST(body == expected); + BOOST_TEST(pr.is_complete()); + }); + BOOST_TEST(r.success); + } + + void + testSourceForBodyTooLarge() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> + { + parser_config cfg{false}; + cfg.body_limit = 5; + auto small_cfg = make_parser_config(cfg); + + capy::test::read_stream rs(f, 1); + rs.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 100\r\n" + "\r\n" + "this body is way over the configured limit"); + + response_parser pr(small_cfg); + pr.reset(); + pr.start(); + + auto source = pr.source_for(rs); + + capy::const_buffer arr[16]; + auto [ec, bufs] = co_await source.pull(arr); + + BOOST_TEST(static_cast(ec)); + BOOST_TEST(ec != capy::cond::eof); + BOOST_TEST(capy::buffer_size(bufs) == 0); + BOOST_TEST(! pr.is_complete()); + }); + BOOST_TEST(r.success); + } + + void + testRead() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> + { + capy::test::read_stream rs(f, 1); + rs.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 13\r\n" + "\r\n" + "Hello, World!"); + + response_parser pr(res_cfg_); + pr.reset(); + pr.start(); + + auto [ec] = co_await pr.read(rs); + if(ec) + co_return; + + BOOST_TEST(pr.is_complete()); + BOOST_TEST(pr.body() == "Hello, World!"); + }); + BOOST_TEST(r.success); + } + + void + testReadChunked() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> + { + capy::test::read_stream rs(f, 1); + rs.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nHello\r\n" + "7\r\n, World\r\n" + "0\r\n\r\n"); + + response_parser pr(res_cfg_); + pr.reset(); + pr.start(); + + auto [ec] = co_await pr.read(rs); + if(ec) + co_return; + + BOOST_TEST(pr.is_complete()); + BOOST_TEST(pr.body() == "Hello, World"); + }); + BOOST_TEST(r.success); + } + + void + testReadEof() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> + { + capy::test::read_stream rs(f, 1); + + response_parser pr(res_cfg_); + pr.reset(); + pr.start(); + + auto [ec] = co_await pr.read(rs); + + BOOST_TEST(static_cast(ec)); + BOOST_TEST(! pr.is_complete()); + }); + BOOST_TEST(r.success); + } + + void + testReadOverflow() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> + { + std::string const big(40000, 'x'); + + capy::test::read_stream rs(f); + rs.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 40000\r\n" + "\r\n"); + rs.provide(big); + + response_parser pr(res_cfg_); + pr.reset(); + pr.start(); + + auto [ec] = co_await pr.read(rs); + + BOOST_TEST(static_cast(ec)); + BOOST_TEST(! pr.is_complete()); + }); + BOOST_TEST(r.success); + } + void run() { @@ -1982,6 +2168,12 @@ struct parser_coro_test testReadWriteSinkChunked(); testSourceFor(); testSourceForChunked(); + testSourceForLargeBody(); + testSourceForBodyTooLarge(); + testRead(); + testReadChunked(); + testReadEof(); + testReadOverflow(); } }; From e6682180e3faf39ca267092f5a702cfccce2e966 Mon Sep 17 00:00:00 2001 From: Mohammad Nejati Date: Fri, 5 Jun 2026 11:46:46 +0000 Subject: [PATCH 2/3] Fix -Werror missing-field-initializers in bcrypt.hpp --- include/boost/http/bcrypt.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/boost/http/bcrypt.hpp b/include/boost/http/bcrypt.hpp index b78aaa62..47f3eb59 100644 --- a/include/boost/http/bcrypt.hpp +++ b/include/boost/http/bcrypt.hpp @@ -646,6 +646,7 @@ hash_async( rounds, ver, {}, + {}, {}}; } @@ -679,6 +680,7 @@ compare_async( detail::password_buf(password), detail::hash_buf(hash_str), false, + {}, {}}; } From 1e6d04c207969bf6fcfe398196d973e8d93e7d0e Mon Sep 17 00:00:00 2001 From: Mohammad Nejati Date: Fri, 5 Jun 2026 12:10:31 +0000 Subject: [PATCH 3/3] Fix MSVC C4927 on error_code conversions --- include/boost/http/parser.hpp | 6 +++--- include/boost/http/serializer.hpp | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/include/boost/http/parser.hpp b/include/boost/http/parser.hpp index 866e06c7..f466a3ad 100644 --- a/include/boost/http/parser.hpp +++ b/include/boost/http/parser.hpp @@ -528,7 +528,7 @@ read_header(Stream& stream) co_return {}; if(ec != condition::need_more_input) - co_return {ec}; + co_return {std::error_code(ec)}; auto mbs = prepare(); @@ -556,7 +556,7 @@ read(Stream& stream) co_return {}; if(ec && ec != condition::need_more_input) - co_return {ec}; + co_return {std::error_code(ec)}; if(ec == condition::need_more_input) { @@ -737,7 +737,7 @@ read(capy::ReadStream auto& stream, Sink&& sink) } if(ec) - co_return {ec}; + co_return {std::error_code(ec)}; } } diff --git a/include/boost/http/serializer.hpp b/include/boost/http/serializer.hpp index b29d1594..42fb7fa5 100644 --- a/include/boost/http/serializer.hpp +++ b/include/boost/http/serializer.hpp @@ -659,7 +659,7 @@ class serializer::sink { if(cbs.error() == error::need_data) break; - co_return {cbs.error()}; + co_return {std::error_code(cbs.error())}; } if(capy::buffer_empty(*cbs)) @@ -709,7 +709,7 @@ class serializer::sink { if(cbs.error() == error::need_data) continue; - co_return {cbs.error()}; + co_return {std::error_code(cbs.error())}; } if(capy::buffer_empty(*cbs)) @@ -906,7 +906,7 @@ class serializer::sink { if(cbs.error() == error::need_data) continue; - co_return {cbs.error()}; + co_return {std::error_code(cbs.error())}; } if(capy::buffer_empty(*cbs))