diff --git a/include/boost/http/parser.hpp b/include/boost/http/parser.hpp index f466a3ad..321c2ab9 100644 --- a/include/boost/http/parser.hpp +++ b/include/boost/http/parser.hpp @@ -307,6 +307,28 @@ class parser core::string_view body() const; + /** Return true if data is buffered past the message. + + After a complete message, returns true when the + parser's buffer still holds octets that lie + beyond it, such as the start of a pipelined + message or data the peer sent past the message + framing. Returns false before the message is + complete. + + This does not include the message body, which is + retrieved separately via @ref body or + @ref pull_body. + + @return true if octets remain buffered past the + completed message. + + @see @ref is_complete, @ref release_buffered_data. + */ + BOOST_HTTP_DECL + bool + has_buffered_data() const noexcept; + /** Return unconsumed data past the last message. Use this after an upgrade or CONNECT request diff --git a/src/parser.cpp b/src/parser.cpp index af60a7cc..a64d36c2 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -1316,13 +1316,23 @@ class parser::impl detail::throw_logic_error(); auto cbp = (is_plain() ? cb0_ : cb1_).data(); - BOOST_ASSERT(cbp[1].size() == 0); - BOOST_ASSERT(cbp[0].size() == body_avail_); + BOOST_ASSERT(body_avail_ <= cbp[0].size()); return core::string_view( static_cast(cbp[0].data()), body_avail_); } + bool + has_buffered_data() const noexcept + { + if(state_ != state::complete) + return false; + + if(is_plain()) + return cb0_.size() > body_avail_; + return cb0_.size() > 0; + } + void set_body_limit(std::uint64_t n) { @@ -1579,6 +1589,14 @@ release_buffered_data() noexcept return {}; } +bool +parser:: +has_buffered_data() const noexcept +{ + BOOST_ASSERT(impl_); + return impl_->has_buffered_data(); +} + void parser:: set_body_limit(std::uint64_t n) diff --git a/test/unit/parser.cpp b/test/unit/parser.cpp index f14db6c6..50247afd 100644 --- a/test/unit/parser.cpp +++ b/test/unit/parser.cpp @@ -1748,6 +1748,144 @@ struct parser_test pr.get().payload(), payload::chunked); } + void + testBodyWithTrailingData() + { + parser_config cfg{false}; + cfg.headers.max_size = 100; + cfg.min_buffer = 100; + auto pcfg = make_parser_config(cfg); + + auto const check = [&pcfg]( + core::string_view octets, + core::string_view body) + { + response_parser pr(pcfg); + pr.reset(); + pr.start(); + pr.commit(capy::buffer_copy( + pr.prepare(), + capy::const_buffer( + octets.data(), octets.size()))); + + system::error_code ec; + pr.parse(ec); + BOOST_TEST(! ec); + BOOST_TEST(pr.got_header()); + pr.parse(ec); + BOOST_TEST(! ec); + BOOST_TEST(pr.is_complete()); + BOOST_TEST_EQ(pr.body(), body); + BOOST_TEST(pr.has_buffered_data()); + }; + + check( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 2\r\n" + "\r\n" + "ok" + "HTTP/1.1 200 OK\r\n", "ok"); + + check( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 2\r\n" + "\r\n" + "okX", "ok"); + } + + void + testHasBufferedData() + { + parser_config cfg{false}; + cfg.headers.max_size = 100; + cfg.min_buffer = 100; + auto pcfg = make_parser_config(cfg); + + auto const check = [&pcfg]( + core::string_view octets, + core::string_view body, + bool buffered) + { + response_parser pr(pcfg); + pr.reset(); + pr.start(); + pr.commit(capy::buffer_copy( + pr.prepare(), + capy::const_buffer( + octets.data(), octets.size()))); + + system::error_code ec; + pr.parse(ec); + BOOST_TEST(! ec); + BOOST_TEST(pr.got_header()); + pr.parse(ec); + BOOST_TEST(! ec); + BOOST_TEST(pr.is_complete()); + + // the body is not counted as buffered data + BOOST_TEST_EQ(pr.body(), body); + BOOST_TEST_EQ(pr.has_buffered_data(), buffered); + }; + + check( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 2\r\n" + "\r\n" + "ok", "ok", false); + + check( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 2\r\n" + "\r\n" + "ok" + "HTTP/1.1 200 OK\r\n", "ok", true); + + check( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 2\r\n" + "\r\n" + "okX", "ok", true); + + check( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "2\r\nok\r\n0\r\n\r\n", "ok", false); + + check( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "2\r\nok\r\n0\r\n\r\n" + "GARBAGE", "ok", true); + + check( + "HTTP/1.1 204 No Content\r\n" + "\r\n" + "GARBAGE", "", true); + + { + response_parser pr(pcfg); + pr.reset(); + pr.start(); + core::string_view octets = + "HTTP/1.1 200 OK\r\n" + "Content-Length: 10\r\n" + "\r\n" + "123"; // body incomplete + pr.commit(capy::buffer_copy( + pr.prepare(), + capy::const_buffer( + octets.data(), octets.size()))); + system::error_code ec; + pr.parse(ec); + BOOST_TEST(pr.got_header()); + pr.parse(ec); + BOOST_TEST(! pr.is_complete()); + BOOST_TEST(! pr.has_buffered_data()); + } + } + void run() { @@ -1764,6 +1902,8 @@ struct parser_test testMultipleMessageInPlaceChunked(); testSetBodyLimit(); testAccessHeaderAfterBodyError(); + testBodyWithTrailingData(); + testHasBufferedData(); #else // For profiling for(int i = 0; i < 10000; ++i )