APNS SSL operation failed with code 1
Asked Answered
A

5

10

EDIT - Using the enhanced binary format

Turns out I wasn't using the enhanced binary format so I changed my code.

<?php

$message = $_POST['message'];
$passphrase = $_POST['pass'];

//Connect to db


if ($db_found) {

// Create the payload body
$body['aps'] = array(
    'alert' => $message,
    'sound' => 'default'
);

$streamContext = stream_context_create();
stream_context_set_option($streamContext, 'ssl', 'local_cert', 'x.pem');
stream_context_set_option($streamContext, 'ssl', 'passphrase', $passphrase);

$fp = stream_socket_client('ssl://gateway.push.apple.com:2195', $error, $errorString, 15, STREAM_CLIENT_CONNECT, $streamContext);
stream_set_blocking ($fp, 0); 

if (!$fp)
    exit("Failed to connect: $err $errstr" . PHP_EOL);

echo 'Connected to APNS for Push Notification' . PHP_EOL;

// Keep push alive (waiting for delivery) for 90 days
$apple_expiry = time() + (90 * 24 * 60 * 60);



$tokenResult = //SQL QUERY TO GET TOKENS

while($row = mysql_fetch_array($tokenResult)) {
    $apple_identifier = $row["id"];
    $deviceToken = $row['device_id'];
    $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.
// 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
usleep(500000); 

checkAppleErrorResponse($fp);

echo 'Completed';

fclose($fp);


// SIMPLE BINARY FORMAT
/*for($i = 0; $i<count($deviceToken); $i++) {

    // Encode the payload as JSON
    $payload = json_encode($body);

    // Build the binary notification
    $msg = chr(0) . pack('n', 32) . pack('H*', $deviceToken[$i]) . pack('n', strlen($payload)) . $payload;

    // Send it to the server
    $result = fwrite($fp, $msg, strlen($msg));

    $bodyError .= 'result: '.$result.', devicetoken: '.$deviceToken[$i].'';

    if (!$result) {
        $errCounter = $errCounter + 1;
        echo 'Message not delivered' . PHP_EOL;
    }
    else
        echo 'Message successfully delivered' . PHP_EOL;
}*/


// Close the connection to the server
//fclose($fp);


//Insert message into database

mysql_close($db_handle);

}

else {

    print "Database niet gevonden ";
    mysql_close($db_handle);
}

// 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.

//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. 

$apple_error_response = fread($fp, 6);

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;
}

?>

While using this new code I still can't send more than 300+ messages because of this error:

Warning: fwrite() [function.fwrite]: SSL operation failed with code 1. OpenSSL Error messages: error:1409F07F:SSL routines:SSL3_WRITE_PENDING:bad write retry in PATH_TO_SCRIPT.php on line NUMBER

this code works fine when sending just a few push messages.

OLD QUESTION with simple binary format So I integrated Push Notifications a long time ago and it was working fine for messages sent to less than 500 people. Now I'm trying to send a push notification to more than 1000 people but then i get the broken error

Warning: fwrite() [function.fwrite]: SSL: Broken pipe in PATH_TO.PHP on line x

I've read the apple docs and I know that invalid tokens can cause the socket to disconnect. Some solutions online recommend on detecting disconnections and reconnect like this one:

Your server needs to detect disconnections and reconnect if necessary. Nothing is
"instant" when networking is involved; there's always some latency and code needs to take
that into account. Also, consider using the enhanced binary interface so you can check the
return response and know why the connection was dropped. The connection can also be
dropped as a result of TCP keep-alive, which is outside of Apple's control.

I'm also running a Feedback Service which detects Invalid tokens (Users who wanted Push Notifications but deleted the application) and that just works fine. That php script echos the deleted ID's and I can confirm that those tokens are deleted from our MySQL database.

How can I be able to detect a disconnect or broken pipe and react to that so my push notifications can reach more than 1000 people?

Currently I'm using this simple push.php script.

<?php

 $message = $_POST['message'];
 $passphrase = $_POST['pass'];

 //Connect to database stuff

 if ($db_found) {
      $streamContext = stream_context_create();
      stream_context_set_option($streamContext, 'ssl', 'local_cert', 'x.pem');
      stream_context_set_option($streamContext, 'ssl', 'passphrase', $passphrase);

      $fp = stream_socket_client('ssl://gateway.push.apple.com:2195', $error, $errorString, 15, STREAM_CLIENT_CONNECT, $streamContext);

 if (!$fp)
    exit("Failed to connect: $err $errstr" . PHP_EOL);

 echo 'Connected to APNS for Push Notification' . PHP_EOL;

 $deviceToken[] = //GET ALL TOKENS FROM DATABASE AND STORE IN ARRAY

for($i = 0; $i<count($deviceToken); $i++) {
    // Create the payload body
    $body['aps'] = array(
    'alert' => $message,
    'sound' => 'default'
    );

    // Encode the payload as JSON
    $payload = json_encode($body);

    // Build the binary notification
    $msg = chr(0) . pack('n', 32) . pack('H*', $deviceToken[$i]) . pack('n', strlen($payload)) . $payload;

    // Send it to the server
    $result = fwrite($fp, $msg, strlen($msg));

    $bodyError .= 'result: '.$result.', devicetoken: '.$deviceToken[$i].'';

    if (!$result) {
        $errCounter = $errCounter + 1;
        echo 'Message not delivered' . PHP_EOL;
    }
    else
        echo 'Message successfully delivered' . PHP_EOL;
}


echo $bodyError;

// Close the connection to the server
fclose($fp);


//CODE TO SAVE MESSAGE TO DATABSE HERE

if (!mysql_query($SQL,$db_handle)) { 
    die('Error: ' . mysql_error()); 
}

}
 else {
     print "Database niet gevonden ";
     mysql_close($db_handle);
 }


 ?>

Also fwrite returns 0 written bytes when the SLL Broken Pipe error occurs.

I must also mention that I'm no PHP or web developer but an app developer so my php skills aren't that good.

Adamo answered 22/8, 2013 at 10:57 Comment(5)
You are using the simple binary format, which doesn't return responses. You should use the enhanced format in order to get error responses. See this answer for more details.Reign
Thanks for that. I'm using the enchanced format now and it returns this error when sending a large amount of messages (not while sending just a few messages): Warning: fwrite() [function.fwrite]: SSL operation failed with code 1. OpenSSL Error messages: error:1409F07F:SSL routines:SSL3_WRITE_PENDING:bad write retry in PATH_TO_PHP_FILE_ON_LINEAdamo
Look at this question, does it help?Cantaloupe
Essentially it says that, when you are sending, you can have a write error. In this case, you must to the same write operation, with the same parameters, otherwise you'll receive the error 1409F07F. Before this error, do you have something echoed by the checkAppleErrorResponse($fp); function? I see that you call it inside the loop, but you don't check it's return value. If that function prints an error, you should manage it and do the fwrite again, before going to the next itemCantaloupe
Or, better, check the number of bytes written by the fwrite function. If it's 0, then there is an error and you must do the write operation againCantaloupe
C
4

When you do:

fwrite($fp, $msg);

you are trying to write to the socket. If something goes wrong, fwrite will return false or 0 (depending on the php version) as the return value. When it happens, you must manage it. You have two possibilities:

  • discard the entire operation
  • try again the last write operation

if you choose the second option, you have to do a new fwrite($fp, $msg) with THE SAME $fp and $msg of the failed fwrite() operation. If you change the parameters, a 1409F07F:SSL error is returned

Moreover, there are situations where the fwrite fails at writing only "some bytes", you should manage even this situation, comparing the returned value with the lenght of $msg. In this case, you should send the remaining part of the message, but in some situations you have to send the whole message again (according to this link).

Have a look at fwrite reference and the comments: Link

Cantaloupe answered 28/8, 2013 at 13:14 Comment(4)
So basically I need to check the length of $msg with strlen and the output of fwrite and compare those. If they aren't the same then do the whole write again with the same values? But what if the tokens isn't valid or something like that. Doesn't the script get stuck in this infinite loop?Adamo
Something like this, yes. To avoid infinite loops you must setup a max number of retries. Moreover, I suggest you to wait a moment (sleep?) before a retry: if the error is because a buffer is full, you'll give it the chance to free some space. Regarding the retries, in theory you should retry: with the whole message, if the bytes are 0, or with the remaining part of the message if the bytes are more than 0, but less than the total lenght. However, it seems that in some cases (depending on server side) you have to send the whole message even in this last case. Give it a tryCantaloupe
What would be a good number of retries and sleep time? 5x and sleep 3s?Adamo
It really depends upon the connection, the number of errors you face...that numbers see reasonable, but maybe I'll try with 1s.Cantaloupe
R
3

I can't give you actual PHP code, since I don't know PHP, but here's the logic you should use (according to Apple) :

Push Notification Throughput and Error Checking

If you're seeing throughput lower than 9,000 notifications per second, your server might benefit from improved error handling logic.

Here's how to check for errors when using the enhanced binary interface. Keep writing until a write fails. If the stream is ready for writing again, resend the notification and keep going. If the stream isn't ready for writing, see if the stream is available for reading.

If it is, read everything available from the stream. If you get zero bytes back, the connection was closed because of an error such as an invalid command byte or other parsing error. If you get six bytes back, that's an error response that you can check for the response code and the ID of the notification that caused the error. You'll need to send every notification following that one again.

Once everything has been sent, do one last check for an error response.

It can take a while for the dropped connection to make its way from APNs back to your server just because of normal latency. It's possible to send over 500 notifications before a write fails because of the connection being dropped. Around 1,700 notifications writes can fail just because the pipe is full, so just retry in that case once the stream is ready for writing again.

Now, here's where the tradeoffs get interesting. You can check for an error response after every write, and you'll catch the error right away. But this causes a huge increase in the time it takes to send a batch of notifications.

Device tokens should almost all be valid if you've captured them correctly and you're sending them to the correct environment. So it makes sense to optimize assuming failures will be rare. You'll get way better performance if you wait for write to fail or the batch to complete before checking for an error response, even counting the time to send the dropped notifications again.

None of this is really specific to APNs, it applies to most socket-level programming.

If your development tool of choice supports multiple threads or interprocess communication, you could have a thread or process waiting for an error response all the time and let the main sending thread or process know when it should give up and retry.

This is taken from Apple's Tech Note: Troubleshooting Push Notifications.

EDIT

I don't know how you detect in PHP that the write failed, but when it does, you should attempt to write the failed notification once again, and if it fails again, try to read the error response and close the connection.

If you manage to read the error response, you will know which notification failed and you'll know the error type (the most likely error is 8 - invalid device token). If after writing 100 messages you get an error response for the 80th message, you must resend messages 81 to 100, since Apple never received them. In my case (Java server), I don't always manage to read the error response (sometimes I get an error when trying to read the response from the socket). In that case I can only move on an send the next notifications (and have no way of knowing which notifications were actually received by Apple). That's why it's important to keep your database clean of invalid tokens.

Anyway, you shouldn't be stuck in an infinite loop, since when getting an error after sending N notifications, you are not going to resend these N notifications. Unless you manage to read an error response from Apple (in which case you know exactly what to resend), you'll only resend the last notification, and even if that notification happens to be the one with the invalid token, you'll probably get the next error after sending more notifications (which is unfortunate, since it would have been much easier to detect the invalid tokens if you would get the failures immediately).

If you keep your database clean (i.e. store in it only device tokens that were sent to your App by Apple, and all of them belong to the same push environment - either sandbox or production), you shouldn't encounter any invalid device tokens.

The device tokens returned by the feedback service are not invalid tokens. They are valid tokens of devices that uninstalled your app. Invalid tokens have never been valid for the current push environment, and never will. The only way to identify invalid tokens is to read the error responses from Apple.

EDIT2:

I forgot to mention it before. I encountered a similar problem to yours when implementing the push notification server side in Java. I couldn't reliably get all the error responses returned by Apple.

I found that in Java there's a way to disable the TCP Nagle's algorithm, which causes the buffering of multiple messages before sending them in a batch to Apple. Though Apple encourages us to use Nagle's algorithm (for performance reasons), I found that when I disable it and then try to read the response from Apple after each message I send to them, I manage to receive 100% of the error responses (I verified it by writing a process that simulated the APNS server).

By disabling Nagle's algorithm and sending the notifications one by one, slowly, and atempting to read the error response after each message, you can locate all the invalid tokens in your DB and remove them. Once you know your DB is clean you can enable Nagle's algorithm and resume sending notifications quickly without bothering to read the error responses from Apple. Then, whenever you get an error while writing a message to the socket, you can simply create a new socket and retry sending only the last message.

Reign answered 28/8, 2013 at 14:43 Comment(2)
Thanks for the info. My problem however is that I know whats going wrong and all the theory on the subject but just don't know PHP as well. There are so much examples of apns with php but they all fail at sending a large amount of notifications because none of them check for errors.Adamo
Btw im not stuck on php or whatsoever but just thought it was easy to implement. If there is another simple (and free) alternative that can fix this I would be happy as well.Adamo
B
1

My solution (to the now semi-old question) was that I had some development-environment APN tokens in my database trying to send to a production-environment. Once I got rid of them from my database the rest worked fine. Unfortunately, out of 7000+ APNs, I wasn't sure which tokens were bad so I had to erase them all in the hope that fresh tokens would be created when the user re-opened the app. So far so good.

Apple will halt all immediate attempts at sending a push notification if it comes across an erroneous APN token.

I had the exact same message appear which I had never seen before (below) on various apps so I'm glad I was able to resolve it.

Warning: fwrite() [function.fwrite]: SSL operation failed with code 1. OpenSSL Error messages: error:1409F07F:SSL routines:SSL3_WRITE_PENDING:bad write retry in PATH_TO_SCRIPT.php on line [NUMBER]

Ballon answered 7/4, 2014 at 11:16 Comment(0)
A
1

The solution is:

$msg = chr(0) . pack('n', 32) . pack('H*', $deviceToken) . pack('n', strlen($payload)) . $payload;
try {
    $result = fwrite($fp, $msg, strlen($msg));
} catch (Exception $ex) {
    sleep(1); //sleep for 5 seconds
    $result = fwrite($fp, $msg, strlen($msg));
}
Anonym answered 29/4, 2016 at 13:49 Comment(0)
D
0

Googling I found some interest things

http://rt.openssl.org/Ticket/Display.html?id=598&user=guest&pass=guest

As the patch comment says

first check if there is a SSL3_BUFFER still being written out. This will happen with non blocking IO

Answer of Why am I getting "error:1409F07F:SSL routines:SSL3_WRITE_PENDING: bad write retry" error while attempting an SSL_write? says:

SSL_Write returns with SSL_ERROR_WANT_WRITE or SSL_ERROR_WANT_READ, you have to repeat the call to SSL_write with the same parameters again, after the condition is satisfied.

Maybe the ssl buffer is still writing when you try to write, you can check if buffer is not writing, retry, or limiting the buffer could enough.

Duplicates:

Additional (edit)

Above I try to say that you need to figure out a way to determine if socket is not writing when you try to write again and then write.

If not have a way to do it, try:

  • Disabling the non-blocking block
  • Rerty the write

    while(!fwrite($fp, $msg)) {
        usleep(400000); //400 msec
    }
    

    if is successful, just disable the erors via error_reporting never use @ operator.

  • Setting stream_set_write_buffer() to 0
Drawl answered 28/8, 2013 at 15:11 Comment(2)
That second link doesn't really makes sense for me but it looks easy. What is $buffer exactly? Again I fail @ phpAdamo
The concept of buffer is too hard to explain for me right now and with my english level, maybe wikipedia or something alike can help you better with that, look at the edits, I read about it a little bit.Drawl

© 2022 - 2024 — McMap. All rights reserved.