Skip to content

Commit 992309f

Browse files
committed
fix(events): parse newline-delimited JSON from Docker event stream
Docker's /events endpoint returns newline-delimited JSON with Content-Type: application/json, not Server-Sent Events. The previous loop only handled ServerSentEvent chunks, which a plain HttpClient never emits, so listenForEvents() never invoked the callback and all container events were silently dropped. Buffer the raw data chunks and split on newlines, decoding each complete JSON line. Also drops the duplicate /events request that was issued on every loop iteration via the while() condition. Verified end-to-end against a live Docker daemon: start/stop/die/restart events now reach the callback. Drive-by: cs:fix adds a missing closure return type in EventsListenCommand.
1 parent a17b285 commit 992309f

2 files changed

Lines changed: 30 additions & 25 deletions

File tree

src/Client/DockerApiClientWrapper.php

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace WebProject\DockerApiClient\Client;
66

77
use JsonException;
8-
use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
98
use Symfony\Component\HttpClient\HttpClient;
109
use Symfony\Component\HttpClient\Psr18Client;
1110
use Symfony\Component\Serializer\Encoder\JsonEncoder;
@@ -17,6 +16,10 @@
1716
use WebProject\DockerApiClient\Event\ContainerEvent;
1817
use function is_array;
1918
use function json_decode;
19+
use function json_validate;
20+
use function strpos;
21+
use function substr;
22+
use function trim;
2023

2124
final class DockerApiClientWrapper
2225
{
@@ -46,35 +49,37 @@ public function listenForEvents(callable $eventCallback): void
4649
encoders: [new JsonEncoder()]
4750
);
4851

49-
// Connect to the Docker API event stream
52+
// Connect to the Docker API event stream. Docker streams
53+
// newline-delimited JSON objects (Content-Type: application/json),
54+
// NOT Server-Sent Events, so the payload arrives as plain data chunks
55+
// that must be buffered and split on newlines before decoding.
5056
$source = $client->request(method: 'GET', url: '/events');
5157

52-
while ($client->request(method: 'GET', url: '/events')) {
53-
foreach ($client->stream(responses: $source, timeout: 2) as $r => $chunk) {
54-
if ($chunk->isTimeout()) {
55-
// Handle timeout
56-
continue;
57-
}
58+
$buffer = '';
59+
foreach ($client->stream(responses: $source) as $chunk) {
60+
if ($chunk->isTimeout()) {
61+
continue;
62+
}
5863

59-
if ($chunk->isLast()) {
60-
// Handle end of stream
61-
return;
62-
}
64+
if ($chunk->isLast()) {
65+
return;
66+
}
6367

64-
// Process the ServerSentEvent chunk
65-
if ($chunk instanceof ServerSentEvent) {
66-
// Do something with the event data
67-
$content = $chunk->getContent();
68-
if (!json_validate($content)) {
69-
continue;
70-
}
68+
$buffer .= $chunk->getContent();
69+
70+
while (false !== ($newlinePos = strpos($buffer, "\n"))) {
71+
$line = trim(substr($buffer, 0, $newlinePos));
72+
$buffer = substr($buffer, $newlinePos + 1);
73+
74+
if ('' === $line || !json_validate($line)) {
75+
continue;
76+
}
7177

72-
$eventObject = json_decode(json: $content, associative: true, depth: 512, flags: JSON_THROW_ON_ERROR);
73-
if (is_array($eventObject) && ($eventObject['Type'] ?? false) === 'container') {
74-
$event = $serializer->deserialize(data: $content, type: ContainerEvent::class, format: 'json');
78+
$eventObject = json_decode(json: $line, associative: true, depth: 512, flags: JSON_THROW_ON_ERROR);
79+
if (is_array($eventObject) && ($eventObject['Type'] ?? false) === 'container') {
80+
$event = $serializer->deserialize(data: $line, type: ContainerEvent::class, format: 'json');
7581

76-
$eventCallback($event);
77-
}
82+
$eventCallback($event);
7883
}
7984
}
8085
}

src/Command/EventsListenCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
3939
'stop',
4040
];
4141

42-
$service->listenForEvents(function (ContainerEvent $event) use ($service, $actions, $io) {
42+
$service->listenForEvents(function (ContainerEvent $event) use ($service, $actions, $io): void {
4343
$container = $this->containers[$event->Actor->ID] ?? null;
4444
$prefix = '[event]';
4545
if ($container) {

0 commit comments

Comments
 (0)