Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Expose DuplexStreamInterface for connections that requires Upgrades #382

Closed
wants to merge 15 commits into from

Conversation

bosunski
Copy link

@bosunski bosunski commented Jul 27, 2020

Based on the Idea of #376 by @clue

Emits an upgrade event when:

  • Connection header is present in the response headers and values has upgrade
  • Response status code is 101

Event emitted exposes a React\Stream\DuplexStreamInterface, a Psr\Http\Message\ResponseInterface and React\Http\Client\Request.

Summary of changes

  • Adds custom header parser to parse response data before data is passed down to the main data event parser.
  • Adds detection of Upgraded responses
  • Updated React\Http\Io\Sender and React\Http\Io\Transaction to be configurable for Upgrade connections

Example Usage:

$request = $client->request("GET", 'https://echo.websocket.org:443', ['Connection' => 'Upgrade', 'Upgrade' => 'websocket']);

$request->on('upgrade', function (DuplexStreamInterface $connection, $response, $request) {
    echo "Request Upgraded";
});

Websocket Client Usage:

$promise = $browser->withOptions(['upgrade' => true])->get('https://echo.websocket.org:443', ['Connection' => 'Upgrade', 'Upgrade' => 'websocket']);

$response->then(function (UpgradedResponse $response) use ($loop) {
    $ws = new \Ratchet\ClientWebSocket($response->getConnection(), $response->getResponse(), $response->getRequest());
});

Example Usage in clue/reactphp-docker

$this->browser->withOptions(array('streaming' => true, 'upgrade' => true))->post(
            $this->uri->expand(
                '/exec/{exec}/start',
                array(
                    'exec' => $exec
                )
            ),
            array(
                'Content-Type' => 'application/json',
                'Connection' => 'Upgrade',
                'Upgrade' => 'tcp',
            ),
            $this->json(array(
                'Tty' => !!$tty
            ))
  );

@bosunski
Copy link
Author

bosunski commented Jul 27, 2020

Still working on some failing tests, you can help with review while I fix the tests.
Thanks!
cc @clue @WyriHaximus

@bosunski bosunski changed the title Exposes an upgraded DuplexStreamInterface for connections that requires Upgrades Exposes a DuplexStreamInterface for connections that requires Upgrades Jul 28, 2020
@clue clue changed the title Exposes a DuplexStreamInterface for connections that requires Upgrades [WIP] Expose DuplexStreamInterface for connections that requires Upgrades Aug 11, 2020
Copy link
Member

@clue clue left a comment

Choose a reason for hiding this comment

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

@bosunski Thanks for looking into this, would love to get this feature in!

It looks like this is still a work in progress, but I like how this doesn't seem to affect a lot of code at the moment. I've added some minor remarks below.

Let us know if there's anything we can help with. Keep it up! 💪

src/Browser.php Outdated
@@ -720,7 +721,7 @@ private function withOptions(array $options)
* @param string $url
* @param array $headers
* @param string|ReadableStreamInterface $body
* @return PromiseInterface<ResponseInterface,Exception>
* @return PromiseInterface<ResponseInterface,Exception, ConnectionInterface>
Copy link
Member

Choose a reason for hiding this comment

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

Not sure what this is supposed to mean? The promise either fulfills with a ResponseInterface or rejects with an Exception.

Copy link
Author

Choose a reason for hiding this comment

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

I added it in the case, where an upgrade did happen, the promise will resolve with a ConnectionInterface rather than an Exception or a ResponseInterface. Still not sure if this is the best way to express that.

@@ -707,7 +708,7 @@ public function withResponseBuffer($maximumSize)
* @see self::withFollowRedirects()
* @see self::withRejectErrorResponse()
*/
private function withOptions(array $options)
public function withOptions(array $options)
Copy link
Member

Choose a reason for hiding this comment

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

Not sure I like exposing the withOptions() method again. It has been deprecated with clue/reactphp-buzz#172 not too long ago.

Do we really need this method? It's my understanding we could just rely on the Connection: upgrade and/or Upgrade: … request header(s) being present?

Copy link
Author

Choose a reason for hiding this comment

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

@clue I didn't know the method was deprecated.

But my thought behind it was to let the consumer using Browser to be able to choose to watch out for the upgrade event or not. As by default, Browser will not. This means that, if an upgrade did happen, I need to configure browser to capture it like $browser->withOptions(['upgrade' => true]) (default is false in Transaction.php).

This, however, won't be necessary if the default mode should be to capture the upgrade. That was the reason I made the method public (also seeing it was public in clue/reactphp-buzz)

if ($this->responseIsAnUpgradeResponse($psrResponse)) {
$this->stream->removeListener('data', array($this, 'handleData'));

$this->emit('upgrade', array($this->stream, $psrResponse, $this));
Copy link
Member

Choose a reason for hiding this comment

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

Not sure I like exposing the React\Http\Client\Request which is currently marked as @internal only. There's ongoing effort to remove all classes from this namespace as there's some duplication with the Io namespace.

Do we really need to expose the response and request objects?

Copy link
Author

Choose a reason for hiding this comment

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

Not sure if this is the right Request object to expose, but it's kind of a good idea to expose a request object because there could be cases where you want to verify some things about the request after upgrade and then close the connection if that verification fails. That's my thought about it. But sure, I might be wrong here.

I actually read a piece of Node docs to see how upgrade is being handled by the consumer side. I followed that in adding the upgrade event also. Here is a sample piece of how node handles the upgrade.

Sure, If there's a better you think this could be or should be done, I'll be willing to make it work.

if ($this->responseIsAnUpgradeResponse($psrResponse)) {
$this->stream->removeListener('data', array($this, 'handleData'));

$this->emit('upgrade', array($this->stream, $psrResponse, $this));
Copy link
Member

Choose a reason for hiding this comment

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

Not sure about the whole upgrade event. How does this interfere with the existing request/response semantics?

I like how this is exposed on the server side via https://github.com/reactphp/http#streaming-outgoing-response, perhaps we can find a similar solution here?

Copy link
Author

Choose a reason for hiding this comment

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

I actually followed a Node piece I read here as I also mentioned above.

I will check out the sample you sent, I kind of like it, knowing that a ThroughStream also implements DuplexStreamInterface.

@bosunski
Copy link
Author

Hello @clue , are there anything that can be done to move this PR forward since this is still a desired feature?

{
return $this->request;
}
}
Copy link
Member

Choose a reason for hiding this comment

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

nit: missing EOL

use Psr\Http\Message\ResponseInterface;
use React\Stream\DuplexStreamInterface;

class UpgradedResponse
Copy link
Member

Choose a reason for hiding this comment

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

This adds a new class as part of our public API in the internal React\Http\Client namespace.

@@ -770,7 +771,7 @@ private function withOptions(array $options)
* @param string $url
* @param array $headers
* @param string|ReadableStreamInterface $body
* @return PromiseInterface<ResponseInterface,\Exception>
* @return PromiseInterface<ResponseInterface,\Exception,ConnectionInterface>
Copy link
Member

Choose a reason for hiding this comment

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

What is this supposed to do? The PromiseInterface does not currently nor the foreseeable future have a third template type.

@clue
Copy link
Member

clue commented Sep 19, 2022

Hello @clue , are there anything that can be done to move this PR forward since this is still a desired feature?

@bosunski Thank you for the friendly reminder! I agree this feature would be nice to have, but in all honestly I don't see the currently suggested API as a viable alternative. We're moving towards a more type-safe future (see also https://github.com/orgs/reactphp/discussions/472) and this PR currently seems to add APIs that can not be covered by the existing type system. I think we're on the same page that the underlying feature request is still interesting, but I wonder what a decent API could look like?

@bosunski
Copy link
Author

Thanks for your response @clue

This PR is already 2 years old 😅 and I also agree that this PR needs a reimplementation to be consistent with the changes that have happened within ReactPHP itself and also to align with the current goals.

I'm still very interested in getting this out of the way.

Maybe we can start by talking about how the upgrade API will be used from a developers' point of view.

@clue
Copy link
Member

clue commented Oct 4, 2022

@bosunski Agreed, let's continue the discussion in the feature ticket #376 and close this PR. May I ask you to post a quick gist in that ticket so others can follow what's been decided so far? Thank you!

@clue clue closed this Oct 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants