Problem/Motivation

To send a streamed response the HtmlResponse::send() method would need to be overwritten.

However cloning the response is not a good option as any subclasses of MyOwnHtmlResponse with added properties would break that.

Also cloning into a new class is quite error prone.

Proposed resolution

Introduce Drupal\Component\HttpFoundation\ResponseEmitterInterface and Drupal\Component\HttpFoundation\ResponseEmitterTrait based on https://github.com/zendframework/zend-diactoros/blob/master/src/Response....

doing basically:


use Symfony\Component\HttpFoundation\Response;

interface ResponseEmitterInterface
{
    /**
     * Emit a response.
     *
     * @param ResponseInterface $response
     */
    public function emit(ResponseInterface $response);
}

and for the trait

trait ResponseEmitterTrait
{
    /**
     * Send a response.
     */
    public function send() {
        if ($this->responseEmitter) {
          $this->responseEmitter->emit($this);
          return $this;
        }

        parent::send();
        return $this;
    }

  public function getResponseEmitter() {
     return $this->responseEmitter;
  }

  public function setResponseEmitter(ResponseEmitterInterface $response_emitter) {
    $this->responseEmitter = $response_emitter;
  }

}

and


class HtmlResponse implements ..., ResponseEmitterInterface {
  use ResponseEmitterTrait;
  // ...
}

and thats it.

Remaining tasks

- Do it
- Write (unit) tests

API changes

- API addition for the Interface

Why this should be an RC target

Soft-Blocker for #2469431: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Fabianx created an issue. See original summary.

Wim Leers’s picture

Component: cache system » request processing system
Assigned: Unassigned » Crell

Moving to the right component, and assigning to @Crell.

Crell’s picture

This is essentially what Fabian and I discussed at DrupalCon BCN. However, we'd need an EmittableResponseInterface, which is what the trait is for. Don't confuse the emitter with the emitted.

Wim, are you assigning to me for validation or asking me to write it? :-)

Wim Leers’s picture

Validation :)

Wim Leers’s picture

Issue tags: +rc target triage

Per #2469431-114: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts and #2469431-115: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts:

Is #2577631: Allow HtmlResponse to use a flexible emitter a nice-to-have or a must-have?

+

#2577631: Allow HtmlResponse to use a flexible emitter is a must-have as neither cloning the response nor creating our own Response is a clean solution in the end - as discussed with Crell.

Also it sends very much the right signal - to be able to flexible stream responses, which is kinda a revived hype at the moment.

Also it is just PSR-7 back-porting overall and could well live in the future in Symfony upstream and be allowed for all Responses.

Crell’s picture

Status: Active » Needs review
FileSize
5.44 KB

Well, here's some code anyway. :-) It doesn't wire up any emitters yet, as I'm not certain where Fabian intended to do so, but this provides the basic code we discussed.

That said, while working on it, it occurred to me that perhaps we could just leverage Diactoros' emitters directly. We can convert a Symfony Response to PSR-7, can we not? And there's already a model in place for PSR-7 response emitters, in Diactoros. It's already in vendor. Could we just use that directly?

Fabianx’s picture

Issue tags: +Needs tests

I like #6 - except for The DefaultResponseEmitter, which cannot call send() for obvious reasons, but should copy the default response send() code and calling sendHeaders() / sendBody() this is exactly what we need.

I don't think it would be good to convert the HtmlResponse to PSR-7 now, because we then need to duplicate all Interfaces of HtmlResponse on PSR-7 and most middlewares are at this moment still in Symfony land.

However as we use kinda the same interface, once we switch to PSR-7 for good, it should continue to work with almost no changes needed.

yched’s picture

I still have to learn how to resist a naming nitpick, it seems, but :

+++ b/core/lib/Drupal/Component/HttpFoundation/EmittableResponseInterface.php
@@ -0,0 +1,17 @@
+interface EmittableResponseInterface {

It feels confusing to formulate this in such a way that "some Responses are emittable, and some are not" - and then what, we keep those and never send them to the client ? ;-). The distinction between "send" and "emit" is fairly subtle, especially since we are still in an HttpFoundation stack , and *not* in PSR-7/Diactoros land (AFAICT, Emitters seem to be a Diactoros construct rather than a PSR-7 one, right ?)

Sticking with layman's terms, this is not really about being "a response that can be emitted", but rather "a response to which you can assign a specific emitter" ?

--> Proposal : ResponseWithEmitterInterface, EmitterAwareResponse ?
That remains understandable even for people that don't have the background of the "we'd like to do some Diactoros stuff but have to stay in HttpFoundation at the moment so we port a concept over" discussion that happened in this issue ?

Crell’s picture

FabianX: I'm not sure I follow on DefaultResponseEmitter. What's wrong with send()? The net result is the same: The response object gets sent to the client. In practice I don't know that we'll use the Default emitter, but it's there to have something to implement that interface. :-) The interesting ones would be the ones you have to write yet. (nudge nudge... :-) )

yched: Yeah, the terminology is a bit mixed as we are, in essence, working around a design limitation of HttpFoundation and using Diactoros as a model. (Emitters are Diactoros specific, but the existence of something like them is implied by PSR-7 being data-only and having no send() of its own. So any PSR-7 server implementation would need something along those lines one way or another.)

I would be OK with EmitterAwareResponseInterface instead of EmittableResponseInterface, although I generally prefer adjective interface names to awareness. (There's something deeply philosophical in that sentence, I'm sure...)

yched’s picture

@Crell :

- Emitters / Diactoros / PSR-7 : thanks for the clarification, totally makes sense.

- "I generally prefer adjective interface names to awareness" - Sure, in the cases where a good adjective exists, ++ on the adjective. "Being able to delegate sending to an external independant emitter implementation" seems hard though (in english at least - I'm sure it's easy peasy in german :-p)
+ lol at the philosophical implications. I guess in Europe we like "aware" because of Jean-Claude Van Damme.

- DefaultResponseEmitter : I think @Fabianx means that the current results in an infinte "you do it, no you do it, no you..." loop between Response::send() -> DefaultResponseEmitter::emit() -> Response::send() -> ... ?

Fabianx’s picture

#10: Yes, that is what I meant.

I like the new naming.

Wim Leers’s picture

lol at the philosophical implications. I guess in Europe we like "aware" because of Jean-Claude Van Damme.

… You just pulled in Jean-Claude Van Damme into a sentence with "philosophical"? HERESY! He's a Belgian actor doing silly American movies, and he's being brought up by a French actor, home to Descarte and Sartre. I really don't know what the hell is happening here :D

Crell’s picture

I feel like a Hercule Poirot reference would be appropriate...

I see what you mean on the infinite loop. (Maybe that's how Jean-Claude Van Damme got in here?) I was thinking that the Emitter could work with any Response, and for most responses (ie, not the ones we're using it for) send() would still work. So we probably need a guard to not send() for an EmitterAwareResponse, or something.

Whatever, at this point I turn it over to FabianX to figure out how to leverage. :-)

Crell’s picture

Assigned: Crell » Fabianx
Wim Leers’s picture

#13: <offtopic>Hercule Poirot is most welcome at https://twitter.com/wimleers/status/653510681820557313</offtopic>

Wim Leers’s picture

Sorry for the distractions on this issue so far.


It seems that "emittable" kinda means "streamable" here. Which got me thinking… why do we even need all this? Can't we just change HtmlResponse to extend StreamedResponse instead of Response?

Then we could have a RESPONSE event listener that does setCallback('BigPipeStreamer::send'), which then results in index.php-> $response->send() -> StreamedResponse::send() -> BigPipeStreamer::send(). The only thing you can't do with that AFAICT is control how headers are sent.

Just playing the devil's advocate here, just trying to minimize change.

Wim Leers’s picture

I think the answer to #16 belongs in the IS, because I strongly suspect a committer will ask the same question.

Fabianx’s picture

#16: Nope, streamed responses don't have getContent() methods, so that won't work.

It is really only the emitting we want to control - not set a callback and make the assumptions that StreamedResponse makes.

Wim Leers’s picture

Oh, wow, what a useless piece of ****** is StreamedResponse!? Indeed, that doesn't work at all.

Ok, so let's continue with the current approach. I'm happy to review.

Wim Leers’s picture

So, from #5:

#2577631: Allow HtmlResponse to use a flexible emitter is a must-have as neither cloning the response nor creating our own Response is a clean solution in the end - as discussed with Crell.

So actually this is NOT clear why this makes it a must-have. If it's just for the sake of making it nicer, that doesn't make it a must-have.

The question is simple: is BigPipe possible without this? If no, then this is critical. If yes, then this isn't critical.

I'm still confused why this could be a must-have/critical if we've had a working BigPipe implementation without emitters.

Crell’s picture

Wim: The issue is the Response. Symfony Response objects are by default self-sending. That is, they have a single hard-coded emitter, send(). That can only be changed by extension, not by composition. That is the problem.

All responses get $response->send() called at the end. Period. That's where the actual printing happens.

In concept, suppose we did write a BigPipe routine, and stuck it into send(). How do you turn on bigpipe, then? You would have to change the response class you're using, at runtime, based on configuration. That's much more involved and complicated, and means you have 2 classes to maintain. If you have an HtmlResponse returned from somewhere (which does *not* have to come from the Renderer service, necessarily, so we can't rely on that), how do you "swap" from "normal page" to "BigPipe page"? You would have to add a Response listener that detects HtmlResponse (or similar), dissects it, and builds a BigPipeHtmlResponse out of it. That's extra cost, and very hard-coded. If you want another output mechanism you have to do that all over again.

If, instead, you allow a Response to opt-in to externalizing its send() (ie, composition), then a listener need only toss in an externalized send(), ie, an emitter. Very lightweight, very flexible.

So I guess it depends on your definition of "possible". Technically we could *probably* implement bigpipe without this approach, but the result would be so brittle that it would barely be usable, or worth using. Fabian?

(Side note: I think I will now use this example as my go-to for the danger of complecting a value object, Response, with behavior, emitting.)

xjm’s picture

Issue summary: View changes
Status: Needs review » Needs work

I gather that the reason this is proposed as an RC target is that it's a soft/niceness blocker for BigPipe? I updated the summary to that effect. Would be good to answer #20 there as well.

Fabianx’s picture

#21: Yes, the only thing a big_pipe contrib atm. can do is to hard-code assumptions to HtmlResponse and probably should check the exact class for that, then clone it as good as possible.

But as there is a known and working work-around, Wim and catch are of the opinion this is not RC material.

Which is unfortunate, but I guess it should be fine to add in 8.1.0, which is just 6 months away after release anyway.

And then present BigPipe as a complete and clean solution / product, rather than rush parts of this in - even though the disruption of this change here is zero.

Personally I actually like having more time overall :).

Crell’s picture

Version: 8.0.x-dev » 8.1.x-dev

Per Fabian, then.

Fabianx’s picture

Status: Needs work » Postponed

Thanks, Crell :).

Wim Leers’s picture

And then present BigPipe as a complete and clean solution / product, rather than rush parts of this in - even though the disruption of this change here is zero.

Emphasis mine. This is my biggest concern also.

Until 8.1.0, the current solution, which indeed involves a response subscriber, will work just fine. #21 uses the adjective "brittle", which I think is a vast exaggeration. 99% of text/html responses in Drupal 8 will be HtmlResponse objects. We can't magically apply BigPipe to plain Response objects anyway.

This is new territory. I'm glad we're not rushing it in. Especially considering there's no API change anyway.

Wim Leers’s picture

Status: Postponed » Active

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

xjm’s picture

Issue tags: -rc target triage
Fabianx’s picture

Assigned: Fabianx » Unassigned

.

Fabianx’s picture

Assigned: Unassigned » Fabianx
Status: Active » Needs review
Issue tags: -Needs issue summary update
FileSize
11.62 KB

And here is Crells start with the renames and BigPipe converted.

As it is easily seeable, changes to BigPipe are minimal - though more could be deleted - but as BigPipeInterface holds a lot of documentation, that would be not a good idea to delete it ...

I do think that except from some unit tests, this should be good to go.

All it does is, that it allows someone to specify the emitter to use for the send() method within a response - nothing more and nothing less.

Fabianx’s picture

FileSize
11.42 KB

Interdiff for previous comment.

Wim Leers’s picture

Status: Needs review » Needs work

Looks nice. But still needs tests. And I'm not sure about the EmitterAwareResponseInterface name. I think ResponseStreamerInterface and StreamedResponseInterface would make more sense. Especially because #2657684: Refactor BigPipe internals to allow a contrib module to extend BigPipe with the ability to stream anonymous responses and prime Page Cache for subsequent visits will likely add CacheableStreamedResponseInterface — see comments 44 and 45 there.

  1. +++ b/core/lib/Drupal/Component/HttpFoundation/DefaultResponseEmitter.php
    @@ -0,0 +1,34 @@
    +/**
    + * @file Contains Drupal\Component\HttpFoundation\DefaultResponseEmitter.
    + */
    

    @file docblocks are not necessary anymore :)

  2. +++ b/core/lib/Drupal/Component/HttpFoundation/DefaultResponseEmitter.php
    @@ -0,0 +1,34 @@
    + * Default Symfony response emitter.
    

    This is not Symfony, but Drupal. I guess the intent is to get this into upstream Symfony, and that's why it is named like this?

  3. +++ b/core/lib/Drupal/Component/HttpFoundation/ResponseEmitterInterface.php
    @@ -0,0 +1,40 @@
    +  public function emit(Response $response);
    

    This must to be typehinted to EmitterAwareResponseInterface, not Response.

  4. +++ b/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php
    @@ -61,7 +60,7 @@ public function onRespondEarly(FilterResponseEvent $event) {
    +   * Transforms a HtmlResponse to a response with BigPipe emitter.
    

    s/to a response with BigPipe emitter/to use the BigPipe emitter/

  5. +++ b/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php
    @@ -91,16 +90,8 @@ public function onRespond(FilterResponseEvent $event) {
    +    $big_pipe_response = $response;
    +    $big_pipe_response->setResponseEmitter($this->bigPipe);
     
    

    Well this can then just become $response->setResponseEmitter($this->bigPipe)

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

steinmb’s picture

Digging into a some performance issues on a site. Noticed this issue. Still needed or? The IS point to https://github.com/zendframework/zend-diactoros/blob/master/src/Response... that is archived and moved to https://github.com/laminas/laminas-diactoros/tree/3.4.x/src/Response that longer have this interface, could be renamed/moved though.

catch’s picture

iirc this was mainly for https://www.drupal.org/project/big_pipe_sessionless but didn't end up either blocking that or the core bigpipe module.