mirror of
https://github.com/BushlanovDev/max-bot-api-client-php.git
synced 2026-10-07 12:29:11 +00:00
Skip updates that do not fit their model instead of crashing
#17 made unsupported update types silent: createUpdate() throws a LogicException, and createUpdateList() and WebhookHandler log it and skip the update. An update of a known type whose payload no longer fits the model (the API made a field optional, added an enum value) fails earlier, with a TypeError or ValueError from the model constructor, and nothing catches those: - createUpdateList() lets it out of getUpdates(), and LongPollingHandler::handle() catches only \Exception, so the loop stops and restarts into the same batch - WebhookHandler answers 500 and MAX keeps retrying createUpdate() now turns TypeError and ValueError into a LogicException (the original one kept as previous) and logs a warning with the payload, so the existing silent mode skips such an update. Errors thrown by user handlers are not affected.
This commit is contained in:
+37
-21
@@ -75,6 +75,8 @@ use LogicException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use ReflectionException;
|
||||
use TypeError;
|
||||
use ValueError;
|
||||
|
||||
/**
|
||||
* Creates DTOs from raw associative arrays returned by the API client.
|
||||
@@ -375,27 +377,41 @@ readonly class ModelFactory
|
||||
*/
|
||||
public function createUpdate(array $data): AbstractUpdate
|
||||
{
|
||||
return match (UpdateType::tryFrom($data['update_type'] ?? '')) {
|
||||
UpdateType::MessageCreated => MessageCreatedUpdate::fromArray($data),
|
||||
UpdateType::MessageCallback => MessageCallbackUpdate::fromArray($data),
|
||||
UpdateType::MessageEdited => MessageEditedUpdate::fromArray($data),
|
||||
UpdateType::MessageRemoved => MessageRemovedUpdate::fromArray($data),
|
||||
UpdateType::BotAdded => BotAddedToChatUpdate::fromArray($data),
|
||||
UpdateType::BotRemoved => BotRemovedFromChatUpdate::fromArray($data),
|
||||
UpdateType::DialogMuted => DialogMutedUpdate::fromArray($data),
|
||||
UpdateType::DialogUnmuted => DialogUnmutedUpdate::fromArray($data),
|
||||
UpdateType::DialogCleared => DialogClearedUpdate::fromArray($data),
|
||||
UpdateType::DialogRemoved => DialogRemovedUpdate::fromArray($data),
|
||||
UpdateType::UserAdded => UserAddedToChatUpdate::fromArray($data),
|
||||
UpdateType::UserRemoved => UserRemovedFromChatUpdate::fromArray($data),
|
||||
UpdateType::BotStarted => BotStartedUpdate::fromArray($data),
|
||||
UpdateType::BotStopped => BotStoppedUpdate::fromArray($data),
|
||||
UpdateType::ChatTitleChanged => ChatTitleChangedUpdate::fromArray($data),
|
||||
UpdateType::MessageChatCreated => MessageChatCreatedUpdate::fromArray($data),
|
||||
default => throw new LogicException(
|
||||
'Unknown or unsupported update type received: ' . ($data['update_type'] ?? 'none')
|
||||
),
|
||||
};
|
||||
try {
|
||||
return match (UpdateType::tryFrom($data['update_type'] ?? '')) {
|
||||
UpdateType::MessageCreated => MessageCreatedUpdate::fromArray($data),
|
||||
UpdateType::MessageCallback => MessageCallbackUpdate::fromArray($data),
|
||||
UpdateType::MessageEdited => MessageEditedUpdate::fromArray($data),
|
||||
UpdateType::MessageRemoved => MessageRemovedUpdate::fromArray($data),
|
||||
UpdateType::BotAdded => BotAddedToChatUpdate::fromArray($data),
|
||||
UpdateType::BotRemoved => BotRemovedFromChatUpdate::fromArray($data),
|
||||
UpdateType::DialogMuted => DialogMutedUpdate::fromArray($data),
|
||||
UpdateType::DialogUnmuted => DialogUnmutedUpdate::fromArray($data),
|
||||
UpdateType::DialogCleared => DialogClearedUpdate::fromArray($data),
|
||||
UpdateType::DialogRemoved => DialogRemovedUpdate::fromArray($data),
|
||||
UpdateType::UserAdded => UserAddedToChatUpdate::fromArray($data),
|
||||
UpdateType::UserRemoved => UserRemovedFromChatUpdate::fromArray($data),
|
||||
UpdateType::BotStarted => BotStartedUpdate::fromArray($data),
|
||||
UpdateType::BotStopped => BotStoppedUpdate::fromArray($data),
|
||||
UpdateType::ChatTitleChanged => ChatTitleChangedUpdate::fromArray($data),
|
||||
UpdateType::MessageChatCreated => MessageChatCreatedUpdate::fromArray($data),
|
||||
default => throw new LogicException(
|
||||
'Unknown or unsupported update type received: ' . ($data['update_type'] ?? 'none')
|
||||
),
|
||||
};
|
||||
} catch (TypeError|ValueError $e) {
|
||||
// The update type is known, but its payload does not fit the model: the API has
|
||||
// changed a field. Skip it like an unsupported update, so a webhook or a long-polling
|
||||
// loop does not stop on it, but log it louder.
|
||||
$message = sprintf(
|
||||
'Failed to parse update of type %s: %s',
|
||||
is_string($data['update_type'] ?? null) ? $data['update_type'] : 'none',
|
||||
$e->getMessage(),
|
||||
);
|
||||
$this->logger->warning($message, ['payload' => $data, 'exception' => $e]);
|
||||
|
||||
throw new LogicException($message, 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -783,6 +783,63 @@ final class ModelFactoryTest extends TestCase
|
||||
$assertionCallback($this, $attachment);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function createUpdateTurnsPayloadThatDoesNotFitTheModelIntoLogicException(): void
|
||||
{
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$logger->expects($this->once())
|
||||
->method('warning')
|
||||
->with($this->stringStartsWith('Failed to parse update of type bot_started: '));
|
||||
$factory = new ModelFactory($logger);
|
||||
|
||||
try {
|
||||
// `user` is required by BotStartedUpdate
|
||||
$factory->createUpdate(['update_type' => 'bot_started', 'timestamp' => 1, 'chat_id' => 123]);
|
||||
$this->fail('LogicException expected');
|
||||
} catch (LogicException $e) {
|
||||
$this->assertInstanceOf(\TypeError::class, $e->getPrevious());
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function createUpdateListSkipsUpdatesThatDoNotFitTheirModel(): void
|
||||
{
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$logger->expects($this->exactly(2))->method('warning');
|
||||
$factory = new ModelFactory($logger);
|
||||
|
||||
$updateList = $factory->createUpdateList([
|
||||
'updates' => [
|
||||
// TypeError: a required field is missing
|
||||
['update_type' => 'bot_started', 'timestamp' => 1, 'chat_id' => 123],
|
||||
// ValueError: an enum value the library does not know yet
|
||||
[
|
||||
'update_type' => 'message_created',
|
||||
'timestamp' => 2,
|
||||
'message' => [
|
||||
'timestamp' => 2,
|
||||
'body' => ['mid' => 'mid.1', 'seq' => 1],
|
||||
'recipient' => ['chat_type' => 'some_new_chat_type'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'update_type' => 'message_created',
|
||||
'timestamp' => 3,
|
||||
'message' => [
|
||||
'timestamp' => 3,
|
||||
'body' => ['mid' => 'mid.2', 'seq' => 2],
|
||||
'recipient' => ['chat_type' => 'dialog'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'marker' => 7,
|
||||
]);
|
||||
|
||||
$this->assertCount(1, $updateList->updates);
|
||||
$this->assertInstanceOf(MessageCreatedUpdate::class, $updateList->updates[0]);
|
||||
$this->assertSame(7, $updateList->marker);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function createUpdateListCatchesAndLogsLogicException(): void
|
||||
{
|
||||
|
||||
@@ -40,6 +40,7 @@ use Psr\Log\LoggerInterface;
|
||||
#[UsesClass(Recipient::class)]
|
||||
#[UsesClass(AbstractUpdate::class)]
|
||||
#[UsesClass(MessageCreatedUpdate::class)]
|
||||
#[UsesClass(ModelFactory::class)]
|
||||
final class WebhookHandlerTest extends TestCase
|
||||
{
|
||||
use PHPMock;
|
||||
@@ -226,4 +227,26 @@ final class WebhookHandlerTest extends TestCase
|
||||
|
||||
$handler->handle($request);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function handleSkipsUpdateThatDoesNotFitItsModel(): void
|
||||
{
|
||||
// `user` is required by BotStartedUpdate: before, the TypeError escaped the handler
|
||||
$payload = '{"update_type":"bot_started","timestamp":123,"chat_id":1}';
|
||||
$request = $this->createMockRequest($payload, self::SECRET);
|
||||
|
||||
$this->loggerMock->expects($this->once())
|
||||
->method('warning')
|
||||
->with($this->stringStartsWith('Failed to parse update of type bot_started: '));
|
||||
|
||||
$handlerWasCalled = false;
|
||||
$this->dispatcher->addHandler(UpdateType::BotStarted, function () use (&$handlerWasCalled) {
|
||||
$handlerWasCalled = true;
|
||||
});
|
||||
|
||||
$handler = new WebhookHandler($this->dispatcher, new ModelFactory($this->loggerMock), $this->loggerMock, self::SECRET);
|
||||
$handler->handle($request);
|
||||
|
||||
$this->assertFalse($handlerWasCalled);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user