Skip to content

net.http: vschannel's one-shot HTTP/1.1 client hangs against keep-alive peers (no Content-Length tracking) #27705

Description

@quaesitor-scientiam

Bug

https_make_request (thirdparty/vschannel/vschannel.c), the core of vschannel's one-shot HTTP/1.1 client (vschannel_h1_do/vschannel_h1_on_open, used both when HTTP/2 is disabled and as the ALPN-negotiation-failed fallback), reads the response until the peer closes the connection — it never parses Content-Length (or chunked encoding) to know the response is already complete:

// Read data from server until done.
while(TRUE){
    if(0 == cbIoBuffer || scRet == SEC_E_INCOMPLETE_MESSAGE) {
        cbData = recv(tls_ctx->socket, pbIoBuffer + cbIoBuffer, cbIoBufferLength - cbIoBuffer, 0);
        ...
    }
    ...
}

This works fine against a server that closes the connection immediately after sending its response (or sends Connection: close and actually closes), but HTTP/1.1 keep-alive is the default — a compliant server is free to leave the connection open waiting for a pipelined next request. Against such a server, the client blocks in recv() until the peer's own idle-read timeout eventually forces it to give up and close the socket — there is no bound on the client's own side at all.

How this was found

While adding HTTP/2 connection pooling for the Windows SChannel backend (#27702), a new automated test exercised vschannel_h1_on_open (the ALPN-negotiation-failed fallback) against a realistic loopback HTTP/1.1 server that keeps the connection open after responding (ordinary keep-alive behavior). The request took ~41 seconds to complete instead of being near-instant, tracking almost exactly to the test server's own configured idle-read timeout before it gave up and closed the connection.

This had never been caught before because every existing caller of this one-shot path either talks to a real-world server that closes promptly, or is gated behind -d network (opt-in, manual). No prior automated test exercised it against a keep-alive-capable peer.

Impact

Any https:// request on native Windows (SChannel backend) that falls into the one-shot HTTP/1.1 path — either because enable_http2 is false, or because the origin doesn't select h2 via ALPN — against a server that keeps the connection open after responding will stall for as long as that server's own idle timeout takes to fire, rather than completing promptly.

Suggested fix

https_make_request's read loop needs to track response completion itself: parse the response headers as they arrive for Content-Length (or detect chunked Transfer-Encoding and parse chunk boundaries), and stop reading once the declared body is fully received, rather than relying solely on the peer closing the connection. This is a non-trivial change to the C read loop (needs incremental HTTP header parsing at the byte level) and is scoped separately from #27702, which only adds pooling on top of the existing (buggy) one-shot client.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions