From bad690da651ef48f9fb8ced5643522947a3e8804 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 24 Mar 2026 21:19:12 +0100 Subject: [PATCH] handle Connection: close without TLS close_notify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ``` --- src/browser/HttpClient.zig | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/browser/HttpClient.zig b/src/browser/HttpClient.zig index 136b578b..08fb8563 100644 --- a/src/browser/HttpClient.zig +++ b/src/browser/HttpClient.zig @@ -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.