-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[HttpClient] add EventSourceHttpClient to consume Server-Sent Events #36692
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
Conversation
Is Both JS and ReactPHP have an event loop running, and the checks in the EventSource are happening in this event loop. But without an event loop, I'm not sure this will be as easy to use. |
For now it is yes. It just opens an http connection open and uses a stream to get the data. On timeout, it reconnects to that http connection. |
I would suggest a radically different approach, based on the same design as HttpClient: a chunk generator. |
You should implement exactly the algorithm described in the WHATWG spec (9.2.4 and 9.2.5): https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream |
I agree. It could be a class decorating $response = $client->request('GET', 'https://demo.mercure.rocks/.well-known/mercure?topic=https://example.com');
if (200 !== $response->getStatusCode()) {
throw new \Exception('...');
}
// messages implement Symfony\Contracts\HttpClient\EventInterface
$fileHandler = fopen('/ubuntu.iso', 'w');
foreach ($client->messages($response) as $message) {
// https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format
// https://github.com/symfony/mercure/blob/master/src/Update.php#L26
$message->getData(); // string
$message->getType(); // ?string
$message->getId(); // ?string
$message->getRetry(); // ?string
// When you're done, close the connection with the server
$response->cancel();
} |
de989de
to
b88c4ca
Compare
src/Symfony/Component/HttpClient/Tests/Chunk/MessageChunkTest.php
Outdated
Show resolved
Hide resolved
|
||
$es = new EventSourceHttpClient(HttpClient::create()); | ||
|
||
$response = $es->request('GET', 'http://localhost:8080/events'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have Vulcain in the CI to test HttpClient, you could add Mercure, it uses all the spec capabilities.
addda43
to
9bb244e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we want to be able to read a stream in a non-blocking way?
HttpClientInterface::stream()
allows doing so by passing $timeout = 0
.
src/Symfony/Component/HttpClient/ServerSentEvents/EventSource.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/HttpClient/ServerSentEvents/EventSource.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/HttpClient/ServerSentEvents/EventSource.php
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really nice :)
Just minor comments on my side.
$this->reconnectionTime = $reconnectionTime; | ||
} | ||
|
||
public function connect($method, string $url, array $options = []): ResponseInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to have an interface for this one?
I'm not sure, just wondering.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so but let's gather opinions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm also in favor of having a dedicated interface. The user may want to provide its own EventSource implementation (for instance, to support a server not respecting strictly the standard or something like that).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'd prefer to have the new interface in the component, not in the contracts.
src/Symfony/Component/HttpClient/Tests/EventSourceHttpClientTest.php
Outdated
Show resolved
Hide resolved
What about adding an autowiring alias btw? |
Now I think having an interface would make sense :p. If we do so should we also add a configuration for the reconnectionTime? What about doing so in another PR? |
Another PR works. We need to gain some experience to know what's the best way to consume this class. |
} | ||
|
||
if ($chunk->isLast()) { | ||
yield $chunk; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it possible for the last chunk to have a non-empty content ? If yes, shouldn't we recreate a new last chunk instead, excluding the content we processed for new chunks ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, is it possible to reach the last chunk with some remaining buffered content in the state ? and what happens in that case ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say that it's not really a problem and if the last chunk is partial content it won't be transformed as a ServerSentEvent
chunk (which parses only full chunks). The full data will be retrieved later by using a Last-Event-Id
and there's no need to process it here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The last chunk is always empty so that's OK. About the remaining data in the buffer, L122 above ensures it's always emptied when the last chunk arrives.
], true)); | ||
} | ||
|
||
public function request(string $method, string $url, array $options = []): ResponseInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering whether it make sense for an EventSource implementation to still be a full HttpClient as well. Shouldn't it use a client inside it (a special one) instead ?
Also, will code using ->connect()
receive other chunks than event ones ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, will code using ->connect() receive other chunks than event ones ?
Yes it does, full power to the end-user. As the API exposed here is rather low-level, having a full HttpClient makes things easier. Also, wrapping a special client inside a new class would add a rather useless class to this component in my opinion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, having this implement HttpClientInterface
allows streaming both regular requests and event-streams at the same time.
e923244
to
9c1f354
Compare
cb05755
to
25387a5
Compare
@soyuka Can you work on a PR for the docs? |
25387a5
to
12ccca3
Compare
Thank you @soyuka. |
This PR was merged into the master branch. Discussion ---------- [HttpClient] Document EventSourceHttpClient Documenting symfony/symfony#36692 Commits ------- 1bfa68f Document EventSourceHttpClient
First implementation
This patch implements the [w3c Server-Sent Events specification](https://www.w3.org/TR/eventsource/#eventsource) on top of symfony's http client. It provides an `EventSource` class that allows you to interact of server sent events.Comparing to the Javascript implementation, we won't be able to use the same API. Indeed, in php listeners need to be setup before we connect to the HTTP stream.
I'm not fond of adding a dependency to EventDispatcher from HTTP Client, therefore I'm all ears if you have better solutions.
About event parsing, I wanted to avoid using regular expression and it uses smart data split. Note that I had to concatenate an internal buffer and only handle the data when a newline is found to cover long chunks. This is an alternative to this react php eventsource. Note that this implementation is closer to the specification in some cases that are still to be covered by tests (
retry
,data:value
without space after colon is valid etc.).This is an implementation of the Server-Sent Events specification based on symfony's HTTP Client. After a few suggestions on the first implementation (see details above), I've implemented a chunk generator with this kind of API:
TODO:
don't use EventDispatcher ?, need to be implemented asstream
instead ofmessage
)