From 8d015ed4c6cb634b2e9697bfed10d501c74486e5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 14 Jul 2026 12:35:41 +0530 Subject: [PATCH] (feat): surface transport error details in FCM push results --- src/Utopia/Messaging/Adapter.php | 10 +++- src/Utopia/Messaging/Adapter/Push/FCM.php | 38 ++++++++++--- .../Adapter/Email/ResendRoutingTest.php | 3 +- .../Adapter/Email/SESRoutingTest.php | 3 +- tests/Messaging/Adapter/Push/FCMTest.php | 55 +++++++++++++++++++ tests/Messaging/Adapter/SMS/Msg91Test.php | 4 +- 6 files changed, 100 insertions(+), 13 deletions(-) diff --git a/src/Utopia/Messaging/Adapter.php b/src/Utopia/Messaging/Adapter.php index 62ae91c2..3cc63f3b 100644 --- a/src/Utopia/Messaging/Adapter.php +++ b/src/Utopia/Messaging/Adapter.php @@ -146,7 +146,8 @@ private function recordResponse(Message $message, array $response): void * statusCode: int, * response: array|string|null, * headers: array, - * error: string|null + * error: string|null, + * errorCode: int * } * * @throws \Exception If the request fails. @@ -214,6 +215,7 @@ protected function request( 'response' => $response, 'headers' => $responseHeaders, 'error' => \curl_error($ch), + 'errorCode' => \curl_errno($ch), ]; } @@ -227,9 +229,10 @@ protected function request( * index: int, * url: string, * statusCode: int, - * response: array|null, + * response: array|string|null, * headers: array, - * error: string|null + * error: string|null, + * errorCode: int * }> * * @throws Exception @@ -340,6 +343,7 @@ protected function requestMulti( // capture (without copy_handle) if a multi-path adapter needs it. 'headers' => [], 'error' => \curl_error($ch), + 'errorCode' => $info['result'], ]; \curl_multi_remove_handle($mh, $ch); diff --git a/src/Utopia/Messaging/Adapter/Push/FCM.php b/src/Utopia/Messaging/Adapter/Push/FCM.php index 49866bd8..aac9af30 100644 --- a/src/Utopia/Messaging/Adapter/Push/FCM.php +++ b/src/Utopia/Messaging/Adapter/Push/FCM.php @@ -166,16 +166,40 @@ protected function process(PushMessage $message): array $response->incrementDeliveredTo(); $response->addResult($message->getTo()[$result['index']]); } else { - $error = - ($result['response']['error']['status'] ?? null) === 'UNREGISTERED' - || ($result['response']['error']['status'] ?? null) === 'NOT_FOUND' - ? $this->getExpiredErrorMessage() - : $result['response']['error']['message'] ?? 'Unknown error'; - - $response->addResult($message->getTo()[$result['index']], $error); + $response->addResult($message->getTo()[$result['index']], $this->getError($result)); } } return $response->toArray(); } + + /** + * @param array{ + * statusCode: int, + * response: array|string|null, + * error: string|null, + * errorCode: int + * } $result + */ + protected function getError(array $result): string + { + $response = \is_array($result['response']) ? $result['response'] : []; + $error = \is_array($response['error'] ?? null) ? $response['error'] : []; + + if (\in_array($error['status'] ?? null, ['UNREGISTERED', 'NOT_FOUND'], true)) { + return $this->getExpiredErrorMessage(); + } + + $message = $error['message'] ?? null; + if (\is_string($message) && $message !== '') { + return $message; + } + + $transportError = $result['error'] ?? null; + $details = "HTTP status {$result['statusCode']}; cURL error code {$result['errorCode']}"; + + return \is_string($transportError) && $transportError !== '' + ? "{$transportError} ({$details})" + : "Request failed ({$details})"; + } } diff --git a/tests/Messaging/Adapter/Email/ResendRoutingTest.php b/tests/Messaging/Adapter/Email/ResendRoutingTest.php index 2b68fa09..4ce70a21 100644 --- a/tests/Messaging/Adapter/Email/ResendRoutingTest.php +++ b/tests/Messaging/Adapter/Email/ResendRoutingTest.php @@ -135,7 +135,7 @@ class ResendStub extends Resend /** * @param array $headers * @param array|null $body - * @return array{url: string, statusCode: int, response: array|string|null, headers: array, error: string|null} + * @return array{url: string, statusCode: int, response: array|string|null, headers: array, error: string|null, errorCode: int} */ protected function request( string $method, @@ -160,6 +160,7 @@ protected function request( 'response' => $stub['response'], 'headers' => [], 'error' => null, + 'errorCode' => 0, ]; } } diff --git a/tests/Messaging/Adapter/Email/SESRoutingTest.php b/tests/Messaging/Adapter/Email/SESRoutingTest.php index 67a24300..3c46a201 100644 --- a/tests/Messaging/Adapter/Email/SESRoutingTest.php +++ b/tests/Messaging/Adapter/Email/SESRoutingTest.php @@ -657,7 +657,7 @@ class SESStub extends SES /** * @param array $headers * @param array|null $body - * @return array{url: string, statusCode: int, response: array|string|null, headers: array, error: string|null} + * @return array{url: string, statusCode: int, response: array|string|null, headers: array, error: string|null, errorCode: int} */ protected function request( string $method, @@ -682,6 +682,7 @@ protected function request( 'response' => $stub['response'], 'headers' => $stub['headers'] ?? [], 'error' => null, + 'errorCode' => 0, ]; } } diff --git a/tests/Messaging/Adapter/Push/FCMTest.php b/tests/Messaging/Adapter/Push/FCMTest.php index 17fdcaef..ccd61344 100644 --- a/tests/Messaging/Adapter/Push/FCMTest.php +++ b/tests/Messaging/Adapter/Push/FCMTest.php @@ -2,6 +2,7 @@ namespace Utopia\Tests\Adapter\Push; +use PHPUnit\Framework\Attributes\DataProvider; use Utopia\Messaging\Adapter\Push\FCM as FCMAdapter; class FCMTest extends Base @@ -19,4 +20,58 @@ protected function getTo(): array { return [\getenv('FCM_TO')]; } + + /** + * @param array{ + * statusCode: int, + * response: array|string|null, + * error: string|null, + * errorCode: int + * } $result + */ + #[DataProvider('errorProvider')] + public function testGetError(array $result, string $expected): void + { + $adapter = new class ('{}') extends FCMAdapter { + /** + * @param array{ + * statusCode: int, + * response: array|string|null, + * error: string|null, + * errorCode: int + * } $result + */ + public function error(array $result): string + { + return $this->getError($result); + } + }; + + $this->assertSame($expected, $adapter->error($result)); + } + + /** + * @return array, 1: string}> + */ + public static function errorProvider(): array + { + return [ + 'firebase error message' => [ + ['statusCode' => 400, 'response' => ['error' => ['message' => 'Invalid registration token']], 'error' => '', 'errorCode' => 0], + 'Invalid registration token', + ], + 'expired token' => [ + ['statusCode' => 404, 'response' => ['error' => ['status' => 'NOT_FOUND', 'message' => 'Requested entity was not found.']], 'error' => '', 'errorCode' => 0], + 'Expired device token', + ], + 'transport error' => [ + ['statusCode' => 0, 'response' => null, 'error' => 'Connection timed out after 10000 milliseconds', 'errorCode' => 28], + 'Connection timed out after 10000 milliseconds (HTTP status 0; cURL error code 28)', + ], + 'no error message' => [ + ['statusCode' => 503, 'response' => 'Service Unavailable', 'error' => '', 'errorCode' => 0], + 'Request failed (HTTP status 503; cURL error code 0)', + ], + ]; + } } diff --git a/tests/Messaging/Adapter/SMS/Msg91Test.php b/tests/Messaging/Adapter/SMS/Msg91Test.php index 3dff601f..9457f659 100644 --- a/tests/Messaging/Adapter/SMS/Msg91Test.php +++ b/tests/Messaging/Adapter/SMS/Msg91Test.php @@ -105,7 +105,8 @@ class Msg91TestAdapter extends Msg91 * statusCode: int, * response: array|string|null, * headers: array, - * error: string|null + * error: string|null, + * errorCode: int * } */ protected function request( @@ -124,6 +125,7 @@ protected function request( 'response' => [], 'headers' => [], 'error' => null, + 'errorCode' => 0, ]; } }