2

I received an email from Google informing me that the following service is being decommissioned:

"Including multiple send requests in a single HTTP request to FCM known as Batch Send."

The recommended approach is to:

"Send messages via the HTTP v1 API, which has been optimized for fanout performance."

On this page:

https://firebase.google.com/support/faq#fcm-23-deprecation

it is mentioned that:

https://fcm.googleapis.com/batch

"Requests to the endpoint will start failing after 6/21/2024."

The recommended action is to:

"Migrate to the standard HTTP v1 API send method, which supports HTTP/2 for multiplexing."

Now, I have a question regarding this matter.

Currently, I am sending FCM messages using PHP and cURL via fcm.googleapis.com/batch. Since this will no longer work next year, I have already discontinued this method and now I have put the message sending process in a foreach (while) loop. This means that if, for example, I send 400 FCM messages, I will trigger or contact the following URL 400 times in succession / row:

https://fcm.googleapis.com/v1/projects/my-app/messages:send

Is this the intended behavior and not a problem? My project already uses HTTP/2. I'm just wondering if this is the correct approach, as I can't imagine that it is better than sending in batches or all at once. Thank you for clarifying.

Please let me know if you need any further assistance.

Here my foreach code:

foreach ($deviceTokens as $token) {
  $data = json_encode(array(
    "message" => array(
      "token" => $token,
      "notification" => array(
        "message_title" => "Test",
        "message_body" => "Test", 
        "website_link" => "example.com", 
        "notification_type" => "message",
        "image" => "example.com/test.jpg"
      )      
    )
  ));

$curl = curl_init();

curl_setopt_array($curl, array(
  CURLOPT_URL => 'https://fcm.googleapis.com/v1/projects/my-app/messages:send',
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => '',
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 0,
  CURLOPT_FOLLOWLOCATION => true,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_POSTFIELDS => $request,
  CURLOPT_HTTPHEADER => array(
    'Content-Type: multipart/mixed; boundary="subrequest_boundary"',
    'Authorization: Bearer ' . $accessToken 
  ),
));

$response = curl_exec($curl);
echo $response . '<br />';

curl_close($curl);
}
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
labu77
  • 605
  • 1
  • 9
  • 30

1 Answers1

1

If you'd like to continue raw cURL requests, you could have a look at the asynchronous curl_multi_* functions - truth be told, it's been a while since I've used cURL directly, so I will just refer to the PHP docs which also provide a usage example that you could use as a template.

However, the way I would recommend to take is using the google/auth PHP Library, the Guzzle HTTP Client to authenticate and send requests to the FCM API, and Guzzle Promises to execute the requests asynchronously.

To make things "worse", you not only have to use the HTTP V1 endpoint, but the HTTP V1 FCM message format as well. The code below shows how the message in your original post would change.

So, here's how I would do it from scratch in a single script with the help of Composer:

# Initialize the project
mkdir myproject
cd myproject
composer require google/auth
<?php
# run.php

# This example uses Google Application Credentials exposed via the
# `GOOGLE_APPLICATION_CREDENTIALS` environment variable
# See https://github.com/googleapis/google-auth-library-php/blob/main/README.md
# for more alternative ways to authenticate requests

declare(strict_types=1);

require 'vendor/autoload.php';

use Google\Auth\ApplicationDefaultCredentials;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Promise;

### Create a Guzzle client that authenticates requests to the FCM API Endpoints

putenv('GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/credentials.json');

// https://developers.google.com/identity/protocols/oauth2/scopes#fcm
$scopes = [
    'https://www.googleapis.com/auth/cloud-platform',
    'https://www.googleapis.com/auth/firebase.messaging',
];

// create middleware
$middleware = ApplicationDefaultCredentials::getMiddleware($scopes);
$stack = HandlerStack::create();
$stack->push($middleware);

$client = new Client([
  'handler' => $stack,
  'auth' => 'google_auth'
]);

### Setup the messages

$deviceTokens = [/* ... */];
$messages = [];

foreach ($deviceTokens as $token) {
    $messages[] = [
        'token' => $token,
        'notification' => [
            'title' => 'Notification Title',
            'body' => 'Notification Body',
            'image' => 'https://example.com/test.jpg',
        ],
        'webpush' => [
            'fcm_options' => [
                'link' => 'https://example.com'
            ],
        ],
    ];
}

### Create message request promises

$promises = function() use ($client, $messages) {
    foreach ($messages as $message) {
        yield $client->requestAsync('POST', 'https://fcm.googleapis.com/v1/projects/my-app/messages:send', [
            'json' => ['message' => $message],
        ]);
    }
};

### Create response handler

$handleResponses = function (array $responses) {
    foreach ($responses as $response) {
        if ($response['state'] === Promise\PromiseInterface::FULFILLED) {
            // $response['value'] is an instance of \Psr\Http\Message\RequestInterface
            echo $response['value']->getBody();
        } elseif ($response['state'] === Promise\PromiseInterface::REJECTED) {
            // $response['reason'] is an exception
            echo $response['reason']->getMessage();
        }
    }
};

Promise\Utils::settle($promises())
    ->then($handleResponses)
    ->wait();

I tested this script with one valid and one invalid registration token and got this result:

❯ php run.php
Client error: `POST https://fcm.googleapis.com/v1/projects/beste-firebase/messages:send` resulted in a `400 Bad Request` response:
{
  "error": {
    "code": 400,
    "message": "The registration token is not a valid FCM registration token",
    "stat (truncated...)
{
  "name": "projects/beste-firebase/messages/e39032c2-866d-4263-83e6-b1ce3770dfe6"
}

Or, if you don't want to do this all manually, you could also use the (unofficial) Firebase Admin PHP SDK, which switch to this exact approach in its latest 7.5.0 release. (Disclaimer: I'm the maintainer and obviously biased):

# Initialize the project
mkdir myproject
cd myproject
composer require kreait/firebase-php
<?php
# run.php

# Again, this example uses Google Application Credentials exposed via the
# `GOOGLE_APPLICATION_CREDENTIALS` environment variable. The SDK picks
# them up automatically.

declare(strict_types=1);

require 'vendor/autoload.php';

use Kreait\Firebase\Factory;

### Initialize FCM
putenv('GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/credentials.json');
$fcm = (new Factory())->createMessaging();

### Setup the message (without target)

$deviceTokens = [/* ... */];
$message = [
    'notification' => [
        'title' => 'Notification Title',
        'body' => 'Notification Body',
        'image' => 'https://example.com/test.jpg',
    ],
    'webpush' => [
        'fcm_options' => [
            'link' => 'https://example.com'
        ],
    ],
];

### Send the message to the given tokens

$reports = $fcm->sendMulticast($message, $deviceTokens);

# https://firebase-php.readthedocs.io/en/stable/cloud-messaging.html#send-messages-to-multiple-devices-multicast
# goes more into detail what you can do with the reports

I hope this helps!

jeromegamez
  • 3,348
  • 1
  • 23
  • 36
  • Thank you for your time. I appreciate it. I have 3 questions regarding this. 1. The credentials.json file is the file that can be obtained from the Console under Service Accounts, right? I already have this file to obtain the bearer token. 2. Would my code, as it is written above, not work next year, and if so, why? I use the v1 Api and a Bearer Token. 3. In your Firebase Admin PHP SDK, you also use sendMulticast. However, the FAQ states that this feature, known as "multicast" in legacy HTTP APIs, is not supported by the HTTP v1 API. Thank you. – labu77 Jun 28 '23 at 07:55
  • 1. Yes, correct! 2. It will! I'm sorry I misread these parts of your message, I saw "batch API" and was already jotting down the answer in my mind . But at least the part of using concurrent/asynchronous requests still stands 3. I kept the method name to not introduce breaking changes, but sending one message to multiple targets is still multicast, so I think it's fine to keep the name ^^ – jeromegamez Jun 28 '23 at 15:50
  • Thank you for your quick response. Let me summarize. My code as mentioned above would work, but due to the use of foreach/while loops, it would be a bad and inefficient solution. Is that correct? Your code (Firebase Admin PHP SDK) is better because it collects all the tokens first and then sends them all at once, instead of using a loop like in my case. Is that right? – labu77 Jun 28 '23 at 16:29
  • Your're correct, the code would perform the requests and handle the responses one after another, whereas the promise-based way allows to send all requests and wait until all are settled (`Promise\Utils::settle(...)`) and then iterates all responses. You don't need the SDK for that, the first described way is the SDK approach, without the SDK. – jeromegamez Jun 28 '23 at 18:52
  • 1
    I hope I can get it to work and figure out how to install it. We might then see each other in your GitHub forum. Once everything is up and running, I will definitely send a donation as well. – labu77 Jun 29 '23 at 08:28