handle Connection: close without TLS close_notify

Some servers (e.g. ec.europa.eu) close the TCP connection without
sending a TLS close_notify alert after responding with Connection: close.
BoringSSL treats this as a fatal error, which libcurl surfaces as
CURLE_RECV_ERROR. If we already received valid HTTP headers and the
response included Connection: close, the connection closure is the
expected end-of-body signal per HTTP/1.1 — treat it as success.

You can reproduce with
```
lightpanda fetch https://ec.europa.eu/commission/presscorner/detail/en/ip_26_614
```
This commit is contained in:
Pierre Tachoire
2026-03-24 21:19:12 +01:00
parent d517488158
commit bad690da65

View File

@@ -848,14 +848,29 @@ fn processMessages(self: *Client) !bool {
}
}
// When the server sends "Connection: close" and closes the TLS
// connection without a close_notify alert, BoringSSL reports
// RecvError. If we already received valid HTTP headers, this is
// a normal end-of-body (the connection closure signals the end
// of the response per HTTP/1.1 when there is no Content-Length).
// We must check this before endTransfer, which may reset the
// easy handle.
const is_conn_close_recv = blk: {
const err = msg.err orelse break :blk false;
if (err != error.RecvError) break :blk false;
if (!transfer._header_done_called) break :blk false;
const hdr = msg.conn.getResponseHeader("connection", 0) orelse break :blk false;
break :blk std.ascii.eqlIgnoreCase(hdr.value, "close");
};
// release it ASAP so that it's available; some done_callbacks
// will load more resources.
self.endTransfer(transfer);
defer transfer.deinit();
if (msg.err) |err| {
requestFailed(transfer, err, true);
if (msg.err != null and !is_conn_close_recv) {
requestFailed(transfer, msg.err.?, true);
} else blk: {
// make sure the transfer can't be immediately aborted from a callback
// since we still need it here.