Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit e79eea1

Browse filesBrowse files
bug #57453 [HttpClient] Fix parsing SSE (fancyweb)
This PR was merged into the 5.4 branch. Discussion ---------- [HttpClient] Fix parsing SSE | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | - | License | MIT This PR fixes at least 2 behaviors: * Correctly parse comments to ignore them (yield them as DataChunk) * Avoid yielding a final empty SSE I also reworked the test to leverage MockHttpClient. Commits ------- add5d45 [HttpClient] Fix parsing SSE
2 parents 7085623 + add5d45 commit e79eea1
Copy full SHA for e79eea1

File tree

2 files changed

+55
-60
lines changed
Filter options

2 files changed

+55
-60
lines changed

‎src/Symfony/Component/HttpClient/EventSourceHttpClient.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/EventSourceHttpClient.php
+20-17Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\HttpClient;
1313

14+
use Symfony\Component\HttpClient\Chunk\DataChunk;
1415
use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
1516
use Symfony\Component\HttpClient\Exception\EventSourceException;
1617
use Symfony\Component\HttpClient\Response\AsyncContext;
@@ -121,17 +122,30 @@ public function request(string $method, string $url, array $options = []): Respo
121122
return;
122123
}
123124

124-
$rx = '/((?:\r\n){2,}|\r{2,}|\n{2,})/';
125-
$content = $state->buffer.$chunk->getContent();
126-
127125
if ($chunk->isLast()) {
128-
$rx = substr_replace($rx, '|$', -2, 0);
126+
if ('' !== $content = $state->buffer) {
127+
$state->buffer = '';
128+
yield new DataChunk(-1, $content);
129+
}
130+
131+
yield $chunk;
132+
133+
return;
129134
}
130-
$events = preg_split($rx, $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
135+
136+
$content = $state->buffer.$chunk->getContent();
137+
$events = preg_split('/((?:\r\n){2,}|\r{2,}|\n{2,})/', $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
131138
$state->buffer = array_pop($events);
132139

133140
for ($i = 0; isset($events[$i]); $i += 2) {
134-
$event = new ServerSentEvent($events[$i].$events[1 + $i]);
141+
$content = $events[$i].$events[1 + $i];
142+
if (!preg_match('/(?:^|\r\n|[\r\n])[^:\r\n]/', $content)) {
143+
yield new DataChunk(-1, $content);
144+
145+
continue;
146+
}
147+
148+
$event = new ServerSentEvent($content);
135149

136150
if ('' !== $event->getId()) {
137151
$context->setInfo('last_event_id', $state->lastEventId = $event->getId());
@@ -143,17 +157,6 @@ public function request(string $method, string $url, array $options = []): Respo
143157

144158
yield $event;
145159
}
146-
147-
if (preg_match('/^(?::[^\r\n]*+(?:\r\n|[\r\n]))+$/m', $state->buffer)) {
148-
$content = $state->buffer;
149-
$state->buffer = '';
150-
151-
yield $context->createChunk($content);
152-
}
153-
154-
if ($chunk->isLast()) {
155-
yield $chunk;
156-
}
157160
});
158161
}
159162
}

‎src/Symfony/Component/HttpClient/Tests/EventSourceHttpClientTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpClient/Tests/EventSourceHttpClientTest.php
+35-43Lines changed: 35 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
use Symfony\Component\HttpClient\Chunk\DataChunk;
1616
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
1717
use Symfony\Component\HttpClient\Chunk\FirstChunk;
18+
use Symfony\Component\HttpClient\Chunk\LastChunk;
1819
use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
1920
use Symfony\Component\HttpClient\EventSourceHttpClient;
2021
use Symfony\Component\HttpClient\Exception\EventSourceException;
22+
use Symfony\Component\HttpClient\MockHttpClient;
2123
use Symfony\Component\HttpClient\Response\MockResponse;
2224
use Symfony\Component\HttpClient\Response\ResponseStream;
2325
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -34,7 +36,11 @@ class EventSourceHttpClientTest extends TestCase
3436
*/
3537
public function testGetServerSentEvents(string $sep)
3638
{
37-
$rawData = <<<TXT
39+
$es = new EventSourceHttpClient(new MockHttpClient(function (string $method, string $url, array $options) use ($sep): MockResponse {
40+
$this->assertSame(['Accept: text/event-stream', 'Cache-Control: no-cache'], $options['headers']);
41+
42+
return new MockResponse([
43+
str_replace("\n", $sep, <<<TXT
3844
event: builderror
3945
id: 46
4046
data: {"foo": "bar"}
@@ -43,7 +49,18 @@ public function testGetServerSentEvents(string $sep)
4349
id: 47
4450
data: {}
4551
52+
: this is a oneline comment
53+
54+
: this is a
55+
: multiline comment
56+
57+
: comments are ignored
4658
event: reload
59+
60+
TXT
61+
),
62+
str_replace("\n", $sep, <<<TXT
63+
: anywhere
4764
id: 48
4865
data: {}
4966
@@ -62,58 +79,33 @@ public function testGetServerSentEvents(string $sep)
6279
6380
id: 60
6481
data
65-
TXT;
66-
$data = str_replace("\n", $sep, $rawData);
67-
68-
$chunk = new DataChunk(0, $data);
69-
$response = new MockResponse('', ['canceled' => false, 'http_method' => 'GET', 'url' => 'http://localhost:8080/events', 'response_headers' => ['content-type: text/event-stream']]);
70-
$responseStream = new ResponseStream((function () use ($response, $chunk) {
71-
yield $response => new FirstChunk();
72-
yield $response => $chunk;
73-
yield $response => new ErrorChunk(0, 'timeout');
74-
})());
75-
76-
$hasCorrectHeaders = function ($options) {
77-
$this->assertSame(['Accept: text/event-stream', 'Cache-Control: no-cache'], $options['headers']);
78-
79-
return true;
80-
};
81-
82-
$httpClient = $this->createMock(HttpClientInterface::class);
83-
$httpClient->method('request')->with('GET', 'http://localhost:8080/events', $this->callback($hasCorrectHeaders))->willReturn($response);
84-
85-
$httpClient->method('stream')->willReturn($responseStream);
86-
87-
$es = new EventSourceHttpClient($httpClient);
82+
TXT
83+
),
84+
], [
85+
'canceled' => false,
86+
'http_method' => 'GET',
87+
'url' => 'http://localhost:8080/events',
88+
'response_headers' => ['content-type: text/event-stream'],
89+
]);
90+
}));
8891
$res = $es->connect('http://localhost:8080/events');
8992

9093
$expected = [
9194
new FirstChunk(),
9295
new ServerSentEvent(str_replace("\n", $sep, "event: builderror\nid: 46\ndata: {\"foo\": \"bar\"}\n\n")),
9396
new ServerSentEvent(str_replace("\n", $sep, "event: reload\nid: 47\ndata: {}\n\n")),
94-
new ServerSentEvent(str_replace("\n", $sep, "event: reload\nid: 48\ndata: {}\n\n")),
97+
new DataChunk(-1, str_replace("\n", $sep, ": this is a oneline comment\n\n")),
98+
new DataChunk(-1, str_replace("\n", $sep, ": this is a\n: multiline comment\n\n")),
99+
new ServerSentEvent(str_replace("\n", $sep, ": comments are ignored\nevent: reload\n: anywhere\nid: 48\ndata: {}\n\n")),
95100
new ServerSentEvent(str_replace("\n", $sep, "data: test\ndata:test\nid: 49\nevent: testEvent\n\n\n")),
96101
new ServerSentEvent(str_replace("\n", $sep, "id: 50\ndata: <tag>\ndata\ndata: <foo />\ndata\ndata: </tag>\n\n")),
102+
new DataChunk(-1, str_replace("\n", $sep, "id: 60\ndata")),
103+
new LastChunk("\r\n" === $sep ? 355 : 322),
97104
];
98-
$i = 0;
99-
100-
$this->expectExceptionMessage('Response has been canceled');
101-
while ($res) {
102-
if ($i > 0) {
103-
$res->cancel();
104-
}
105-
foreach ($es->stream($res) as $chunk) {
106-
if ($chunk->isTimeout()) {
107-
continue;
108-
}
109-
110-
if ($chunk->isLast()) {
111-
continue;
112-
}
113-
114-
$this->assertEquals($expected[$i++], $chunk);
115-
}
105+
foreach ($es->stream($res) as $chunk) {
106+
$this->assertEquals(array_shift($expected), $chunk);
116107
}
108+
$this->assertSame([], $expected);
117109
}
118110

119111
/**

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.