Skip to content

Fix http 1.1 streaming on client connection: close#2527

Open
Greisby wants to merge 1 commit into
drogonframework:masterfrom
Greisby:fix/streaming-close-connection
Open

Fix http 1.1 streaming on client connection: close#2527
Greisby wants to merge 1 commit into
drogonframework:masterfrom
Greisby:fix/streaming-close-connection

Conversation

@Greisby
Copy link
Copy Markdown
Contributor

@Greisby Greisby commented May 29, 2026

This is a fix for async stream responses broken when client sends Connection: close

Summary

HttpResponseImpl::makeHeaderString() skips emitting Transfer-Encoding: chunked for async stream responses when the client requests Connection: close.
Additionally, the async stream callback itself is skipped in close-connection mode.
This leaves the response body completely unframed: clients receive headers but zero body bytes.

Affected versions: drogon 1.9.13 (and all prior versions with the same logic).

Root cause (two bugs)

Bug 1 — Missing Transfer-Encoding: chunked header for async streaming responses

In lib/src/HttpResponseImpl.cc, makeHeaderString() line ~753:

else if (streamCallback_ || asyncStreamCallback_)
{
    if (!ifCloseConnection() &&
        headers_.find("content-length") == headers_.end())
    {
        headers_["transfer-encoding"] = "chunked";
    }
    len = 0;
}

The code appears to assume that when Connection: close is enabled, async streams can rely on close-delimited HTTP body framing:

  • omit Content-Length
  • omit Transfer-Encoding
  • send the body
  • terminate the message by closing the TCP connection.

That framing mode is valid both HTTP/1.0 and HTTP/1.1.
However, this does not work correctly for async streaming responses because:

  1. makeHeaderString() is executed before any body data is produced.
  2. Headers are sent immediately.
  3. The async stream callback is expected to produce body data later.
  4. When Transfer-Encoding: chunked is omitted, Drogon still enters the async streaming path, but the response no longer has any explicit body framing. Drogon’s HttpResponse async streaming implementation internally uses chunked-transfer semantics, so omitting the header makes the transport semantics inconsistent.

Bug 2 — Async stream callback skipped when Connection: close is enabled

In lib/src/HttpServer.cc, sendResponse() line ~988 and sendResponses() line ~1069:

if (asyncStreamCallback)
{
    if (!respImplPtr->ifCloseConnection())
    {
        asyncStreamCallback(
            std::make_unique<ResponseStream>(conn->sendAsyncStream(
                respImplPtr->asyncStreamKickoffDisabled())));
    }
    else
    {
        LOG_INFO << "Chunking Set CloseConnection !!!";
    }
}

When ifCloseConnection() is true, the async stream callback is never invoked.
As a consequence:

  • the application never receives the ResponseStream,
  • no body data is ever produced,
  • the response body is silently discarded,
  • and the connection is closed immediately after headers are sent.

This may have been intentionally linked to Bug 1 in order to avoid sending incorrectly framed stream data.
However, this behavior breaks async streaming responses independently of Bug 1.
Even if Transfer-Encoding: chunked were emitted correctly, the response body would still remain empty because the callback responsible for producing the stream is skipped entirely.

Observable symptoms

  • ffmpeg/libavformat: [http] Stream ends prematurely at 0, should be 18446744073709551615
    (UINT64_MAX = "unknown length" sentinel).
    0 bytes read despite 76 ms latency (headers arrived).
  • curl: Receives HTTP 200 with headers but writes a 0-byte file.
  • Drogon log: [isEAGAIN] write buffer is full after attempting to send 3 KB → client never drains the socket because
    it considers the response complete.

The bug is triggered by any HTTP/1.1 client that sends Connection: close (ffmpeg's HTTP demuxer, many embedded clients, HTTP load balancers doing single-shot requests).

Fix

  • Fix 1, in HttpResponseImpl.cc:
    Replace the !ifCloseConnection() guard with version_ != Version::kHttp10:
  • Fix 2 in HttpServer.cc:
    Replace the !ifCloseConnection() guard with the same version check (both in sendResponse() and sendResponses())

Rationale:

  • HTTP/1.1 allows Connection: close + Transfer-Encoding: chunked together (RFC 9112 §6.1 + §7.1).
    They are orthogonal: chunked frames the body, close terminates the connection after.
  • HTTP/1.0 does not support chunked encoding, so the != kHttp10 guard correctly preserves the old behavior for 1.0
    (skip async stream, log a warning).
  • HTTP/2+ has its own binary framing and does not use Transfer-Encoding; the != kHttp10 guard is future-proof for any (potential) version beyond HTTP/1.0.
  • Drogon’s async stream implementation is fundamentally based on chunked-transfer semantics.
    Therefore HTTP/1.1 async streams must continue emitting Transfer-Encoding: chunked independently of connection persistence.

Reproduction

  1. Start a drogon server with an newAsyncStreamResponse endpoint that writes data asynchronously.
  2. Request it with: curl -H "Connection: close" -o out.bin http://host:port/stream
  3. Observe: out.bin is 0 bytes. Server logs show write-buffer-full / EAGAIN.
  4. Same request without Connection: close header → works fine (chunked encoding emitted).

Compatibility

  • No behavioral change for HTTP/1.0 clients (chunked still skipped, as before).
  • No behavioral change for HTTP/1.1 keep-alive clients (chunked was already emitted).
  • Fixes HTTP/1.1 Connection: close + async stream (previously broken).
  • Connection: close still works correctly for non-stream responses (Content-Length framed).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant