Problem/Motivation

From #3275864: Update to Symfony 6.1.1. Looks like a PHP 8.1 compatibility issue being exposed by the Symfony update rather than anything to do with Symfony specifically.

Addition to symfony/http-foundation/Response::send()

BigPipeResponses are getting a Content-Length header that's based on the size before the replacement magic happened, which leads Responses being cut-off before their "true" end, leaving us with incomplete/incorrect HTML.

Testing Drupal\Tests\big_pipe\Functional\BigPipeTest

1) Drupal\Tests\big_pipe\Functional\BigPipeTest::testBigPipe
Undefined array key 0

/var/www/html/core/modules/big_pipe/tests/src/Functional/BigPipeTest.php:390
/var/www/html/core/modules/big_pipe/tests/src/Functional/BigPipeTest.php:185
/var/www/html/vendor/phpunit/phpunit/src/Framework/TestResult.php:726

2) Drupal\Tests\big_pipe\Functional\BigPipeTest::testBigPipeMultiOccurrencePlaceholders
Behat\Mink\Exception\ResponseTextException: The text "The count is 1." was not found anywhere in the text of the current page.

Knockdown-effect:

ctype_alpha(): Argument of type bool will be interpreted as string in the future
    1x in DemoUmamiProfileTest::testUser from Drupal\Tests\demo_umami\Functional
  1x: ctype_alpha(): Argument of type bool will be interpreted as string in the future
    1x in BreadcrumbTest::testBreadCrumbs from Drupal\Tests\system\Functional\Menu
1x: ctype_alpha(): Argument of type bool will be interpreted as string in the future
    1x in NodeTranslationUITest::testPublishedStatusNoFields from Drupal\Tests\node\Functional

https://www.drupal.org/pift-ci-job/2364329

Steps to reproduce

Proposed resolution

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

Comments

catch created an issue. See original summary.

catch’s picture

Title: ctype_alpha(): Argument of type bool will be interpreted as string in the future 1x in DemoUmamiProfileTest::testUser from Drupal\Tests\demo_umami\Functional » ctype_alpha(): Argument of type bool will be interpreted as string in the future
Issue summary: View changes
catch’s picture

Issue summary: View changes
spokje’s picture

Assigned: Unassigned » spokje
spokje’s picture

Version: 9.4.x-dev » 10.0.x-dev
Assigned: spokje » Unassigned
Status: Active » Needs review
Issue tags: -PHP 8.1
StatusFileSize
new59.61 KB

Objects Responses in the Mirror Big Pipe Are Closer Bigger Than They Appear: Addition to symfony/http-foundation/Response::send()

BigPipeResponses are getting a Content-Length header that's based on the size before the replacement magic happened, which leads Responses being cut-off before their "true" end.

Attached patch works(1) (Down from 27 to 22 failures (EDIT: Yes, 22 and boo! to random JS test failures)), doesn't break the current SF6.0 setup(2), but feels dirty and more of a workaround than actually solving anything.

Since the Content-Length header is set in symfony/http-foundation/Response::send() and that's being at the very end of the chain, I think we could do something like wrapping it in a \Drupal\Core\DrupalKernel::send function where we strip the Content-Length header if the Response is a \Drupal\big_pipe\Render\BigPipeResponse, and swap the $response->send(); here with our custom function.

Or maybe this is something that should be addressed upstream in symfony/http-foundation.

Anyway: I'll happily leave decisions like that to the Bigger Brains around here. Putting this on NR to attract those Brains.


(1) 3275864+3276186.patch: The diff of the MR in #3275864: Update to Symfony 6.1.1 as on 2022/04/23, on top of the changes in 3276186.patch, so the \Drupal\big_pipe\Render\BigPipeResponse::populateBasedOnOriginalHtmlResponse change.
(2) 3276186.patch:Solely the \Drupal\big_pipe\Render\BigPipeResponse::populateBasedOnOriginalHtmlResponse change.
spokje’s picture

StatusFileSize
new739 bytes
spokje’s picture

Title: ctype_alpha(): Argument of type bool will be interpreted as string in the future » BigPipe breaks with symfony/http-foundation 6.1-BETA1
Issue summary: View changes
spokje’s picture

Issue summary: View changes
catch’s picture

Priority: Major » Critical
StatusFileSize
new746 bytes

Good spot, bumping this to critical since it's a proper regression rather than a new deprecation or similar.

The issue we have is that big_pipe implements 'chunking' without actually using Transfer-Encoding: chunked (Drupal\big_pipe\BigPipe even claims that it uses the header). But do we need to do a lot of work to use Transfer-Encoding: chunked or is it just a case of setting the header? I guess there's an easy way to find out so uploading a patch. There's a further issue though that http/2 doesn't support Transfer-Encoding: chunked although I'm not clear what this means for responses that send it.

If we can't use the proper header, then #6 actually seems cleaner than doing this in DrupalKernel - it shouldn't need to know about BigPipe. The follow-up would end up being #2577631: Allow HtmlResponse to use a flexible emitter or something like it to centralise some of the logic.

Feels like this is worth an upstream issue to at least tell them what happened.

spokje’s picture

But do we need to do a lot of work to use Transfer-Encoding: chunked or is it just a case of setting the header? I guess there's an easy way to find out so uploading a patch.

I think I tried this (or something similar) locally and that failed tests as well.
Anyway: Locally and _think_ in one sentence, so we need a proper patch in here, thanks for that.

If we can't use the proper header, then #6 actually seems cleaner than doing this in DrupalKernel - it shouldn't need to know about BigPipe

Agreed.

Feels like this is worth an upstream issue to at least tell them what happened.

Agreed, I think we''ll wait on #3276186-9: [upstream] BigPipe breaks with symfony/http-foundation 6.1 return from The Land of TestBot before doing so?

Status: Needs review » Needs work

The last submitted patch, 9: 3276186_0.patch, failed testing. View results

spokje’s picture

@catch So (what seems to be) the correct way doesn't work.

I think we need to open an upstream issue, and I think if you did it, the issue would have a bit more gravitas (yes, I picked up a new word and want to use it more in conversation...). Also I think your Big Brain would jolt it down a bit more to the point than my incoherent ramblings.

Could you open it?

andypost’s picture

catch’s picture

Status: Needs work » Needs review
StatusFileSize
new676 bytes
new818 bytes

Here's the patch from #6 with a link to the upstream issue I just opened https://github.com/symfony/symfony/issues/46449

Status: Needs review » Needs work

The last submitted patch, 14: 3276186-14.patch, failed testing. View results

spokje’s picture

Status: Needs work » Needs review

Rrrrrandom JS testfailure. Back to NR

mondrake’s picture

Title: BigPipe breaks with symfony/http-foundation 6.1-BETA1 » BigPipe breaks with symfony/http-foundation 6.1

Symfony 6.1 is out.

mondrake’s picture

Status: Needs review » Reviewed & tested by the community

Looks like an upstream patch may take a while, other users reported similar issues, the Symfony team is looking for a test app that reproduces the issue. Maybe we can get this in to get going, the @todo is there, will need a follow up to apply the upstream fix once available.

catch’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new676 bytes

Suggested by Nicolas Grekas in the upstream issue, this might be a better workaround - just override ::send(), it's only a couple of lines without the code they've added. Keeping the @todo because I'm not sure we want to do this forever.

No interdiff because it's just a completely different couple line fix with no overlap.

mondrake’s picture

Status: Needs review » Needs work

PHPStan :)

mondrake’s picture

Also, maybe it's better open a followup in our queue and reference that in the @todo

catch’s picture

Status: Needs work » Needs review
StatusFileSize
new1 KB

This should pass phpcs/phpstan now. Also opened the follow-up in our queue.

mondrake’s picture

Status: Needs review » Reviewed & tested by the community
andypost’s picture

+++ b/core/modules/big_pipe/src/Render/BigPipeResponse.php
@@ -105,6 +105,27 @@ public function setBigPipeService(BigPipe $big_pipe) {
+    elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], TRUE)) {
+      static::closeOutputBuffers(0, TRUE);

interesting how it will affect swoole

catch’s picture

It shouldn't affect it at all, because the code is a direct 1-1 copy from the Symfony method. The only thing I did was remove the bit adding the content length header.

andypost’s picture

I see, thanks! +1 rtbc

wim leers’s picture

Wow, what a huge BC break! :O

The issue we have is that big_pipe implements 'chunking' without actually using Transfer-Encoding: chunked (Drupal\big_pipe\BigPipe even claims that it uses the header).

Indeed, it does, and I can observe it being set locally, at least according to Chrome. Just stepped through it with xdebug and indeed, $response as received by public function sendContent(BigPipeResponse $response) does not have that header set!

print_r($response->headers->all(), TRUE)

gives

Array
(
    [content-type] => Array
        (
            [0] => text/html; charset=UTF-8
        )

    [cache-control] => Array
        (
            [0] => must-revalidate, no-cache, private
        )

    [date] => Array
        (
            [0] => Tue, 31 May 2022 15:50:42 GMT
        )

    [x-drupal-dynamic-cache] => Array
        (
            [0] => HIT
        )

    [link] => Array
        (
            [0] => <http://d8.test/>; rel="canonical", <http://d8.test/>; rel="shortlink"
            [1] => <http://d8.test/>; rel="canonical", <http://d8.test/>; rel="shortlink"
        )

    [surrogate-control] => Array
        (
            [0] => no-store, content="BigPipe/1.0"
        )

    [x-accel-buffering] => Array
        (
            [0] => no
        )

    [x-ua-compatible] => Array
        (
            [0] => IE=edge
        )

    [content-language] => Array
        (
            [0] => en
        )

    [x-content-type-options] => Array
        (
            [0] => nosniff
        )

    [x-frame-options] => Array
        (
            [0] => SAMEORIGIN
        )

    [permissions-policy] => Array
        (
            [0] => interest-cohort=()
        )

    [x-drupal-cache-tags] => Array
        (
            [0] => 4xx-response block_view config:block.block.olivero_account_menu config:block.block.olivero_breadcrumbs config:block.block.olivero_content config:block.block.olivero_help config:block.block.olivero_main_menu config:block.block.olivero_messages config:block.block.olivero_page_title config:block.block.olivero_powered config:block.block.olivero_primary_admin_actions config:block.block.olivero_primary_local_tasks config:block.block.olivero_search_form_narrow config:block.block.olivero_search_form_wide config:block.block.olivero_secondary_local_tasks config:block.block.olivero_site_branding config:block.block.olivero_syndicate config:block_list config:search.settings config:system.menu.account config:system.menu.admin config:system.menu.main config:system.site config:system.theme http_response local_task rendered
        )

    [x-drupal-cache-contexts] => Array
        (
            [0] => cookies:big_pipe_nojs languages:language_interface route session.exists theme url.path.is_front url.path.parent url.query_args:_wrapper_format user.permissions user.roles:anonymous user.roles:authenticated
        )

    [x-drupal-cache-max-age] => Array
        (
            [0] => 0 (Uncacheable)
        )

    [expires] => Array
        (
            [0] => Sun, 19 Nov 1978 05:00:00 GMT
        )

    [x-generator] => Array
        (
            [0] => Drupal 9 (https://www.drupal.org)
        )

)
There's a further issue though that http/2 doesn't support Transfer-Encoding: chunked although I'm not clear what this means for responses that send it.

🤯 Source?

+++ b/core/modules/big_pipe/src/Render/BigPipeResponse.php
@@ -105,6 +105,27 @@ public function setBigPipeService(BigPipe $big_pipe) {
+    // cspell:ignore litespeed phpdbg
+    if (\function_exists('fastcgi_finish_request')) {
+      fastcgi_finish_request();
+    }
+    elseif (\function_exists('litespeed_finish_request')) {
+      litespeed_finish_request();
+    }
+    elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], TRUE)) {
+      static::closeOutputBuffers(0, TRUE);
+    }

I don't like that we're repeating all this … but fortunately https://github.com/symfony/symfony/issues/46449 was fixed 23 minutes ago, by reverting the upstream change that caused this 👍

I wonder if it makes more sense to wait for a new Symfony 6.1 patch release? I'm fine with moving ahead here and reverting all of this in #3283145: Try to use HTTP trailers or a real streaming response in Big Pipe. of course 😄

catch’s picture

Title: BigPipe breaks with symfony/http-foundation 6.1 » [upstream] BigPipe breaks with symfony/http-foundation 6.1
Status: Reviewed & tested by the community » Postponed

This has been reverted upstream due to the bc break: https://github.com/symfony/symfony/pull/46523

So I think we can forget about all the patches here, and wait for a new Symfony tag.

catch’s picture

@Wim Leers

My source for http/2 was https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding

Note: HTTP/2 doesn't support HTTP 1.1's chunked transfer encoding mechanism, as it provides its own, more efficient, mechanisms for data streaming.

Also found this 2013 listserve thread, which seems inconclusive https://lists.w3.org/Archives/Public/ietf-http-wg/2014JulSep/1676.html

Neither really explain what "doesn't support" means - i.e. what happens to a response using chunked transfer encoding when it meets http/2.

fwiw I tried just setting the header in #3276186-10: [upstream] BigPipe breaks with symfony/http-foundation 6.1 and that definitely doesn't work. Which means if we want to use it, we need to implement the actual encoding or find something that does it for us (i.e. encode the content length at the beginning of each chunk).

My overall feeling is that I think it'd be worth us trying chunked transfer encoding, and assume it'll work (i.e. be a no-op) if someone's using http/2.

I was thinking we should move this discussion to a follow-up, and already opened a placeholder. But since we don't need to commit anything here, maybe it'd be easier to repurpose this issue with a new title and issue summary for the docs/behaviour mismatch.

spokje’s picture

Status: Postponed » Closed (outdated)

AFAICT: This was resolved with SF 6.1.1, so I'm closing this as outdated.