Loading src/http.h +9 −0 Changes for src/http.h: 9 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include <memory> #include <deque> #include <map> #include <chrono> #include <netplus/socket.h> #include <netplus/connection.h> Loading Loading @@ -453,6 +454,14 @@ namespace libhttppp { size_t emptyCount = 0; unsigned int backoffMs = 1; bool finished = false; // Set while totalSent < contentLength and the peer's flow-control // window is the reason no DATA can go out (not an upstream stall). // Reaped by _reapStalledH2Streams() if it stays true too long — // guards against a peer that stops sending WINDOW_UPDATE entirely, // which would otherwise leave the stream (and its tempreq) parked // in activeStreams forever. bool windowBlocked = false; std::chrono::steady_clock::time_point blockedSince{}; }; struct H2PendingIncoming { Loading src/httpd.cpp +95 −9 Changes for src/httpd.cpp: 95 added lines, 9 removed lines. Original line number Diff line number Diff line Loading @@ -66,6 +66,18 @@ static constexpr uint8_t H2_FLAG_PRIORITY = 0x20; static constexpr size_t H2_FRAME_HEADER_LEN = 9; static constexpr size_t H2_MAX_FRAME_SIZE = 16384; // RFC 7540 §6.9.1: a flow-control window must never exceed 2^31-1. static constexpr int64_t H2_MAX_WINDOW_SIZE = 0x7fffffff; // How long a streaming response may sit blocked purely on a closed peer // flow-control window (no WINDOW_UPDATE arriving) before we give up on // it. Guards against a peer that silently stops crediting the window — // without this, the stream (and its per-stream tempreq) stays parked in // activeStreams forever and the connection looks hung from the client's // side until it tears down the TCP connection itself. static constexpr std::chrono::seconds H2_WINDOW_STALL_TIMEOUT{30}; static const char H2_CLIENT_PREFACE[] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"; static constexpr size_t H2_CLIENT_PREFACE_LEN = 24; Loading Loading @@ -640,7 +652,13 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq, // First: drain any buffered pending data while (!ss->pendingData.empty() && ss->pendingOffset < ss->pendingData.size()) { int32_t allowed = std::min(sw, cw); if (allowed <= 0) break; if (allowed <= 0) { if (!ss->windowBlocked) { ss->windowBlocked = true; ss->blockedSince = std::chrono::steady_clock::now(); } break; } size_t remaining = ss->contentLength > ss->totalSent ? ss->contentLength - ss->totalSent : 0; if (remaining == 0) break; Loading @@ -657,6 +675,7 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq, cw -= static_cast<int32_t>(chunk_sz); ss->pendingOffset += chunk_sz; ss->totalSent += chunk_sz; ss->windowBlocked = false; } if (ss->pendingOffset > 0) { ss->pendingData.erase(0, ss->pendingOffset); Loading @@ -675,7 +694,13 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq, // Fetch more data from upstream via ResponseEvent if window available while (ss->totalSent < ss->contentLength) { int32_t allowed = std::min(sw, cw); if (allowed <= 0) break; if (allowed <= 0) { if (!ss->windowBlocked) { ss->windowBlocked = true; ss->blockedSince = std::chrono::steady_clock::now(); } break; } // NOTE: Do NOT reset SendData.pos here — application code // (e.g. file serving) uses pos as a source-file read cursor Loading Loading @@ -716,6 +741,10 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq, allowed = std::min(sw, cw); if (allowed <= 0) { // Buffer remainder for next WINDOW_UPDATE if (!ss->windowBlocked) { ss->windowBlocked = true; ss->blockedSince = std::chrono::steady_clock::now(); } size_t buf_remaining = ss->contentLength > ss->totalSent ? ss->contentLength - ss->totalSent : 0; size_t buf_len = std::min(sa - doff, buf_remaining); Loading @@ -741,6 +770,7 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq, cw -= static_cast<int32_t>(chunk_sz); doff += chunk_sz; ss->totalSent += chunk_sz; ss->windowBlocked = false; } ss->tempreq->SendData.clear(); } else { Loading Loading @@ -785,6 +815,39 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq, } } // Abort streaming responses that have been sitting blocked purely on a // closed peer flow-control window for too long. Called on every // Http2RequestEvent invocation (i.e. whenever any frame arrives on the // connection) so that even a connection where one stream is permanently // stuck still gets cleaned up as soon as something else happens on it — // there is no separate timer driving the H2 layer. void libhttppp::HttpEvent::_reapStalledH2Streams(HttpRequest &cureq, std::string &out) { auto &streams = cureq.h2state().activeStreams; if (streams.empty()) return; auto now = std::chrono::steady_clock::now(); std::vector<uint32_t> sids; sids.reserve(streams.size()); for (auto &kv : streams) sids.push_back(kv.first); for (uint32_t sid : sids) { auto it = streams.find(sid); if (it == streams.end()) continue; auto &ss = it->second; if (!ss->windowBlocked) continue; if (now - ss->blockedSince < H2_WINDOW_STALL_TIMEOUT) continue; // Peer never credited the window back — give up on this stream // instead of leaving it (and its tempreq) parked forever. uint8_t rst[4] = {0, 0, 0, 0x03}; // FLOW_CONTROL_ERROR out += h2BuildFrame(H2_FRAME_RST_STREAM, 0, sid, std::string(reinterpret_cast<char*>(rst), 4)); cureq.h2state().peerStreamWindows.erase(sid); streams.erase(it); } } bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon, const int tid, ULONG_PTR args, Loading Loading @@ -817,6 +880,12 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon, out += h2ConnectionWindowBoost(); } // Reap any streams that have been window-blocked past the stall // timeout before doing anything else — a new request coming in on an // otherwise-stuck connection is exactly the point where an old // zombie stream would never get cleaned up any other way. _reapStalledH2Streams(cureq, out); // Process all complete frames in RecvData reprocess: while (off + H2_FRAME_HEADER_LEN <= cureq.RecvData.size()) { Loading Loading @@ -848,13 +917,20 @@ reprocess: (static_cast<uint8_t>(data[off + soff + 3]) << 16) | (static_cast<uint8_t>(data[off + soff + 4]) << 8) | static_cast<uint8_t>(data[off + soff + 5]); if (sid_s == 0x04) { // SETTINGS_INITIAL_WINDOW_SIZE if (sid_s == 0x04 && sval <= H2_MAX_WINDOW_SIZE) { // SETTINGS_INITIAL_WINDOW_SIZE // RFC 7540 §6.5.2: values above 2^31-1 are a // FLOW_CONTROL_ERROR — reject rather than let the // int32_t narrowing below turn it negative. int32_t delta = static_cast<int32_t>(sval) - cureq.h2state().peerInitialStreamWindow; cureq.h2state().peerInitialStreamWindow = static_cast<int32_t>(sval); // Adjust all existing per-stream windows (RFC 7540 §6.9.2) for (auto &kv : cureq.h2state().peerStreamWindows) kv.second += delta; // Adjust all existing per-stream windows (RFC 7540 §6.9.2), // clamped so an existing window can't be pushed past // the legal maximum either. for (auto &kv : cureq.h2state().peerStreamWindows) { int64_t adjusted = static_cast<int64_t>(kv.second) + delta; kv.second = static_cast<int32_t>(std::min<int64_t>(adjusted, H2_MAX_WINDOW_SIZE)); } } else if (sid_s == 0x05) { // SETTINGS_MAX_FRAME_SIZE // RFC 7540 §6.5.2: value must be between 16384 and 16777215 if (sval >= 16384 && sval <= 16777215) Loading Loading @@ -1024,14 +1100,24 @@ reprocess: (static_cast<uint8_t>(data[off + 2]) << 8) | static_cast<uint8_t>(data[off + 3]); if (sid == 0) { cureq.h2state().peerConnWindow += static_cast<int32_t>(increment); // Clamp instead of letting the int32_t wrap negative // (RFC 7540 §6.9.1). An overflowed window never // recovers on its own — every future send sees // min(sw, cw) <= 0 and the connection stalls // permanently until the client tears down the TCP // connection and reconnects. int64_t newWindow = static_cast<int64_t>(cureq.h2state().peerConnWindow) + increment; cureq.h2state().peerConnWindow = static_cast<int32_t>(std::min<int64_t>(newWindow, H2_MAX_WINDOW_SIZE)); } else { // Only update streams we're still tracking — ignore // WINDOW_UPDATEs for already-completed streams to // avoid creating phantom peerStreamWindows entries. auto swit = cureq.h2state().peerStreamWindows.find(sid); if (swit != cureq.h2state().peerStreamWindows.end()) swit->second += static_cast<int32_t>(increment); if (swit != cureq.h2state().peerStreamWindows.end()) { int64_t newWindow = static_cast<int64_t>(swit->second) + increment; swit->second = static_cast<int32_t>(std::min<int64_t>(newWindow, H2_MAX_WINDOW_SIZE)); } } // Resume any parked streaming responses _resumeH2Streams(cureq, out, tid, args); Loading src/httpd.h +1 −0 Changes for src/httpd.h: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -122,6 +122,7 @@ namespace libhttppp { const int tid, ULONG_PTR args); void _resumeH2Streams(HttpRequest &cureq, std::string &out, const int tid, ULONG_PTR args); void _reapStalledH2Streams(HttpRequest &cureq, std::string &out); }; class HttpD { Loading Loading
src/http.h +9 −0 Changes for src/http.h: 9 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include <memory> #include <deque> #include <map> #include <chrono> #include <netplus/socket.h> #include <netplus/connection.h> Loading Loading @@ -453,6 +454,14 @@ namespace libhttppp { size_t emptyCount = 0; unsigned int backoffMs = 1; bool finished = false; // Set while totalSent < contentLength and the peer's flow-control // window is the reason no DATA can go out (not an upstream stall). // Reaped by _reapStalledH2Streams() if it stays true too long — // guards against a peer that stops sending WINDOW_UPDATE entirely, // which would otherwise leave the stream (and its tempreq) parked // in activeStreams forever. bool windowBlocked = false; std::chrono::steady_clock::time_point blockedSince{}; }; struct H2PendingIncoming { Loading
src/httpd.cpp +95 −9 Changes for src/httpd.cpp: 95 added lines, 9 removed lines. Original line number Diff line number Diff line Loading @@ -66,6 +66,18 @@ static constexpr uint8_t H2_FLAG_PRIORITY = 0x20; static constexpr size_t H2_FRAME_HEADER_LEN = 9; static constexpr size_t H2_MAX_FRAME_SIZE = 16384; // RFC 7540 §6.9.1: a flow-control window must never exceed 2^31-1. static constexpr int64_t H2_MAX_WINDOW_SIZE = 0x7fffffff; // How long a streaming response may sit blocked purely on a closed peer // flow-control window (no WINDOW_UPDATE arriving) before we give up on // it. Guards against a peer that silently stops crediting the window — // without this, the stream (and its per-stream tempreq) stays parked in // activeStreams forever and the connection looks hung from the client's // side until it tears down the TCP connection itself. static constexpr std::chrono::seconds H2_WINDOW_STALL_TIMEOUT{30}; static const char H2_CLIENT_PREFACE[] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"; static constexpr size_t H2_CLIENT_PREFACE_LEN = 24; Loading Loading @@ -640,7 +652,13 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq, // First: drain any buffered pending data while (!ss->pendingData.empty() && ss->pendingOffset < ss->pendingData.size()) { int32_t allowed = std::min(sw, cw); if (allowed <= 0) break; if (allowed <= 0) { if (!ss->windowBlocked) { ss->windowBlocked = true; ss->blockedSince = std::chrono::steady_clock::now(); } break; } size_t remaining = ss->contentLength > ss->totalSent ? ss->contentLength - ss->totalSent : 0; if (remaining == 0) break; Loading @@ -657,6 +675,7 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq, cw -= static_cast<int32_t>(chunk_sz); ss->pendingOffset += chunk_sz; ss->totalSent += chunk_sz; ss->windowBlocked = false; } if (ss->pendingOffset > 0) { ss->pendingData.erase(0, ss->pendingOffset); Loading @@ -675,7 +694,13 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq, // Fetch more data from upstream via ResponseEvent if window available while (ss->totalSent < ss->contentLength) { int32_t allowed = std::min(sw, cw); if (allowed <= 0) break; if (allowed <= 0) { if (!ss->windowBlocked) { ss->windowBlocked = true; ss->blockedSince = std::chrono::steady_clock::now(); } break; } // NOTE: Do NOT reset SendData.pos here — application code // (e.g. file serving) uses pos as a source-file read cursor Loading Loading @@ -716,6 +741,10 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq, allowed = std::min(sw, cw); if (allowed <= 0) { // Buffer remainder for next WINDOW_UPDATE if (!ss->windowBlocked) { ss->windowBlocked = true; ss->blockedSince = std::chrono::steady_clock::now(); } size_t buf_remaining = ss->contentLength > ss->totalSent ? ss->contentLength - ss->totalSent : 0; size_t buf_len = std::min(sa - doff, buf_remaining); Loading @@ -741,6 +770,7 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq, cw -= static_cast<int32_t>(chunk_sz); doff += chunk_sz; ss->totalSent += chunk_sz; ss->windowBlocked = false; } ss->tempreq->SendData.clear(); } else { Loading Loading @@ -785,6 +815,39 @@ void libhttppp::HttpEvent::_resumeH2Streams(HttpRequest &cureq, } } // Abort streaming responses that have been sitting blocked purely on a // closed peer flow-control window for too long. Called on every // Http2RequestEvent invocation (i.e. whenever any frame arrives on the // connection) so that even a connection where one stream is permanently // stuck still gets cleaned up as soon as something else happens on it — // there is no separate timer driving the H2 layer. void libhttppp::HttpEvent::_reapStalledH2Streams(HttpRequest &cureq, std::string &out) { auto &streams = cureq.h2state().activeStreams; if (streams.empty()) return; auto now = std::chrono::steady_clock::now(); std::vector<uint32_t> sids; sids.reserve(streams.size()); for (auto &kv : streams) sids.push_back(kv.first); for (uint32_t sid : sids) { auto it = streams.find(sid); if (it == streams.end()) continue; auto &ss = it->second; if (!ss->windowBlocked) continue; if (now - ss->blockedSince < H2_WINDOW_STALL_TIMEOUT) continue; // Peer never credited the window back — give up on this stream // instead of leaving it (and its tempreq) parked forever. uint8_t rst[4] = {0, 0, 0, 0x03}; // FLOW_CONTROL_ERROR out += h2BuildFrame(H2_FRAME_RST_STREAM, 0, sid, std::string(reinterpret_cast<char*>(rst), 4)); cureq.h2state().peerStreamWindows.erase(sid); streams.erase(it); } } bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon, const int tid, ULONG_PTR args, Loading Loading @@ -817,6 +880,12 @@ bool libhttppp::HttpEvent::Http2RequestEvent(netplus::con &curcon, out += h2ConnectionWindowBoost(); } // Reap any streams that have been window-blocked past the stall // timeout before doing anything else — a new request coming in on an // otherwise-stuck connection is exactly the point where an old // zombie stream would never get cleaned up any other way. _reapStalledH2Streams(cureq, out); // Process all complete frames in RecvData reprocess: while (off + H2_FRAME_HEADER_LEN <= cureq.RecvData.size()) { Loading Loading @@ -848,13 +917,20 @@ reprocess: (static_cast<uint8_t>(data[off + soff + 3]) << 16) | (static_cast<uint8_t>(data[off + soff + 4]) << 8) | static_cast<uint8_t>(data[off + soff + 5]); if (sid_s == 0x04) { // SETTINGS_INITIAL_WINDOW_SIZE if (sid_s == 0x04 && sval <= H2_MAX_WINDOW_SIZE) { // SETTINGS_INITIAL_WINDOW_SIZE // RFC 7540 §6.5.2: values above 2^31-1 are a // FLOW_CONTROL_ERROR — reject rather than let the // int32_t narrowing below turn it negative. int32_t delta = static_cast<int32_t>(sval) - cureq.h2state().peerInitialStreamWindow; cureq.h2state().peerInitialStreamWindow = static_cast<int32_t>(sval); // Adjust all existing per-stream windows (RFC 7540 §6.9.2) for (auto &kv : cureq.h2state().peerStreamWindows) kv.second += delta; // Adjust all existing per-stream windows (RFC 7540 §6.9.2), // clamped so an existing window can't be pushed past // the legal maximum either. for (auto &kv : cureq.h2state().peerStreamWindows) { int64_t adjusted = static_cast<int64_t>(kv.second) + delta; kv.second = static_cast<int32_t>(std::min<int64_t>(adjusted, H2_MAX_WINDOW_SIZE)); } } else if (sid_s == 0x05) { // SETTINGS_MAX_FRAME_SIZE // RFC 7540 §6.5.2: value must be between 16384 and 16777215 if (sval >= 16384 && sval <= 16777215) Loading Loading @@ -1024,14 +1100,24 @@ reprocess: (static_cast<uint8_t>(data[off + 2]) << 8) | static_cast<uint8_t>(data[off + 3]); if (sid == 0) { cureq.h2state().peerConnWindow += static_cast<int32_t>(increment); // Clamp instead of letting the int32_t wrap negative // (RFC 7540 §6.9.1). An overflowed window never // recovers on its own — every future send sees // min(sw, cw) <= 0 and the connection stalls // permanently until the client tears down the TCP // connection and reconnects. int64_t newWindow = static_cast<int64_t>(cureq.h2state().peerConnWindow) + increment; cureq.h2state().peerConnWindow = static_cast<int32_t>(std::min<int64_t>(newWindow, H2_MAX_WINDOW_SIZE)); } else { // Only update streams we're still tracking — ignore // WINDOW_UPDATEs for already-completed streams to // avoid creating phantom peerStreamWindows entries. auto swit = cureq.h2state().peerStreamWindows.find(sid); if (swit != cureq.h2state().peerStreamWindows.end()) swit->second += static_cast<int32_t>(increment); if (swit != cureq.h2state().peerStreamWindows.end()) { int64_t newWindow = static_cast<int64_t>(swit->second) + increment; swit->second = static_cast<int32_t>(std::min<int64_t>(newWindow, H2_MAX_WINDOW_SIZE)); } } // Resume any parked streaming responses _resumeH2Streams(cureq, out, tid, args); Loading
src/httpd.h +1 −0 Changes for src/httpd.h: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -122,6 +122,7 @@ namespace libhttppp { const int tid, ULONG_PTR args); void _resumeH2Streams(HttpRequest &cureq, std::string &out, const int tid, ULONG_PTR args); void _reapStalledH2Streams(HttpRequest &cureq, std::string &out); }; class HttpD { Loading