Commit b020f81a authored by jan.koester's avatar jan.koester
Browse files

debian: bump version to 20260731+13, fix H2 offload WINDOW_UPDATE race

peerStreamWindows[sid] wasn't seeded until the offloaded render
finished, so a WINDOW_UPDATE arriving first for that stream found
nothing to credit and was silently dropped, permanently capping the
response at the default window. Seed it synchronously in
_dispatchH2Stream before handing off to the offload pool instead.

Co-Authored-By: default avatarClaude Sonnet 5 <noreply@anthropic.com>
parent 68f4a184
Loading
Loading
Loading
Loading
+23 −0
Changes for debian/changelog: 23 added lines, 0 removed lines.
Original line number Diff line number Diff line
libhttppp (20260731+13) unstable; urgency=high

  * HttpEvent: fix H2 offloaded streaming responses (e.g. mediadb previews)
    stalling permanently once the peer's initial flow-control window is
    exhausted, whenever the response was dispatched to the H2 offload pool
    (shouldOffloadH2Dispatch() == true, shipped in 20260731+12). A client's
    WINDOW_UPDATE for that stream could arrive and be processed before the
    background render finished, at which point peerStreamWindows had no
    entry yet for that stream id — the credit was silently discarded, and
    _finishH2Dispatch then unconditionally reset the window to the default
    once the render completed, permanently losing it. A well-behaved peer
    never repeats a grant it believes already landed, so any response body
    over ~64KB (the default window) hung forever with no timeout and no
    error. _dispatchH2Stream now seeds peerStreamWindows[sid] via
    try_emplace synchronously before handing the request to the offload
    pool (safe: all frame dispatch for a connection, including a later
    WINDOW_UPDATE, is serialized through the same connection event_mutex),
    and _finishH2Dispatch's two window-init sites use try_emplace instead
    of unconditional assignment so they no longer clobber credit gained in
    the meantime. New test: httpd_h2_window_credit_race_test.cpp.

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 31 Jul 2026 09:45:00 +0200

libhttppp (20260731+12) unstable; urgency=high

  * HttpEvent: fix H2 streaming responses (large images/video) stalling
+14 −1
Changes for test/httpd_h2_window_credit_race_test.cpp: 14 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// and having _finishH2Dispatch() only fill in the default if no entry
// exists yet, instead of unconditionally overwriting it.

#include <cerrno>
#include <chrono>
#include <cstring>
#include <iostream>
@@ -184,7 +185,12 @@ size_t runRaceRequest(int port) {
    // well beyond the default 65535-byte window, before any response byte
    // has arrived. A correct server credits this to stream 1's window
    // whenever it gets processed, even if that's before the offloaded
    // render has produced a response at all.
    // render has produced a response at all. Real clients (e.g. curl's
    // nghttp2 backend) also proactively boost the connection-level window
    // (stream 0) the same way — peerConnWindow isn't a lazily-created map
    // entry, so it was never subject to the race being tested here, but a
    // real client sends both, so the test does too.
    appendWindowUpdate(req, 0, 1'000'000);
    appendWindowUpdate(req, 1, 1'000'000);

    size_t sent = 0;
@@ -194,6 +200,9 @@ size_t runRaceRequest(int port) {
        sent += static_cast<size_t>(n);
    }

    timeval rcvTimeout{2, 0};
    ::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &rcvTimeout, sizeof(rcvTimeout));

    size_t dataBytes = 0;
    bool sawEndStream = false;
    std::vector<char> buf;
@@ -201,6 +210,7 @@ size_t runRaceRequest(int port) {
    auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
    while (!sawEndStream && std::chrono::steady_clock::now() < deadline) {
        ssize_t n = ::recv(fd, tmp, sizeof(tmp), 0);
        if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) continue;
        if (n <= 0) break;
        buf.insert(buf.end(), tmp, tmp + n);

@@ -217,6 +227,9 @@ size_t runRaceRequest(int port) {
                           static_cast<uint8_t>(buf[off + 8]);
            if (off + FRAME_HEADER_LEN + len > buf.size())
                break; // incomplete frame, wait for more bytes
            std::cerr << "[test] frame type=" << static_cast<int>(type)
                      << " sid=" << sid << " len=" << len
                      << " flags=" << static_cast<int>(flags) << std::endl;
            if (type == FRAME_DATA && sid == 1) {
                dataBytes += len;
                if (flags & FLAG_END_STREAM) sawEndStream = true;