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:
Timofey
2026-09-28 00:34:08 +05:00
parent 9bfe60a3a2
commit a50e5be823
3 changed files with 117 additions and 21 deletions
+57
View File
@@ -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
{
+23
View File
@@ -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);
}
}