Skip to content

Conversation

@Alkarex
Copy link
Contributor

@Alkarex Alkarex commented Sep 20, 2025

fix #942

Some feeds are not parsed correctly due to wrong handling of HTTP trailer headers such as Server-Timing.

Example of feed:
https://www.caranddriver.com/rss/all.xml
(Note that the bug may or may not exhibit itself depending on the cURL and PHP versions)

Downstream discussion:

SimplePie tries to XML parse something like </rss>server-timing: rtt; dur=6.131, retrans; dur=0, trailer-timestamp; dur=1758222994925 which obviously fails.

In the case of normal HTTP transfer (not chunked), we need to use content-length to know where the body stops, but content-length is wrong if any compression was used.
So I believe we should let cURL perform the separation of HTTP headers and body instead of using the SimplePie parser.

fix simplepie#942

Some feeds are not parsed correctly due to wrong handling of [HTTP trailer headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Trailer) such as [Server-Timing](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing).

Example of feed:
https://www.caranddriver.com/rss/all.xml
(Note that the bug may or may not exhibit itself depending on the cURL and PHP versions)

Downstream discussion:
* FreshRSS/FreshRSS#7981
* FreshRSS/FreshRSS#7983

SimplePie tries to XML parse something like `</rss>server-timing: rtt; dur=6.131, retrans; dur=0, trailer-timestamp; dur=1758222994925` which obviously fails.

In the case of normal HTTP transfer (not chunked), we need to use content-length to know where the body stops, but content-length is wrong if any compression was used.
So I believe we should let cURL perform the separation of HTTP headers and body instead of using the SimplePie parser.
@jtojnar
Copy link
Contributor

jtojnar commented Sep 21, 2025

So I believe we should let cURL perform the separation of HTTP headers and body instead of using the SimplePie parser.

That sounds reasonable. cURL is good HTTP client, we should not replicate its functionality ourselves.

Following the breadcrumbs unfortunately does not tell us much about the rationale for introducing it:

  • 4b1fc33 introduced Parser::prepareHeaders
  • e5b93e1 extracted parsing to SimplePie_HTTP_Parser
  • Initially introduced in 5bf1814, too large to look into the reasoning.

In the case of normal HTTP transfer (not chunked), we need to use content-length to know where the body stops

HTTP 1

In non-chunked HTTP response (version 1), there are no trailers, stuff after Content-Length should be either ignored, or if re-using the connection for successive requests, be parsed as part of the next message.

In chunked response, Content-Length appears to be not used. In fact one of the reasons to use chunked encoding is that the length of the whole content is not yet available because the content is streamed in chunks.

Inspecting the Reassembled TLS in Wireshark produced by env SSLKEYLOGFILE=/tmp/sslkey.log curl --http1.1 https://www.caranddriver.com/rss/all.xml, they indeed not send Content-Length with HTTP 1, and trailers are inserted into last chunk (declared with 0 length). Actually, it seems that the trailer headers are only terminated by the second CRLF of the 0 chunk there is no explicitly specified length that I can see.

HTTP 1.1 chunked response with trailer

CRLF denoted with C-style escape sequences, with actual new lines inserted after each CRLF for readability.

HTTP/1.1 200 OK\r\n
Connection: keep-alive\r\n
Content-Type: text/xml; charset=utf-8\r\n
x-robots-tag: all\r\n
Accept-Ranges: bytes\r\n
Age: 24\r\n
Date: Sun, 21 Sep 2025 09:35:43 GMT\r\n
Set-Cookie: _pxhd=7870bf6de03134b9ca13753fe377f14fe48fec3ce80d764681c3e9045efe3267:57ec8275-96ce-11f0-b3c9-2072c9786893; Max-Age=31536000; path=/; SameSite=Lax\r\n
X-Cache: HIT, MISS\r\n
Vary: Accept-Encoding\r\n
Server-Timing: time-start-msec;dur=1758447343064,time-elapsed;dur=180,fastly-pop;desc=VIE,hit-state;desc=MISS-CLUSTER\r\n
set-cookie: location_data={"country_code":"CZ","postal_code":"436 01"}; path=/;\r\n
Link: <https://googletagservices.com>; rel=preconnect, <https://api.backfires.caranddriver.com>; rel=preconnect, <https://securepubads.g.doubleclick.net>; rel=preconnect, <https://cdn.optimizely.com>; rel=preconnect, <https://adservice.google.com>; rel=preconnect, <https://connect.facebook.net>; rel=preconnect, <https://logx.optimizely.com>; rel=preconnect, <https://www.facebook.com>; rel=preconnect, <https://h.nexac.com>; rel=preconnect, <https://gtrk.s3.amazonaws.com>; rel=preconnect, <https://graph.facebook.com>; rel=preconnect, <https://hips.hearstapps.com>; rel=preconnect, <https://nexus.ensighten.com>; rel=preconnect, <https://www.google-analytics.com>; rel=preconnect, <https://www.googletagmanager.com>; rel=preconnect, <https://stats.g.doubleclick.net>; rel=preconnect, <https://www.google.com>; rel=preconnect, <https://tpc.googlesyndication.com>; rel=preconnect, <https://fonts.googleapis.com>; rel=preconnect, <https://img.vast.com>; rel=preconnect, <https://z.moatads.com>; rel=preconnect\r\n
strict-transport-security: max-age=31557600; includeSubDomains\r\n
x-country: CZ\r\n
Cache-Control: max-age=0, must-revalidate, private\r\n
alt-svc: h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400\r\n
transfer-encoding: chunked\r\n
trailer: server-timing\r\n
\r\n
32c6\r\n
<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">\n…\n</rss>\r\n
0\r\n
server-timing: rtt; dur=19.315, retrans; dur=0, trailer-timestamp; dur=1758447343245\r\n
\r\n

For completeness, here is also gzipped version captured from SSLKEYLOGFILE=/tmp/sslkey.log curl --compressed --http1.1 https://www.caranddriver.com/rss/all.xml:

The same request but compressed
HTTP/1.1 200 OK\r\n
Connection: keep-alive\r\n
Content-Type: text/xml; charset=utf-8\r\n
Content-Encoding: gzip\r\n
x-robots-tag: all\r\n
Accept-Ranges: bytes\r\n
Age: 32\r\n
Date: Sun, 21 Sep 2025 10:18:34 GMT\r\n
Set-Cookie: _pxhd=820a4553273c294ccaed386f4dce2a532a4f730eaea4f9f465b2783cdcca392b:544d3522-96d4-11f0-a86e-a41b8f8b85a3; Max-Age=31536000; path=/; SameSite=Lax\r\n
X-Cache: HIT, MISS\r\n
Vary: Accept-Encoding\r\n
Server-Timing: time-start-msec;dur=1758449913956,time-elapsed;dur=181,fastly-pop;desc=VIE,hit-state;desc=MISS-CLUSTER\r\n
set-cookie: location_data={"country_code":"CZ","postal_code":"436 01"}; path=/;\r\n
Link: <https://googletagservices.com>; rel=preconnect, <https://api.backfires.caranddriver.com>; rel=preconnect, <https://securepubads.g.doubleclick.net>; rel=preconnect, <https://cdn.optimizely.com>; rel=preconnect, <https://adservice.google.com>; rel=preconnect, <https://connect.facebook.net>; rel=preconnect, <https://logx.optimizely.com>; rel=preconnect, <https://www.facebook.com>; rel=preconnect, <https://h.nexac.com>; rel=preconnect, <https://gtrk.s3.amazonaws.com>; rel=preconnect, <https://graph.facebook.com>; rel=preconnect, <https://hips.hearstapps.com>; rel=preconnect, <https://nexus.ensighten.com>; rel=preconnect, <https://www.google-analytics.com>; rel=preconnect, <https://www.googletagmanager.com>; rel=preconnect, <https://stats.g.doubleclick.net>; rel=preconnect, <https://www.google.com>; rel=preconnect, <https://tpc.googlesyndication.com>; rel=preconnect, <https://fonts.googleapis.com>; rel=preconnect, <https://img.vast.com>; rel=preconnect, <https://z.moatads.com>; rel=preconnect\r\n
strict-transport-security: max-age=31557600; includeSubDomains\r\n
x-country: CZ\r\n
Cache-Control: max-age=0, must-revalidate, private\r\n
alt-svc: h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400\r\n
transfer-encoding: chunked\r\n
trailer: server-timing\r\n
\r\n
1eb3\r\n
[binary gzipped data]\r\n
0\r\n
server-timing: rtt; dur=24.233, retrans; dur=0, trailer-timestamp; dur=1758449914137\r\n
\r\n

HTTP 2

HTTP 2 replaces chunked encoding with custom framing so the trailers are explicitly separated into HEADERS frame.

000096DC  61 6e 6e 65 6c 3e 0a 3c  2f 72 73 73 3e 00 00 35   annel>.< /rss>..5
000096EC  01 05 00 00 00 01 7f 06  b2 b1 29 fb 52 4b 6c 80   ........ ..).RKl.
000096FC  2e 2e 20 b3 f4 a5 85 4d  83 a9 1f 6a 49 6d 90 03   .. ....M ...jIm..
0000970C  e9 44 d8 33 50 5b 16 49  a9 2a 12 3a 6b fb 52 4b   .D.3P[.I .*.:k.RK
0000971C  6c 80 2e b6 f3 4d 80 42  69 96 81                  l....M.B i..

Here is Wireshark’s breakdown of the HEADERS frame (starting with 00 00 35 on the first line above

HyperText Transfer Protocol 2
    Stream: HEADERS, Stream ID: 1, Length 53
        Length: 53
        Type: HEADERS (1)
        Flags: 0x05, End Headers, End Stream
        0... .... .... .... .... .... .... .... = Reserved: 0x0
        .000 0000 0000 0000 0000 0000 0000 0001 = Stream Identifier: 1
        [3 Body fragments (37289 bytes): #52(14574), #67(16384), #72(6331)]
        Data […]: 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d38223f3e0a3c7273732076657273696f6e3d22322e302220786d6c6e733a61746f6d3d22687474703a2f2f7777772e77332e6f72672f323030352f41746f6d2220786d6c6e733a6d656469613d22687
        [Pad Length: 0]
        Header Block Fragment: 7f06b2b129fb524b6c802e2e20b3f4a5854d83a91f6a496d9003e944d833505b1649a92a123a6bfb524b6c802eb6f34d8042699681
        [Header Length: 90]
        [Header Count: 1]
        Header: server-timing: rtt; dur=16.213, retrans; dur=0, trailer-timestamp; dur=1758450224340
            Name Length: 13
            Name: server-timing
            Value Length: 69
            Value: rtt; dur=16.213, retrans; dur=0, trailer-timestamp; dur=1758450224340
            [Unescaped: rtt; dur=16.213, retrans; dur=0, trailer-timestamp; dur=1758450224340]
            Representation: Literal Header Field with Incremental Indexing - Indexed Name
            Index: 69

@Alkarex
Copy link
Contributor Author

Alkarex commented Sep 21, 2025

My guess is that the parser was mainly needed for the fsockopen() mode.

curl_setopt($fp, CURLOPT_USERAGENT, $useragent);
curl_setopt($fp, CURLOPT_HTTPHEADER, $headers2);
$responseHeaders = '';
curl_setopt($fp, CURLOPT_HEADERFUNCTION, function ($ch, string $header) use (&$responseHeaders) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears to have been available in PHP 4 already so it should be fine: https://www.php.net/ChangeLog-4.php#4.4.5. Very nice improvement 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to break HTTP proxy tunnel.

Running tinyproxy -d -c tinyproxy.conf with the following config:

Port 8888
Listen 127.0.0.1
Timeout 600
Allow 127.0.0.1

The test below fails:

--- a/tests/Integration/HTTP/ClientsTest.php
+++ b/tests/Integration/HTTP/ClientsTest.php
@@ -290,4 +290,33 @@ class ClientsTest extends TestCase
             'Body does not end as expected: ' . $response->get_body_content()
         );
     }
+
+    /**
+     * Check that proxy client handling works
+     */
+    public function testProxyTunnel(): void
+    {
+        $client = new FileClient(new Registry(), [
+            'curl_options' => [
+                \CURLOPT_HTTPPROXYTUNNEL => true,
+                \CURLOPT_PROXY => '127.0.0.1',
+                \CURLOPT_PROXYPORT => 8888,
+            ]
+        ]);
+        $server = new MockWebServer();
+        $server->start();
+
+
+        $url = $server->setResponseOfPath(
+            '/hello',
+            new MockWebServerResponse('Hello World!', [], 200)
+        );
+
+        $response = $client->request(Client::METHOD_GET, $url);
+
+        $server->stop();
+
+        self::assertSame(200, $response->get_status_code());
+        self::assertSame('Hello World!', $response->get_body_content());
+    }
 }
1) SimplePie\Tests\Integration\HTTP\ClientsTest::testProxyTunnel
SimplePie\HTTP\ClientException: assert($this->body !== null)

/home/jtojnar/Projects/simplepie/src/HTTP/FileClient.php:68
/home/jtojnar/Projects/simplepie/tests/Integration/HTTP/ClientsTest.php:315

Caused by
AssertionError: assert($this->body !== null)

/home/jtojnar/Projects/simplepie/src/File.php:312
/home/jtojnar/Projects/simplepie/src/Registry.php:203
/home/jtojnar/Projects/simplepie/src/HTTP/FileClient.php:59
/home/jtojnar/Projects/simplepie/tests/Integration/HTTP/ClientsTest.php:315

src/File.php Outdated
curl_setopt($fp, CURLOPT_HTTPHEADER, $headers2);
$responseHeaders = '';
curl_setopt($fp, CURLOPT_HEADERFUNCTION, function ($ch, string $header) use (&$responseHeaders) {
if (trim($header) !== '') { // Skip e.g. separation with trailer headers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quoting https://www.rfc-editor.org/rfc/rfc9110#section-6.5.2-3 (emphasis mine):

At the end of a message, a recipient MAY treat the set of received trailer fields as a data structure of name/value pairs, similar to (but separate from) the header fields.

I guess we should just stop after the first line consisting of only CRLF, as SimplePie\HTTP\Parser does not support trailers.

Though I guess this might be problematic with tunnelling too. curl_exec return value will include the contents of HTTP tunnel so Parser::prepareHeaders expects something like this (new lines added for clarity):

HTTP/1.1 200 Connection established\r\n
Proxy-agent: tinyproxy/1.11.2\r\n
\r\n
HTTP/1.1 404 Not Found\r\n
Host: 127.0.0.1:56009\r\n
Date: Sun, 21 Sep 2025 15:00:53 GMT\r\n
Connection: close\r\n
X-Powered-By: PHP/8.0.30\r\n
Content-type: text/html; charset=UTF-8\r\n
\r\n
Hello World!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From memory, cURL returning or not the proxy headers depend on picking the correct type of proxy settings such as CURLOPT_HTTPPROXYTUNNEL.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They will be included whenever using tunnelling (unless disabled with CURLOPT_SUPPRESS_CONNECT_HEADERS (but that is only available in PHP 7.3)).

But Parser::prepareHeaders will strip it just fine, as long as the empty CRLF line is preserved.

So I would suggest using CURLOPT_WRITEFUNCTION to get the body rather than getting it from curl_exec, and ignoring any CURLOPT_HEADERFUNCTION calls after CURLOPT_WRITEFUNCTION was called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about using only CURLOPT_HEADERFUNCTION but keeping the blanks?
906fa67

I am worried CURLOPT_WRITEFUNCTION may decrease performance and increase memory use

$parser = new \SimplePie\HTTP\Parser($responseHeaders, true);
if ($parser->parse()) {
$this->set_headers($parser->headers);
$this->body = $parser->body;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested Transfer-encoding: chunked and looks like curl itself handles this correctly:

Test server
<?php

declare(strict_types=1);

$socket = stream_socket_server("tcp://0.0.0.0:8888", $errno, $errstr);

echo "connected\n";

if (!$socket) {
    echo "Failed to open socket: $errstr ($errno)<br />\n";
} else {
    while ($conn = stream_socket_accept($socket)) {
        echo "accepted\n";

        // echo "simulating response to CONNECT\n";
        // fwrite(
        //     $conn,
        //     // "HTTP/1.1 100 Continue\r\n"
        //     // . "\r\n"
        //     // .
        //     "HTTP/1.1 200 Connection established\r\n"
        //     . "Proxy-agent: tinyproxy/1.11.2\r\n"
        //     . "\r\n"
        // );

        sleep(1);

        fwrite(
            $conn,
            "HTTP/1.1 100 Continue\r\n"
            . "\r\n"
            . "HTTP/1.1 100 Continue\r\n"
            . "\r\n"
            . "HTTP/1.1 200 OK\r\n"
            . "Host: 127.0.0.1:56009\r\n"
            . "Date: Sun, 21 Sep 2025 15:00:53 GMT\r\n"
            . "Connection: close\r\n"
            . "X-Powered-By: PHP/8.0.30\r\n"
            . "Content-type: text/html; charset=UTF-8\r\n"
            . "Transfer-encoding: chunked\r\n"
            // . "Content-length: 12\r\n"
            . "\r\n"
            // . "Hello World!"
            . "5\r\n"
            . "Hello"
            . "\r\n6\r\n"
            . "World!"
            . "\r\n0\r\n\r\n"
        );

        sleep(1);

        fclose($conn);
    }

    fclose($socket);
}

src/File.php Outdated
if ($parser->parse()) {
$this->set_headers($parser->headers);
$this->body = $parser->body;
$this->status_code = $parser->status_code;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, this removal in fact fixes overwrite the final response code returned by CURLINFO_HTTP_CODE with the interim one (e.g. 100) when server returns an interim response. Extracted this fix into #945 with other related fixes.

This was referenced Sep 30, 2025
Alkarex added a commit to FreshRSS/simplepie that referenced this pull request Sep 30, 2025
alexlebens pushed a commit to alexlebens/infrastructure that referenced this pull request Dec 26, 2025
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [freshrss/freshrss](https://freshrss.org/) ([source](https://github.com/FreshRSS/FreshRSS)) | minor | `1.27.1` -> `1.28.0` |

---

### Release Notes

<details>
<summary>FreshRSS/FreshRSS (freshrss/freshrss)</summary>

### [`v1.28.0`](https://github.com/FreshRSS/FreshRSS/blob/HEAD/CHANGELOG.md#2025-12-24-FreshRSS-1280)

[Compare Source](FreshRSS/FreshRSS@1.27.1...1.28.0)

- Features
  - New sorting and filtering by date of *User modified* [#&#8203;7886](FreshRSS/FreshRSS#7886), [#&#8203;8090](FreshRSS/FreshRSS#8090),
    [#&#8203;8105](FreshRSS/FreshRSS#8105), [#&#8203;8118](FreshRSS/FreshRSS#8118), [#&#8203;8130](FreshRSS/FreshRSS#8130)
    - Corresponding search operator, e.g. `userdate:PT1H` for the past hour [#&#8203;8093](FreshRSS/FreshRSS#8093)
    - Allows finding articles marked by the local user as read/unread or starred/unstarred at specific dates for e.g. undo action.
  - New sorting by article length [#&#8203;8119](FreshRSS/FreshRSS#8119)
  - New advanced search form [#&#8203;8103](FreshRSS/FreshRSS#8103), [#&#8203;8122](FreshRSS/FreshRSS#8122), [#&#8203;8226](FreshRSS/FreshRSS#8226)
  - Add compatibility with PCRE word boundary `\b` and `\B` for regex search using PostgreSQL [#&#8203;8141](FreshRSS/FreshRSS#8141)
  - More uniform SQL search and PHP search for accents and case-sensitivity (e.g. for automatically marking as read) [#&#8203;8329](FreshRSS/FreshRSS#8329)
  - New overview of dates with most unread articles [#&#8203;8089](FreshRSS/FreshRSS#8089)
  - Allow marking as read articles older than 1 or 7 days also when sorting by publication date [#&#8203;8163](FreshRSS/FreshRSS#8163)
  - New option to show user labels instead of tags in RSS share [#&#8203;8112](FreshRSS/FreshRSS#8112)
  - Add new feed visibility (priority) *Show in its feed* [#&#8203;7972](FreshRSS/FreshRSS#7972)
  - New ability to share feed visibility through API (implemented by e.g. Capy Reader) [#&#8203;7583](FreshRSS/FreshRSS#7583), [#&#8203;8158](FreshRSS/FreshRSS#8158)
  - Configurable notification timeout [#&#8203;7942](FreshRSS/FreshRSS#7942)
  - OPML export/import of unicity criteria [#&#8203;8243](FreshRSS/FreshRSS#8243)
  - Ensure stable IDs (categories, feeds, labels) during export/import [#&#8203;7988](FreshRSS/FreshRSS#7988)
  - Add username and timestamp to SQLite export from Web UI [#&#8203;8169](FreshRSS/FreshRSS#8169)
  - Add option to apply filter actions to existing articles [#&#8203;7959](FreshRSS/FreshRSS#7959), [#&#8203;8259](FreshRSS/FreshRSS#8259)
  - Support CSS selector `~` *subsequent-sibling* [#&#8203;8154](FreshRSS/FreshRSS#8154)
    - Upstream PR [phpgt/CssXPath#231](phpgt/CssXPath#231)
  - Rework saving of configuration files for more reliability in case of e.g. full disk [#&#8203;8220](FreshRSS/FreshRSS#8220)
  - Web scraping support date format as milliseconds for Unix epoch [#&#8203;8266](FreshRSS/FreshRSS#8266)
  - Allow negative category sort numbers [#&#8203;8330](FreshRSS/FreshRSS#8330)
- Performance
  - Improve SQL speed for updating cached information [#&#8203;6957](FreshRSS/FreshRSS#6957), [#&#8203;8207](FreshRSS/FreshRSS#8207),
    [#&#8203;8255](FreshRSS/FreshRSS#8255), [#&#8203;8254](FreshRSS/FreshRSS#8254), [#&#8203;8255](FreshRSS/FreshRSS#8255)
  - Fix SQL performance issue with MySQL, using an index hint [#&#8203;8211](FreshRSS/FreshRSS#8211)
  - Scaling of user statistics in Web UI and CLI, to help instances with 1k+ users [#&#8203;8277](FreshRSS/FreshRSS#8277)
  - API streaming of large responses for reducing memory consumption and increasing speed [#&#8203;8041](FreshRSS/FreshRSS#8041)
- Security
  - 💥 Move unsafe autologin to an extension [#&#8203;7958](FreshRSS/FreshRSS#7958)
  - Fix some CSRFs [#&#8203;8035](FreshRSS/FreshRSS#8035)
  - Strengthen some crypto (login, tokens, nonces) [#&#8203;8061](FreshRSS/FreshRSS#8061), [#&#8203;8320](FreshRSS/FreshRSS#8320)
  - Create separate HTTP `Retry-After` rules for proxies [#&#8203;8029](FreshRSS/FreshRSS#8029), [#&#8203;8218](FreshRSS/FreshRSS#8218)
  - Add `data:` to CSP in subscription controller [#&#8203;8253](FreshRSS/FreshRSS#8253)
  - Improve anonymous authentication logic [#&#8203;8165](FreshRSS/FreshRSS#8165)
  - Enable GitHub [release immutability](https://github.blog/changelog/2025-10-28-immutable-releases-are-now-generally-available/) [#&#8203;8205](FreshRSS/FreshRSS#8205)
- Bug fixing
  - Exclude local networks for domain-wide HTTP `Retry-After` [#&#8203;8195](FreshRSS/FreshRSS#8195)
  - Fix OpenID Connect with Debian 13 [#&#8203;8032](FreshRSS/FreshRSS#8032)
  - Fix MySQL / MariaDB bug wrongly sorting new articles [#&#8203;8223](FreshRSS/FreshRSS#8223)
  - Fix MySQL / MariaDB database size calculation [#&#8203;8282](FreshRSS/FreshRSS#8282)
  - Fix SQLite bind bug when adding user label [#&#8203;8101](FreshRSS/FreshRSS#8101)
  - Fix SQL auto-update of field `f.kind` to ease migrations from FreshRSS versions older than 1.20.0 [#&#8203;8148](FreshRSS/FreshRSS#8148)
  - Fix search encoding and quoting [#&#8203;8311](FreshRSS/FreshRSS#8311), [#&#8203;8324](FreshRSS/FreshRSS#8324), [#&#8203;8338](FreshRSS/FreshRSS#8338)
  - Fix handling of database unexpected null content (during migrations) [#&#8203;8319](FreshRSS/FreshRSS#8319), [#&#8203;8321](FreshRSS/FreshRSS#8321)
  - Fix drag & drop of user query losing information [#&#8203;8113](FreshRSS/FreshRSS#8113)
  - Fix DOM error while filtering retrieved full content [#&#8203;8132](FreshRSS/FreshRSS#8132), [#&#8203;8161](FreshRSS/FreshRSS#8161)
  - Fix `config.custom.php` during install [#&#8203;8033](FreshRSS/FreshRSS#8033)
  - Fix do not mark important feeds as read from category [#&#8203;8067](FreshRSS/FreshRSS#8067)
  - Fix regression of warnings in Web browser console due to lack of `window.bcrypt` object [#&#8203;8166](FreshRSS/FreshRSS#8166)
  - Fix chart resize regression due to `chart.js` v4 update [#&#8203;8298](FreshRSS/FreshRSS#8298)
  - Fix CLI user creation warning when language is not given [#&#8203;8283](FreshRSS/FreshRSS#8283)
  - Fix merging of custom HTTP headers [#&#8203;8251](FreshRSS/FreshRSS#8251)
  - Fix bug in the case of duplicated mark-as-read filters [#&#8203;8322](FreshRSS/FreshRSS#8322)
- SimplePie
  - Fix support of HTTP trailer headers [#&#8203;7983](FreshRSS/FreshRSS#7983), [simplepie#943](simplepie/simplepie#943)
  - Apply HTTPS policy also on GUIDs and permalinks [#&#8203;8037](FreshRSS/FreshRSS#8037), [simplepie#951](simplepie/simplepie#951)
    - Fix `WordPress.com` HTTP duplicates with WebSub [Automattic/pushpress#16](Automattic/pushpress#16)
  - Implement HTML whitelist for SimplePie sanitizer [#&#8203;7924](FreshRSS/FreshRSS#7924), [simplepie#947](simplepie/simplepie#947)
  - Various upstream contributions [simplepie#940](simplepie/simplepie#940), [simplepie#944](simplepie/simplepie#944)
- Deployment
  - Docker default image updated to Debian 13 Trixie with PHP 8.4.11 and Apache 2.4.65 [#&#8203;8032](FreshRSS/FreshRSS#8032)
  - Docker alternative image updated to Alpine 3.23 with PHP 8.4.15 and Apache 2.4.65 [#&#8203;8285](FreshRSS/FreshRSS#8285)
  - Fix Docker healthcheck `cli/health.php` compatibility with OpenID Connect [#&#8203;8040](FreshRSS/FreshRSS#8040)
  - Improve Docker for compatibility with other base images such as Arch Linux [#&#8203;8299](FreshRSS/FreshRSS#8299)
    - Improve `cli/access-permissions.sh` to detect the correct permission Web group such as `www-data`, `apache`, or `http`
  - Update PostgreSQL volume for Docker [#&#8203;8216](FreshRSS/FreshRSS#8216), [#&#8203;8224](FreshRSS/FreshRSS#8224)
  - Catch lack of `exec()` function for git update [#&#8203;8228](FreshRSS/FreshRSS#8228)
  - Work around `DOMDocument::saveHTML()` scrambling charset encoding in some versions of libxml2 [#&#8203;8296](FreshRSS/FreshRSS#8296)
  - Improve configuration checks for PHP extensions (in Web UI and CLI), including recommending e.g. `php-intl` [#&#8203;8334](FreshRSS/FreshRSS#8334)
- UI
  - New button for toggling sidebar on desktop view [#&#8203;8201](FreshRSS/FreshRSS#8201), [#&#8203;8286](FreshRSS/FreshRSS#8286)
  - Better transitions between groups of articles [#&#8203;8174](FreshRSS/FreshRSS#8174)
  - New links in transitions and jump ⏭ to next transition [#&#8203;8294](FreshRSS/FreshRSS#8294)
  - More visible selected article [#&#8203;8230](FreshRSS/FreshRSS#8230)
  - Show the parsed search query instead of the original user input [#&#8203;8293](FreshRSS/FreshRSS#8293),
    [#&#8203;8306](FreshRSS/FreshRSS#8306), [#&#8203;8341](FreshRSS/FreshRSS#8341)
  - Show search query in the page title [#&#8203;8217](FreshRSS/FreshRSS#8217)
  - Scroll into filtered feed/category on page load in the sidebar [#&#8203;8281](FreshRSS/FreshRSS#8281), [#&#8203;8307](FreshRSS/FreshRSS#8307)
  - Fix autocomplete issues in change password form [#&#8203;7812](FreshRSS/FreshRSS#7812)
  - Fix navigating between read feeds using shortcut <kbd>shift</kbd>+<kbd>j</kbd>/<kbd>k</kbd> [#&#8203;8057](FreshRSS/FreshRSS#8057)
  - Dark background in Web app manifest to avoid white flash when opening [#&#8203;8140](FreshRSS/FreshRSS#8140)
  - Increase button visibility in UI to change theme [#&#8203;8149](FreshRSS/FreshRSS#8149)
  - Replace arrow navigation in theme switcher with `<select>` [#&#8203;8190](FreshRSS/FreshRSS#8190)
  - Improve scroll of article after load of user labels [#&#8203;7962](FreshRSS/FreshRSS#7962)
  - Keep scroll state of page when closing the slider [#&#8203;8295](FreshRSS/FreshRSS#8295), [#&#8203;8301](FreshRSS/FreshRSS#8301)
  - Scroll into filtered feed/category on page load [#&#8203;8281](FreshRSS/FreshRSS#8281)
  - Display sidebar dropdowns above if no space below [#&#8203;8335](FreshRSS/FreshRSS#8335), [#&#8203;8336](FreshRSS/FreshRSS#8336)
  - Use native CSS instead of SCSS [#&#8203;8200](FreshRSS/FreshRSS#8200), [#&#8203;8241](FreshRSS/FreshRSS#8241)
    - Using [CSS nesting](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Nesting) and [relative colours](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Colors/Using_relative_colors).
  - Various UI and style improvements: [#&#8203;8171](FreshRSS/FreshRSS#8171), [#&#8203;8185](FreshRSS/FreshRSS#8185), [#&#8203;8196](FreshRSS/FreshRSS#8196)
  - JavaScript finalise migration from `Promise` to `async`/`await`: [#&#8203;8182](FreshRSS/FreshRSS#8182)
- API
  - API performance optimisation: streaming of large responses [#&#8203;8041](FreshRSS/FreshRSS#8041)
  - Fever API: Add `with_ids` parameter to mass-change read/unread/saved/unsaved on lists of articles [#&#8203;8312](FreshRSS/FreshRSS#8312)
  - Misc API: better REST error semantics [#&#8203;8232](FreshRSS/FreshRSS#8232)
- Extensions
  - Add support for extension priority [#&#8203;8038](FreshRSS/FreshRSS#8038)
  - Add support for extension compatibility [#&#8203;8081](FreshRSS/FreshRSS#8081)
  - Improve PHP code with hook enums [#&#8203;8036](FreshRSS/FreshRSS#8036)
  - New hook `nav_entries` [#&#8203;8054](FreshRSS/FreshRSS#8054)
  - Rename [Extensions](https://github.com/FreshRSS/Extensions) default branch from *master* to *main* [#&#8203;8194](FreshRSS/FreshRSS#8194)
- I18n
  - Translation status as text in README [#&#8203;7842](FreshRSS/FreshRSS#7842)
  - Add new translate CLI commands `move` [#&#8203;8214](FreshRSS/FreshRSS#8214)
  - Change some regional language codes to comply with RFC 5646 / IETF BCP 47 / ISO 3166 / ISO 639-1 [#&#8203;8065](FreshRSS/FreshRSS#8065)
  - Improve German [#&#8203;8028](FreshRSS/FreshRSS#8028)
  - Improve Greek [#&#8203;8146](FreshRSS/FreshRSS#8146)
  - Improve Finnish [#&#8203;8073](FreshRSS/FreshRSS#8073), [#&#8203;8092](FreshRSS/FreshRSS#8092)
  - Improve Hungarian [#&#8203;8244](FreshRSS/FreshRSS#8244)
  - Improve Italian [#&#8203;8115](FreshRSS/FreshRSS#8115), [#&#8203;8186](FreshRSS/FreshRSS#8186)
  - Improve Polish [#&#8203;8134](FreshRSS/FreshRSS#8134), [#&#8203;8135](FreshRSS/FreshRSS#8135)
  - Improve Russian [#&#8203;8155](FreshRSS/FreshRSS#8155), [#&#8203;8197](FreshRSS/FreshRSS#8197)
  - Improve Simplified Chinese [#&#8203;8308](FreshRSS/FreshRSS#8308), [#&#8203;8313](FreshRSS/FreshRSS#8313)
- Misc.
  - Add code to modify a search expression [#&#8203;8293](FreshRSS/FreshRSS#8293)
  - Remove *Pocket* sharing service [#&#8203;8127](FreshRSS/FreshRSS#8127), [#&#8203;8128](FreshRSS/FreshRSS#8128)
  - Update to PHPMailer 7.0.1 [#&#8203;8048](FreshRSS/FreshRSS#8048), [#&#8203;8180](FreshRSS/FreshRSS#8180), [#&#8203;8272](FreshRSS/FreshRSS#8272)
  - 💥 Housekeeping of `lib_rss.php` with potential breaking changes for some extensions [#&#8203;8193](FreshRSS/FreshRSS#8193),
  - Use native PHP `#[Deprecated]` [#&#8203;8325](FreshRSS/FreshRSS#8325)
  - Improve PHP code [#&#8203;8156](FreshRSS/FreshRSS#8156), [#&#8203;8203](FreshRSS/FreshRSS#8203), [#&#8203;8284](FreshRSS/FreshRSS#8284),
    [#&#8203;8292](FreshRSS/FreshRSS#8292), [#&#8203;8297](FreshRSS/FreshRSS#8297)
  - GitHub Actions: `--no-progress` [#&#8203;8315](FreshRSS/FreshRSS#8315)
  - Update dev dependencies [#&#8203;8043](FreshRSS/FreshRSS#8043), [#&#8203;8044](FreshRSS/FreshRSS#8044),
    [#&#8203;8045](FreshRSS/FreshRSS#8045), [#&#8203;8046](FreshRSS/FreshRSS#8046), [#&#8203;8047](FreshRSS/FreshRSS#8047),
    [#&#8203;8052](FreshRSS/FreshRSS#8052), [#&#8203;8176](FreshRSS/FreshRSS#8176), [#&#8203;8177](FreshRSS/FreshRSS#8177),
    [#&#8203;8178](FreshRSS/FreshRSS#8178), [#&#8203;8179](FreshRSS/FreshRSS#8179), [#&#8203;8210](FreshRSS/FreshRSS#8210),
    [#&#8203;8270](FreshRSS/FreshRSS#8270), [#&#8203;8271](FreshRSS/FreshRSS#8271), [#&#8203;8273](FreshRSS/FreshRSS#8273),
    [#&#8203;8274](FreshRSS/FreshRSS#8274), [#&#8203;8275](FreshRSS/FreshRSS#8275), [#&#8203;8276](FreshRSS/FreshRSS#8276)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi4zOS4xIiwidXBkYXRlZEluVmVyIjoiNDIuMzkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW1hZ2UiXX0=-->

Reviewed-on: https://gitea.alexlebens.dev/alexlebens/infrastructure/pulls/2851
Co-authored-by: Renovate Bot <renovate-bot@alexlebens.net>
Co-committed-by: Renovate Bot <renovate-bot@alexlebens.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrong parsing of HTTP trailer headers

2 participants