Repository navigation
Conversation
http_parser_execute() goes on after on_message_complete, and HttpMessage::ParseFromIOBuf() checks Completed() only between IOBuf blocks (ParseFromArray() never does). When pipelined HTTP/1 messages arrive in the same read, the url, header fields and body of the next message are appended to the same HttpMessage: two requests become one request whose path is both paths joined, with both header sets merged and both bodies concatenated, and the client gets a single response. Responses read by a client are merged the same way. The merge goes on across blocks and reads as long as no block ends exactly at a message end, so one HttpMessage also grows past the per-message header limit, and every merged request parses the whole joined url again. Pause the parser in on_message_complete_cb, so that ParseFromArray() and ParseFromIOBuf() return the bytes of one message; HPE_PAUSED is not an error. ParseHttpMessage() pops them and returns the message, and InputMessenger parses the rest with a new HttpContext, as it already does when the messages arrive in separate reads. request_sanity and response_sanity end their input with a CRLF after the body, which is not part of the message and is now left unparsed.
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
4 open issues
When the parser is paused aton_message_complete,http_parser_executewill stop mid-block, but… · New This loop only retries onEAGAIN. A syscall can also fail withEINTR, which would currently… · New Fix spelling in the comment: 'Emtpy' -> 'Empty' and 'sliently' -> 'silently'. · New This helper manually manages heap memory and assumes allocation succeeds. To make the test helper… · New
What changed in this PR
Fixes HTTP/1 message parsing so parsing stops exactly at on_message_complete, preventing pipelined messages received in a single read from being merged into one logical request/response.
Changes:
- Pause
http_parseron message completion and treatHPE_PAUSEDas a successful parse outcome. - Update/extend unit tests to verify pipelined requests/responses are parsed as distinct messages.
- Document the updated parsing contract in
http_message.h.
Review effort: Lite
| &_parser, &g_parser_settings, blk.data(), blk.size()); | ||
| nprocessed += n; | ||
| _parsed_block_size += n; | ||
| if (_parser.http_errno != 0) { | ||
| if (!ParserOk(_parser)) { | ||
| // May try HTTP on other formats, failure is norm. | ||
| RPC_VLOG << "Fail to parse http message, parser=" << _parser | ||
| << ", buf=" << butil::ToPrintable(buf); |
There was a problem hiding this comment.
[replied by brpc-oncall robot] The loop already checks Completed() right after each http_parser_execute() and breaks. on_message_complete_cb() sets the stage to HTTP_ON_MESSAGE_COMPLETE before pausing, so once the parser is paused the message is completed and no subsequent blocks are parsed.
| const ssize_t nr = read_buf.append_from_file_descriptor(sock->fd(), 4096); | ||
| if (nr < 0 && errno == EAGAIN) { | ||
| ASSERT_LT(butil::cpuwide_time_us(), start_time + 1000000L) | ||
| << "Too long! bodies.size()=" << bodies.size(); | ||
| bthread_usleep(1000); | ||
| continue; | ||
| } | ||
| ASSERT_GT(nr, 0); |
There was a problem hiding this comment.
[replied by brpc-oncall robot] Fixed, the loop now retries on EINTR as well.
| // Parse from butil::IOBuf. | ||
| // Emtpy `buf' is sliently ignored, which is different from ParseFromArray. | ||
| // Parsing stops at the end of the message as in ParseFromArray. | ||
| // Returns bytes parsed, -1 on failure. |
There was a problem hiding this comment.
[replied by brpc-oncall robot] Fixed the typos.
| void AppendInOneBlock(butil::IOBuf* buf, const std::string& data) { | ||
| char* copy = (char*)malloc(data.size()); | ||
| memcpy(copy, data.data(), data.size()); | ||
| ASSERT_EQ(0, buf->append_user_data(copy, data.size(), free)); | ||
| } |
There was a problem hiding this comment.
[replied by brpc-oncall robot] Keeping the helper minimal on purpose: the input is a few dozen bytes, and an allocation failure would fail the test anyway, so an explicit null check would only add noise. This matches the existing test helpers in the repo.
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
8 open issues
AppendInOneBlockis defined with external linkage in a test TU. This can cause ODR/linker errors… · New This loop only retries onEAGAIN. A syscall can also fail withEINTR, which would currently… When the parser is paused aton_message_complete,http_parser_executewill stop mid-block, but… Ifappend_user_datafails,ASSERT_EQaborts the test without freeingcopy, leaking memory in… · New The timeout value1000000L(and related sleep interval) is duplicated. Consider extracting a… · New The timeout value1000000L(and related sleep interval) is duplicated. Consider extracting a… · New This helper manually manages heap memory and assumes allocation succeeds. To make the test helper… Fix spelling in the comment: 'Emtpy' -> 'Empty' and 'sliently' -> 'silently'.
Review effort: Lite
| void AppendInOneBlock(butil::IOBuf* buf, const std::string& data) { | ||
| char* copy = (char*)malloc(data.size()); | ||
| memcpy(copy, data.data(), data.size()); | ||
| ASSERT_EQ(0, buf->append_user_data(copy, data.size(), free)); | ||
| } |
| char* copy = (char*)malloc(data.size()); | ||
| memcpy(copy, data.data(), data.size()); | ||
| ASSERT_EQ(0, buf->append_user_data(copy, data.size(), free)); |
| int64_t start_time = butil::cpuwide_time_us(); | ||
| while (sock->fd() < 0) { | ||
| bthread_usleep(1000); | ||
| ASSERT_LT(butil::cpuwide_time_us(), start_time + 1000000L) << "Too long!"; | ||
| } |



What problem does this PR solve?
Problem Summary:
http_parser_execute()keeps going afteron_message_complete, andHttpMessage::ParseFromIOBuf()checksCompleted()only between IOBuf blocks (ParseFromArray()never does). When pipelined HTTP/1 messages arrive in the same read, the url, header fields and body of the next message are appended to the sameHttpMessage: two requests become one request whose path is both paths joined, with both header sets merged and both bodies concatenated, and the client gets a single response. Responses read by a client are merged the same way.The merge goes on across IOBuf blocks and reads as long as no block ends exactly at a message end, so one
HttpMessagealso grows past the per-message header limit, and every merged request parses the whole joined url again (quadratic parsing cost).What is changed and the side effects?
Changed:
HttpMessage::on_message_complete_cb()pauses the parser once the message is complete, soParseFromArray()andParseFromIOBuf()return the bytes of exactly one message.HPE_PAUSEDis treated as success instead of an error.ParseHttpMessage()pops the bytes of that message and returns it;InputMessengerparses the remaining bytes with a newHttpContext, exactly as it already does when the messages arrive in separate reads.HttpMessageTest.parse_stops_at_end_of_pipelined_request,parse_stops_at_end_of_pipelined_chunked_requestandparse_stops_at_end_of_pipelined_responseintest/brpc_http_message_unittest.cpp, andHttpTest.pipelined_requests_get_one_response_eachintest/brpc_http_rpc_protocol_unittest.cpp.request_sanity/response_sanitynow expect the CRLF after the body to be left unparsed, since it is not part of the message.Side effects:
Performance effects: no regression on the normal path; merged messages no longer cause repeated re-parsing of a growing url.
Breaking backward compatibility: no public interface change.
ParseFromArray()/ParseFromIOBuf()now stop at the end of the message, which is documented inhttp_message.h.Check List:
Tests run (CMake Debug build):
brpc_http_message_unittest,brpc_http_rpc_protocol_unittest,brpc_http_parser_unittest,brpc_server_unittest,brpc_builtin_service_unittest,brpc_socket_unittest,brpc_alpn_protocol_unittest(run fromtest/so the test certs are found) andbrpc_input_messenger_unittest. The three newHttpMessageTest.parse_stops_at_end_of_pipelined_*tests and the newHttpTest.pipelined_requests_get_one_response_eachtest fail without the source change and pass with it.brpc_channel_unittest/brpc_http_rpc_protocol_unittestpass except the DNS-dependentChannelTest.parse_hostnameandHttpTest.parse_http_addresscases, which cannot resolve external names in the offline environment.🤖 This PR was automatically created by brpc-oncall