Loading src/httpd.cpp +1 −0 Changes for src/httpd.cpp: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -2238,6 +2238,7 @@ bool libhttppp::HttpEvent::_startH3BodyStream(netplus::quic &q, uint64_t stream_ // Whatever of the body RequestEvent didn't read (it answered first) is unwanted: // STOP_SENDING(H3_NO_ERROR) after the response, RFC 9114 §4.1.2. const bool unread = !reader->finished(); std::cerr << "[DBG-H3] finish sid=" << stream_id << " unread=" << unread << " resvalid=" << (r->getHeaderData(":res-valid") != nullptr) << std::endl; reader->abort(); r->_bodyStream.reset(); try { Loading test/httpd_request_body_stream_test.cpp +86 −2 Changes for test/httpd_request_body_stream_test.cpp: 86 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -38,6 +38,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // consumer (backpressure instead of overrun), and a handler answering before reading // the body, followed by more uploads on the same connection than one connection window // holds (credit must still be returned for DATA of the abandoned stream). // - H3 (real QUIC, HttpClient::RequestStream over HTTP/3): known and unknown length up to // 20MB (> the 16MB QUIC stream window, so stream credit must follow consumption), a slow // consumer, an early answer followed by more uploads on the same connection; plus the // non-streamed H3 paths on top of QUIC pull dispatch: a 20MB buffered upload (credit // must still be returned as the server pulls) and onH3StreamHeaders/onH3DataChunk. // - A buffered POST still works when shouldStreamRequestBody() says no. #include <chrono> Loading @@ -58,6 +63,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "checksum_common.h" #include "hpack.h" #include "qpack.h" #include "https_certs.h" #include "https_ca_cert.h" #include "raw_tcp_client.h" Loading Loading @@ -94,7 +100,34 @@ public: return true; } bool shouldStreamRequestBody(libhttppp::HttpRequest &req) const override { return req.getRequestURL() != "/buffered"; return req.getRequestURL() != "/buffered" && req.getRequestURL() != "/h3cb"; } // "/h3cb" over H3: the pre-existing callback ingestion API, answered with "<size>:<sha256>". bool onH3StreamHeaders(netplus::socket *sock, uint64_t sid, const std::vector<libhttppp::qpack::HeaderField> &headers) override { for (auto &h : headers) { if (h.name == ":path" && h.value == "/h3cb") { std::lock_guard<std::mutex> lk(_cbMtx); _cbBodies[{sock, sid}].clear(); return true; } } return false; } void onH3DataChunk(netplus::socket *sock, uint64_t sid, const char *data, size_t len, bool fin) override { std::vector<char> done; { std::lock_guard<std::mutex> lk(_cbMtx); auto &b = _cbBodies[{sock, sid}]; if (len) b.insert(b.end(), data, data + len); if (!fin) return; done = std::move(b); _cbBodies.erase({sock, sid}); } sendH3StreamResponse(sock, sid, 200, "text/plain", std::to_string(done.size()) + ":" + sha256Hex(done)); } void RequestEvent(libhttppp::HttpRequest &req, const int, ULONG_PTR) override { Loading Loading @@ -127,6 +160,10 @@ public: std::string n = std::to_string(body.size()); res.send(req, n, static_cast<int>(n.size())); } private: std::mutex _cbMtx; std::map<std::pair<netplus::socket *, uint64_t>, std::vector<char>> _cbBodies; }; netplus::ssl::CertificateBundle buildTestCertBundle() { Loading Loading @@ -370,6 +407,7 @@ std::string recvSome(netplus::tcp &sock) { int main() { std::cout << "=== Streamed request body test ===" << std::endl; const int kTlsPort = 19651; const int kQuicPort = 19652; const size_t kBig = 20u * 1024 * 1024; // > the 16MB H2 stream window try { Loading @@ -384,8 +422,10 @@ int main() { certs["127.0.0.1"] = bundle; auto sslSock = std::make_unique<netplus::ssl>(certs, "127.0.0.1", kTlsPort, 16, -1); auto quicSock = std::make_unique<netplus::quic>(certs, "127.0.0.1", kQuicPort, 64, -1); auto server = std::make_unique<StreamEvent>( std::vector<netplus::socket *>{tcpSock.get(), sslSock.get()}); std::vector<netplus::socket *>{tcpSock.get(), sslSock.get(), quicSock.get()}); std::thread serverThread([s = server.get()] { s->runEventloop(); }); ServerThreadGuard guard{&serverThread}; std::this_thread::sleep_for(std::chrono::milliseconds(50)); Loading Loading @@ -441,6 +481,50 @@ int main() { "TLS buffered fallback"); } // ---------------- HTTP/3 ---------------- std::cout << "-- HTTP/3 --" << std::endl; { libhttppp::HttpUrl url("https://127.0.0.1:" + std::to_string(kQuicPort) + "/", /*http3=*/true); libhttppp::HttpClient client(url, /*vers=*/3, /*timeoutSec=*/20); for (size_t size : {size_t(1), size_t(65536), size_t(1048576), kBig}) { std::vector<char> data = randomBuffer(size); std::string s = std::to_string(size); streamedUpload(client, "/sum", data, true, true, "H3 known length size=" + s); streamedUpload(client, "/sum", data, false, true, "H3 unknown length size=" + s); } streamedUpload(client, "/slowsum", randomBuffer(kBig), true, true, "H3 slow consumer (backpressure) 20MB"); try { std::vector<char> data = randomBuffer(1048576); libhttppp::HttpRequest req; req.setRequestURL("/early"); client.RequestStream(POSTREQUEST, req, sourceOf(data, 50000), static_cast<int64_t>(data.size())); check(client.lastStatusCode() == 413, "H3 early response before body read"); } catch (std::exception &e) { check(false, std::string("H3 early response: threw ") + e.what()); } for (int i = 0; i < 2; ++i) streamedUpload(client, "/sum", randomBuffer(8u * 1024 * 1024), true, true, "H3 same connection after early response #" + std::to_string(i)); streamedUpload(client, "/buffered", randomBuffer(kBig), true, false, "H3 buffered 20MB (credit returned as pulled)"); try { std::vector<char> data = randomBuffer(kBig); libhttppp::HttpRequest req; req.setRequestURL("/h3cb"); std::vector<char> resp = client.RequestStream(POSTREQUEST, req, sourceOf(data, 50000), static_cast<int64_t>(data.size())); check(std::string(resp.begin(), resp.end()) == std::to_string(data.size()) + ":" + sha256Hex(data), "H3 onH3DataChunk callback ingestion 20MB"); } catch (std::exception &e) { check(false, std::string("H3 onH3DataChunk: threw ") + e.what()); } } // ---------------- HTTP/2 (h2c) ---------------- std::cout << "-- HTTP/2 (h2c) --" << std::endl; { Loading Loading
src/httpd.cpp +1 −0 Changes for src/httpd.cpp: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -2238,6 +2238,7 @@ bool libhttppp::HttpEvent::_startH3BodyStream(netplus::quic &q, uint64_t stream_ // Whatever of the body RequestEvent didn't read (it answered first) is unwanted: // STOP_SENDING(H3_NO_ERROR) after the response, RFC 9114 §4.1.2. const bool unread = !reader->finished(); std::cerr << "[DBG-H3] finish sid=" << stream_id << " unread=" << unread << " resvalid=" << (r->getHeaderData(":res-valid") != nullptr) << std::endl; reader->abort(); r->_bodyStream.reset(); try { Loading
test/httpd_request_body_stream_test.cpp +86 −2 Changes for test/httpd_request_body_stream_test.cpp: 86 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -38,6 +38,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // consumer (backpressure instead of overrun), and a handler answering before reading // the body, followed by more uploads on the same connection than one connection window // holds (credit must still be returned for DATA of the abandoned stream). // - H3 (real QUIC, HttpClient::RequestStream over HTTP/3): known and unknown length up to // 20MB (> the 16MB QUIC stream window, so stream credit must follow consumption), a slow // consumer, an early answer followed by more uploads on the same connection; plus the // non-streamed H3 paths on top of QUIC pull dispatch: a 20MB buffered upload (credit // must still be returned as the server pulls) and onH3StreamHeaders/onH3DataChunk. // - A buffered POST still works when shouldStreamRequestBody() says no. #include <chrono> Loading @@ -58,6 +63,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "checksum_common.h" #include "hpack.h" #include "qpack.h" #include "https_certs.h" #include "https_ca_cert.h" #include "raw_tcp_client.h" Loading Loading @@ -94,7 +100,34 @@ public: return true; } bool shouldStreamRequestBody(libhttppp::HttpRequest &req) const override { return req.getRequestURL() != "/buffered"; return req.getRequestURL() != "/buffered" && req.getRequestURL() != "/h3cb"; } // "/h3cb" over H3: the pre-existing callback ingestion API, answered with "<size>:<sha256>". bool onH3StreamHeaders(netplus::socket *sock, uint64_t sid, const std::vector<libhttppp::qpack::HeaderField> &headers) override { for (auto &h : headers) { if (h.name == ":path" && h.value == "/h3cb") { std::lock_guard<std::mutex> lk(_cbMtx); _cbBodies[{sock, sid}].clear(); return true; } } return false; } void onH3DataChunk(netplus::socket *sock, uint64_t sid, const char *data, size_t len, bool fin) override { std::vector<char> done; { std::lock_guard<std::mutex> lk(_cbMtx); auto &b = _cbBodies[{sock, sid}]; if (len) b.insert(b.end(), data, data + len); if (!fin) return; done = std::move(b); _cbBodies.erase({sock, sid}); } sendH3StreamResponse(sock, sid, 200, "text/plain", std::to_string(done.size()) + ":" + sha256Hex(done)); } void RequestEvent(libhttppp::HttpRequest &req, const int, ULONG_PTR) override { Loading Loading @@ -127,6 +160,10 @@ public: std::string n = std::to_string(body.size()); res.send(req, n, static_cast<int>(n.size())); } private: std::mutex _cbMtx; std::map<std::pair<netplus::socket *, uint64_t>, std::vector<char>> _cbBodies; }; netplus::ssl::CertificateBundle buildTestCertBundle() { Loading Loading @@ -370,6 +407,7 @@ std::string recvSome(netplus::tcp &sock) { int main() { std::cout << "=== Streamed request body test ===" << std::endl; const int kTlsPort = 19651; const int kQuicPort = 19652; const size_t kBig = 20u * 1024 * 1024; // > the 16MB H2 stream window try { Loading @@ -384,8 +422,10 @@ int main() { certs["127.0.0.1"] = bundle; auto sslSock = std::make_unique<netplus::ssl>(certs, "127.0.0.1", kTlsPort, 16, -1); auto quicSock = std::make_unique<netplus::quic>(certs, "127.0.0.1", kQuicPort, 64, -1); auto server = std::make_unique<StreamEvent>( std::vector<netplus::socket *>{tcpSock.get(), sslSock.get()}); std::vector<netplus::socket *>{tcpSock.get(), sslSock.get(), quicSock.get()}); std::thread serverThread([s = server.get()] { s->runEventloop(); }); ServerThreadGuard guard{&serverThread}; std::this_thread::sleep_for(std::chrono::milliseconds(50)); Loading Loading @@ -441,6 +481,50 @@ int main() { "TLS buffered fallback"); } // ---------------- HTTP/3 ---------------- std::cout << "-- HTTP/3 --" << std::endl; { libhttppp::HttpUrl url("https://127.0.0.1:" + std::to_string(kQuicPort) + "/", /*http3=*/true); libhttppp::HttpClient client(url, /*vers=*/3, /*timeoutSec=*/20); for (size_t size : {size_t(1), size_t(65536), size_t(1048576), kBig}) { std::vector<char> data = randomBuffer(size); std::string s = std::to_string(size); streamedUpload(client, "/sum", data, true, true, "H3 known length size=" + s); streamedUpload(client, "/sum", data, false, true, "H3 unknown length size=" + s); } streamedUpload(client, "/slowsum", randomBuffer(kBig), true, true, "H3 slow consumer (backpressure) 20MB"); try { std::vector<char> data = randomBuffer(1048576); libhttppp::HttpRequest req; req.setRequestURL("/early"); client.RequestStream(POSTREQUEST, req, sourceOf(data, 50000), static_cast<int64_t>(data.size())); check(client.lastStatusCode() == 413, "H3 early response before body read"); } catch (std::exception &e) { check(false, std::string("H3 early response: threw ") + e.what()); } for (int i = 0; i < 2; ++i) streamedUpload(client, "/sum", randomBuffer(8u * 1024 * 1024), true, true, "H3 same connection after early response #" + std::to_string(i)); streamedUpload(client, "/buffered", randomBuffer(kBig), true, false, "H3 buffered 20MB (credit returned as pulled)"); try { std::vector<char> data = randomBuffer(kBig); libhttppp::HttpRequest req; req.setRequestURL("/h3cb"); std::vector<char> resp = client.RequestStream(POSTREQUEST, req, sourceOf(data, 50000), static_cast<int64_t>(data.size())); check(std::string(resp.begin(), resp.end()) == std::to_string(data.size()) + ":" + sha256Hex(data), "H3 onH3DataChunk callback ingestion 20MB"); } catch (std::exception &e) { check(false, std::string("H3 onH3DataChunk: threw ") + e.what()); } } // ---------------- HTTP/2 (h2c) ---------------- std::cout << "-- HTTP/2 (h2c) --" << std::endl; { Loading