Socket::write is a bare ::send(fd_, buf, len, 0) (socket.cppm:176-179), and nothing in the package sets MSG_NOSIGNAL, SO_NOSIGPIPE or a SIGPIPE disposition. A write to a socket whose peer has gone away therefore raises SIGPIPE, and a program that has not disarmed the signal, which is the default, is killed rather than told.
Where it was measured (the peer-close case of #15, same scripts): one HttpClient, the server answers a request and then closes the connection; the client keeps the socket in the pool because is_valid() only tests the descriptor. On the next request the write goes out without error, read_line at http.cppm:447 returns empty, and the "No response" path erases the pool entry at :449. That destroys the pooled TlsSocket; its destructor calls close() (tls.cppm:151), which sends a TLS close-notify through bio_send (tls.cppm:48) into Socket::write on a socket that has received the peer's RST. The process ends with exit status 141. gdb: __libc_send under Socket::write (socket.cppm:178) under bio_send (tls.cppm:48) under mbedtls_ssl_flush_output, mbedtls_ssl_write_record, mbedtls_ssl_send_alert_message, mbedtls_ssl_close_notify, under TlsSocket::close (tls.cppm:151) under ~TlsSocket (tls.cppm:70) under send_impl (http.cppm:449). With SIGPIPE ignored the same run returns statusCode=0 statusText="No response", which is what the code intends.
Why this is separate from #15: fixing the pool bookkeeping there does not remove this path. Any peer that closes an idle keep-alive connection, which servers do after a few seconds to a few minutes, leaves the client's pooled socket in the same state, and the next request through it takes the same route to :449. I have not measured the idle-timeout case itself, only the explicit close in #15; the socket state the two leave behind is the same.
What the fix could be: on Linux, ::send(fd_, buf, len, MSG_NOSIGNAL); on macOS setsockopt(fd_, SOL_SOCKET, SO_NOSIGPIPE, ...) after connecting, since MSG_NOSIGNAL is not available there; Windows has no SIGPIPE. With that in place bio_send returns MBEDTLS_ERR_NET_SEND_FAILED on the dead socket and the close-notify simply fails, which TlsSocket::close already tolerates. Ignoring SIGPIPE process-wide also works, and is what I am doing on my side, but that is a decision for the program rather than something a library that calls send should require.
Socket::writeis a bare::send(fd_, buf, len, 0)(socket.cppm:176-179), and nothing in the package setsMSG_NOSIGNAL,SO_NOSIGPIPEor aSIGPIPEdisposition. A write to a socket whose peer has gone away therefore raisesSIGPIPE, and a program that has not disarmed the signal, which is the default, is killed rather than told.Where it was measured (the peer-close case of #15, same scripts): one
HttpClient, the server answers a request and then closes the connection; the client keeps the socket in the pool becauseis_valid()only tests the descriptor. On the next request the write goes out without error,read_lineathttp.cppm:447returns empty, and the "No response" path erases the pool entry at:449. That destroys the pooledTlsSocket; its destructor callsclose()(tls.cppm:151), which sends a TLS close-notify throughbio_send(tls.cppm:48) intoSocket::writeon a socket that has received the peer's RST. The process ends with exit status 141. gdb:__libc_sendunderSocket::write(socket.cppm:178) underbio_send(tls.cppm:48) undermbedtls_ssl_flush_output,mbedtls_ssl_write_record,mbedtls_ssl_send_alert_message,mbedtls_ssl_close_notify, underTlsSocket::close(tls.cppm:151) under~TlsSocket(tls.cppm:70) undersend_impl(http.cppm:449). WithSIGPIPEignored the same run returnsstatusCode=0 statusText="No response", which is what the code intends.Why this is separate from #15: fixing the pool bookkeeping there does not remove this path. Any peer that closes an idle keep-alive connection, which servers do after a few seconds to a few minutes, leaves the client's pooled socket in the same state, and the next request through it takes the same route to
:449. I have not measured the idle-timeout case itself, only the explicit close in #15; the socket state the two leave behind is the same.What the fix could be: on Linux,
::send(fd_, buf, len, MSG_NOSIGNAL); on macOSsetsockopt(fd_, SOL_SOCKET, SO_NOSIGPIPE, ...)after connecting, sinceMSG_NOSIGNALis not available there; Windows has noSIGPIPE. With that in placebio_sendreturnsMBEDTLS_ERR_NET_SEND_FAILEDon the dead socket and the close-notify simply fails, whichTlsSocket::closealready tolerates. IgnoringSIGPIPEprocess-wide also works, and is what I am doing on my side, but that is a decision for the program rather than something a library that callssendshould require.