PHP Apple Enhanced Push Notification read error response

24,395

Solution 1

When you send a push notification, there are several problems:

  • If there is a problem, Apple will disconnect you but you don't know about it. When you use basic notifications there is no way to know if they were all sent or not. SOLUTION: This is the whole point of using an enhanced notification and then checking for an error response. Note that we will use "ORDER BY id" in database query and then use the id as the identifier that we send in notification. This way, if there is a problem, we know exactly which row in the db caused the problem (and therefore we know when Apple disconnected us and stopped sending the notifications). We can then continue sending Push notifications to all the rows after the row that caused the problem, without having to resend to the ones we already sent to.

  • Apple does NOT send any response back if everything is ok, so this can cause your script to pause and wait forever while fread() is waiting for data that is not coming. SOLUTION: Need to set stream_set_blocking to 0 so that fread always returns right away. Note that this causes another minor issue that fread can return before it receives an error response, but see the workaround in the code, which is just to pause for 1/2 a second AFTER all your sending is done and then check fread one more time.

  • You can send multiple push notifications much faster than it takes an error response to get back to you. SOLUTION: Again this is the same workaround mentioned above... pause for 1/2 a second AFTER all your sending is done and then check fread one more time.

Here is my solution using PHP, which addresses all my problems that I encountered. Its pretty basic but gets the job done. I have tested it with sending a few notifications at a time as well as sending out 120,000 at one time.

<?php
/*
 * Read Error Response when sending Apple Enhanced Push Notification
 *
 * This assumes your iOS devices have the proper code to add their device tokens
 * to the db and also the proper code to receive push notifications when sent.
 *
 */

//database
$host = "localhost";
$user = "my_db_username";
$pass = "my_db_password";
$dbname = "my_db_name";
$con = mysql_connect($host, $user, $pass);
if (!$con) {
    die('Could not connect to database: ' . mysql_error());
} else {
    mysql_select_db($dbname, $con);
}

// IMPORTANT: make sure you ORDER BY id column
$result = mysql_query("SELECT id,token FROM `device_tokens` ORDER BY id");

//Setup notification message
$body = array();
$body['aps'] = array('alert' => 'My push notification message!');
$body['aps']['notifurl'] = 'http://www.myexampledomain.com';
$body['aps']['badge'] = 1;

//Setup stream (connect to Apple Push Server)
$ctx = stream_context_create();
stream_context_set_option($ctx, 'ssl', 'passphrase', 'password_for_apns.pem_file');
stream_context_set_option($ctx, 'ssl', 'local_cert', 'apns.pem');
$fp = stream_socket_client('ssl://gateway.push.apple.com:2195', $err, $errstr, 60, STREAM_CLIENT_CONNECT, $ctx);
stream_set_blocking ($fp, 0); //This allows fread() to return right away when there are no errors. But it can also miss errors during last seconds of sending, as there is a delay before error is returned. Workaround is to pause briefly AFTER sending last notification, and then do one more fread() to see if anything else is there.

if (!$fp) {
    //ERROR
    echo "Failed to connect (stream_socket_client): $err $errstrn";
} else {
    $apple_expiry = time() + (90 * 24 * 60 * 60); //Keep push alive (waiting for delivery) for 90 days

    //Loop thru tokens from database
    while($row = mysql_fetch_array($result)) {
        $apple_identifier = $row["id"];
        $deviceToken = $row["token"];
        $payload = json_encode($body);
        //Enhanced Notification
        $msg = pack("C", 1) . pack("N", $apple_identifier) . pack("N", $apple_expiry) . pack("n", 32) . pack('H*', str_replace(' ', '', $deviceToken)) . pack("n", strlen($payload)) . $payload;
        //SEND PUSH
        fwrite($fp, $msg); 
        //We can check if an error has been returned while we are sending, but we also need to check once more after we are done sending in case there was a delay with error response.
        checkAppleErrorResponse($fp);
    }

    //Workaround to check if there were any errors during the last seconds of sending.
    usleep(500000); //Pause for half a second. Note I tested this with up to a 5 minute pause, and the error message was still available to be retrieved

    checkAppleErrorResponse($fp);

    echo 'DONE!';

    mysql_close($con);
    fclose($fp);
}

//FUNCTION to check if there is an error response from Apple
//         Returns TRUE if there was and FALSE if there was not
function checkAppleErrorResponse($fp) {

   //byte1=always 8, byte2=StatusCode, bytes3,4,5,6=identifier(rowID). Should return nothing if OK.
   $apple_error_response = fread($fp, 6);
   //NOTE: Make sure you set stream_set_blocking($fp, 0) or else fread will pause your script and wait forever when there is no response to be sent.

   if ($apple_error_response) {
        //unpack the error response (first byte 'command" should always be 8)
        $error_response = unpack('Ccommand/Cstatus_code/Nidentifier', $apple_error_response);

        if ($error_response['status_code'] == '0') {
            $error_response['status_code'] = '0-No errors encountered';
        } else if ($error_response['status_code'] == '1') {
            $error_response['status_code'] = '1-Processing error';
        } else if ($error_response['status_code'] == '2') {
            $error_response['status_code'] = '2-Missing device token';
        } else if ($error_response['status_code'] == '3') {
            $error_response['status_code'] = '3-Missing topic';
        } else if ($error_response['status_code'] == '4') {
            $error_response['status_code'] = '4-Missing payload';
        } else if ($error_response['status_code'] == '5') {
            $error_response['status_code'] = '5-Invalid token size';
        } else if ($error_response['status_code'] == '6') {
            $error_response['status_code'] = '6-Invalid topic size';
        } else if ($error_response['status_code'] == '7') {
            $error_response['status_code'] = '7-Invalid payload size';
        } else if ($error_response['status_code'] == '8') {
            $error_response['status_code'] = '8-Invalid token';
        } else if ($error_response['status_code'] == '255') {
            $error_response['status_code'] = '255-None (unknown)';
        } else {
            $error_response['status_code'] = $error_response['status_code'] . '-Not listed';
        }

        echo '<br><b>+ + + + + + ERROR</b> Response Command:<b>' . $error_response['command'] . '</b>&nbsp;&nbsp;&nbsp;Identifier:<b>' . $error_response['identifier'] . '</b>&nbsp;&nbsp;&nbsp;Status:<b>' . $error_response['status_code'] . '</b><br>';
        echo 'Identifier is the rowID (index) in the database that caused the problem, and Apple will disconnect you from server. To continue sending Push Notifications, just start at the next rowID after this Identifier.<br>';

        return true;
   }
   return false;
}
?>

Solution 2

Am not sure the content of you code but you should try ApnsPHP it is well tested works perfectly fine and it able to handle all possible exception and error for you.

Other Alternatives

https://github.com/sebastianborggrewe/PHP-Apple-Push-Notification-Server https://github.com/bortuzar/PHP-Mysql---Apple-Push-Notification-Server

Have tested 2 out of the 3 i examples and did not have issue of implementation and error management.

Thanks

:)

Share:
24,395
jsherk
Author by

jsherk

http://www.iwebss.com/voip/196-callcentric-voip-phone-provider-review

Updated on July 30, 2020

Comments

  • jsherk
    jsherk almost 4 years

    In PHP, how do you use fread() to check if there is an error response when sending enhanced push notifications?

    I have read the Apple docs, a couple vague posts thru Google, and a couple questions/answers here on SO but this was still very confusing.

    Here is what I looked at: http://developer.apple.com/library/mac/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/IPhoneOSClientImp/IPhoneOSClientImp.html Reading error from Apple enhanced push notification with PHP iPhone Push Notification - Error response problem

    I am going to answer my own question below, based on the fact that: (1) I found this a very confusing topic, and (2) I had to piece the information together with lots of trial and error to get it to work, and (3) this blog post that says it is encouraged: https://blog.stackoverflow.com/2011/07/its-ok-to-ask-and-answer-your-own-questions/

  • jsherk
    jsherk about 12 years
    Thanks for these alternatives.
  • Michael Kunst
    Michael Kunst over 10 years
    Hi. I implemented you're code in my (working) pusher class. Strangely I dont't ever get a response from apple. The $apple_error_response is always false. But some of the push messages ARE delivered, and some just fail. Have you any idea why I don't get any response?
  • jsherk
    jsherk over 10 years
    I posted this almost a year and a half ago and stopped using push notifications not long after so I don't really remember all the issues I dealt with. For debugging create list of tokens that you know work then copy tokens & modify so they will fail (so 5 known good & 5 known bad). Then also once you have accepted push notification on your device and verified that you receive it, disable the push notifications on that device and see what happens when you try to send to that token again. Also check Apple's documentation to make sure they did not change something on how their server responds.
  • Michael Kunst
    Michael Kunst over 10 years
    Thanks for your answer. I had to increase the usleep time to one second, then it worked (mostly). But I still wasnt sure if the answer would be there in this time. So I made a loop which checks for an answer every 50 miliseconds, and if it didnt come at all after 5 seconds it returns.
  • Julian F. Weinert
    Julian F. Weinert over 9 years
    I don't even get error data when providing a token of lalelu...?!
  • Alex
    Alex over 8 years
    Late to the party, but I figured out that Apple will only tell you if the push token is invalid if you're sending it a VALID hexadecimal value as a push token. If it's not, the message won't even be read on their end. So the lalelu value needs to first be checked if it's valid hex (use PHP's ctype_xdigit)
  • Paresh Gami
    Paresh Gami over 8 years
    can i send push message to 100 user with single request only?
  • Jasminder Pal Singh
    Jasminder Pal Singh about 7 years
    Hey , I am having an issue while using fread , it finishes the loading . Please check my question : stackoverflow.com/questions/42343382/…
  • Ionut
    Ionut over 6 years
    The page keeps loading with nothing to see.
  • Biswas Khayargoli
    Biswas Khayargoli over 5 years
    What is row['id'] here ? is it just incremental primary key?
  • jsherk
    jsherk over 5 years
    @BiswasKhayargoli Yes its just the auto-increment primary key.