Collect header lines separately from the response body. A proxied transfer may deliver CONNECT, authentication, redirect and destination response headers in one operation:

$headerLines = [];
curl_setopt($handle, CURLOPT_HEADERFUNCTION,
    function ($handle, $line) use (&$headerLines) {
        $headerLines[] = $line;
        return strlen($line);
    }
);

This callback retains the raw line sequence and returns the number of bytes accepted. It receives complete lines; it does not yet group them into responses or select the final website headers.

Interpret the sequence as blocks

An HTTP status line starts a response block, and a blank line ends its headers. Preserve those boundaries before choosing the final destination response. Intermediate authentication attempts and redirects can each add blocks.

Trailers can arrive through the callback later. Keep enough parser state to distinguish them from initial headers instead of attaching every later field to the first response.

Keep or suppress the tunnel evidence

Read CURLINFO_HTTP_CONNECTCODE separately for the HTTPS tunnel's CONNECT status. An accepted tunnel does not establish a successful website response.

If CONNECT headers are unnecessary for the collector, CURLOPT_SUPPRESS_CONNECT_HEADERS controls their delivery to the configured header output and callback paths. Check support in the linked libcurl and PHP build. Suppression simplifies collection but removes header-level tunnel evidence that can help diagnosis.

Do not recover the body using a fixed offset or by scanning for an HTML tag. Header sizes vary, intermediate responses differ, and the body may be binary or empty. An old release-specific bug report does not justify a guessed offset.

Keep body handling separate, and retain the transfer error with the observed response stages if collection or parsing fails. That distinguishes an aborted callback, malformed application data and a rejected proxy exchange.

Artihkkalat mat laktásit