Convert non-array update payloads to LogicException instead of TypeError

This commit is contained in:
Alex
2026-09-30 23:40:03 +03:00
parent bf5219af7a
commit 919970fa20
3 changed files with 108 additions and 2 deletions
+11 -2
View File
@@ -369,14 +369,23 @@ readonly class ModelFactory
/**
* Creates a specific Update model based on the 'update_type' field.
*
* @param array<string, mixed> $data Raw data for a single update.
* @param mixed $data Raw data for a single update. Anything that is not an array
* (e.g. a scalar produced by json_decode) is converted to LogicException,
* so a webhook or a long-polling loop can skip it instead of crashing.
*
* @return AbstractUpdate
* @throws ReflectionException
* @throws LogicException
*/
public function createUpdate(array $data): AbstractUpdate
public function createUpdate(mixed $data): AbstractUpdate
{
if (!is_array($data)) {
$message = sprintf('Failed to parse update: expected JSON object, got %s', get_debug_type($data));
$this->logger->warning($message, ['payload' => $data]);
throw new LogicException($message);
}
try {
return match (UpdateType::tryFrom($data['update_type'] ?? '')) {
UpdateType::MessageCreated => MessageCreatedUpdate::fromArray($data),
+61
View File
@@ -840,6 +840,67 @@ final class ModelFactoryTest extends TestCase
$this->assertSame(7, $updateList->marker);
}
public static function nonArrayPayloadProvider(): array
{
return [
'string' => ['123'],
'integer' => [42],
'null' => [null],
'float' => [3.14],
'boolean' => [true],
];
}
#[Test]
#[DataProvider('nonArrayPayloadProvider')]
public function createUpdateTurnsNonArrayPayloadIntoLogicException(mixed $payload): void
{
$logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->once())
->method('warning')
->with(
'Failed to parse update: expected JSON object, got ' . get_debug_type($payload),
['payload' => $payload],
);
$factory = new ModelFactory($logger);
try {
$factory->createUpdate($payload);
$this->fail('LogicException expected');
} catch (LogicException $e) {
$this->assertNull($e->getPrevious());
}
}
#[Test]
public function createUpdateListSkipsNonArrayElements(): void
{
$logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->exactly(2))->method('warning');
$factory = new ModelFactory($logger);
$updateList = $factory->createUpdateList([
'updates' => [
'junk',
41,
[
'update_type' => 'message_created',
'timestamp' => 3,
'message' => [
'timestamp' => 3,
'body' => ['mid' => 'mid.2', 'seq' => 2],
'recipient' => ['chat_type' => 'dialog'],
],
],
],
'marker' => 9,
]);
$this->assertCount(1, $updateList->updates);
$this->assertInstanceOf(MessageCreatedUpdate::class, $updateList->updates[0]);
$this->assertSame(9, $updateList->marker);
}
#[Test]
public function createUpdateListCatchesAndLogsLogicException(): void
{
+36
View File
@@ -249,4 +249,40 @@ final class WebhookHandlerTest extends TestCase
$this->assertFalse($handlerWasCalled);
}
public static function nonJsonObjectBodyProvider(): array
{
return [
'number' => ['123'],
'null' => ['null'],
'string' => ['"abc"'],
'boolean' => ['true'],
];
}
#[Test]
#[DataProvider('nonJsonObjectBodyProvider')]
public function handleSkipsBodyThatIsNotAJsonObject(string $payload): void
{
$request = $this->createMockRequest($payload, self::SECRET);
$this->loggerMock->expects($this->once())
->method('warning')
->with($this->stringContains('Failed to parse update: expected JSON object'));
$handlerWasCalled = false;
$this->dispatcher->addHandler(UpdateType::MessageCreated, function () use (&$handlerWasCalled) {
$handlerWasCalled = true;
});
$handler = new WebhookHandler(
$this->dispatcher,
new ModelFactory($this->loggerMock),
$this->loggerMock,
self::SECRET,
);
$handler->handle($request);
$this->assertFalse($handlerWasCalled);
}
}