A socket closed while the peer still has unread bytes in its receive buffer is
reset rather than finished, and the reset discards what this side has written
but the peer has not read. #288 answered that for plaintext HTTP/1 with a
lingering close; #305 armed the same drain from the WebSocket inbound-cap
teardown. Neither reaches a TLS connection.
The drain is fed from the plaintext read paths alone —
http_connection_linger_note_inbound is reached from http_connection.c:1324
and :1429, and http_connection_tls.c does not mention the drain at all. A
TLS connection that armed it would wait with nothing being read and close on the
same unread bytes at the end, so http_connection_linger_begin refuses one
outright rather than spending the deadline for nothing.
That refusal is the honest state, not the fix. Over TLS:
Both are the disclosure of nothing rather than of something — the client learns
no reason for the connection ending, which is exactly what the lingering close
exists to prevent.
What closing it takes
The TLS read FSM needs the drop-and-refresh hook the plaintext paths have: while
conn->linger_close is set, ciphertext that arrives is decrypted and thrown
away rather than buffered, and each chunk pushes deadline_ms out by one idle
window. Then linger_begin can stop refusing.
Read tls_advance_state in src/core/http_connection_tls.c alongside the
plaintext sites above: the one-shot TLS read is not re-armed once
tls_finalize_if_closing returns true, so the drain needs a re-arm there as
well as the drop.
Feedback loop
websocket/035-recv-queue-overflow proves the plaintext half today. The TLS half
has no test: a wss copy of 035 — same flood, same cap, listener with
enableTls(true) — is the loop, and it should read client saw close: NULL
before the fix. Run it outside run-tests or without usleep(/microtime( in
its FILE section, or the retry will report only the second attempt (#TBD).
Found by the review of #305.
A socket closed while the peer still has unread bytes in its receive buffer is
reset rather than finished, and the reset discards what this side has written
but the peer has not read. #288 answered that for plaintext HTTP/1 with a
lingering close; #305 armed the same drain from the WebSocket inbound-cap
teardown. Neither reaches a TLS connection.
The drain is fed from the plaintext read paths alone —
http_connection_linger_note_inboundis reached fromhttp_connection.c:1324and
:1429, andhttp_connection_tls.cdoes not mention the drain at all. ATLS connection that armed it would wait with nothing being read and close on the
same unread bytes at the end, so
http_connection_linger_beginrefuses oneoutright rather than spending the deadline for nothing.
That refusal is the honest state, not the fix. Over TLS:
case A 413 refusal never reaches a client that is still uploading #287 described;
CLOSE 1013, the case WebSocket: the 1013 close is lost when the overflowing peer is still sending #305 described.
Both are the disclosure of nothing rather than of something — the client learns
no reason for the connection ending, which is exactly what the lingering close
exists to prevent.
What closing it takes
The TLS read FSM needs the drop-and-refresh hook the plaintext paths have: while
conn->linger_closeis set, ciphertext that arrives is decrypted and thrownaway rather than buffered, and each chunk pushes
deadline_msout by one idlewindow. Then
linger_begincan stop refusing.Read
tls_advance_stateinsrc/core/http_connection_tls.calongside theplaintext sites above: the one-shot TLS read is not re-armed once
tls_finalize_if_closingreturns true, so the drain needs a re-arm there aswell as the drop.
Feedback loop
websocket/035-recv-queue-overflowproves the plaintext half today. The TLS halfhas no test: a
wsscopy of035— same flood, same cap, listener withenableTls(true)— is the loop, and it should readclient saw close: NULLbefore the fix. Run it outside
run-testsor withoutusleep(/microtime(inits FILE section, or the retry will report only the second attempt (#TBD).
Found by the review of #305.