admin管理员组

文章数量:1405314

WooCommerce Checkout Problem

About a week ago, with no changes to our code base, we started receiving fairly frequent order duplicates. There are no automatic updates applied to plugins or WordPress, and I'm the only one that has access to do these kinds of things. Approximately 1 order in 10 orders has a duplicate order. the customer would be contacted (or contact us) and offer some feedback on what they were seeing, which was mostly one of the two following errors:

  • "Sorry, your session has expired."
  • "Please check the reCaptcha box to place your order."

They had no idea that their order had already been created, and being the good customer that wanted their stuff, they would click on the place order button again.

Sometimes they would actually see no error message, and the AJAX call to /?wc-ajax=checkout would complete but not redirect them to the order confirmation page. This fact is an important thing to consider, because it means that if the result is success or failure, the order gets created, but the customer doesn't know it was created. On success, the browser isn't redirecting.

So as a temporary hack to find out what was going on, we altered the wp_send_json function in wp-includes/functions.php to provide some logging. We were then able to see what the JSON responses were, but nothing looked abnormal here. It did however show us that not all responses that included success as the result were being redirected to the order confirmation page:

{
    "result": "success",
    "redirect": "/?key=wc_order_ABCabc123xyz",
    "order_id": 123456
}

To me this looks like some sort of JavaScript error, as the success method of the AJAX call to wc_checkout_params.checkout_url should redirect the customer, but did not. So we tried disabling or deactivating many plugins and 3rd party JavaScript sources on the checkout page, but still no luck. We even tried disabling Varnish without success. We asked Cloudways if they had made any changes to the server, and they said no.

The next step was to prove that the JSON response from the server was actually received by checkout.js. So we hacked checkout.min.js adding a function to send a second AJAX call back to the server, with the response from the first call. That function looks something like this:

function logServerResponse(res,ts,xhr,err){
    var h = xhr && xhr.getAllResponseHeaders
        ? xhr.getAllResponseHeaders()
        : "N/A";
    var payload={
        result: res ? JSON.stringify(res) : "N/A",
        textStatus: ts,
        headers: h,
        error: err || ""
    };
    e.ajax({
        url:"/abc.php",
        method:"POST",
        cache:false,
        data:payload,
        dataType:"json"
    })
};

Function calls were added to the success and error methods of the first AJAX. The success and error methods themselves were also updated to include all the parameters needed to pass to logServerResponse:

Success modified

success:function(c,wy,wz){
    logServerResponse(c,wy,wz,"");
    //...
}

Error modified

error:function(e,o,c){
    logServerResponse(null,o,e,c);
    //...
}

This is when we realized that some of the responses from the server were just not showing up. The logging script at /abc.php would have logged something every time, but it doesn't:

<?php

$d = new DateTime('now', new DateTimeZone('America/Chicago') );

// Define the log file path
$logFile = __DIR__ . '/' . $d->format('Y-m-d') . '.log';

// Check if required keys exist in $_POST
if( 
    ! isset( $_POST['result'] ) OR
    ! isset( $_POST['textStatus'] ) OR 
    ! isset( $_POST['headers'] ) OR
    ! isset( $_POST['error'] )
){
    // Silently fail if all post vars not present
    http_response_code(200);
    exit;
}

// Prepare the log entry
$logEntry = [
    'timestamp' => $d->format('Y-m-d H:i:s'),
    'post_data' => [
        'result'     => $_POST['result'],
        'textStatus' => $_POST['textStatus'],
        'headers'    => $_POST['headers'],
        'error'      => $_POST['error'],
    ]
];

// JSON-encode with pretty printing
$jsonData = json_encode(
    $logEntry, 
    JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
);

if( $jsonData === FALSE )
{
    $jsonData = json_encode([
        'timestamp' => $d->format('Y-m-d H:i:s'),
        'message'   => 'Posted data could not be encoded successfully',
    ]);
}

// Ensure the log entry ends with a newline, and add an extra empty line
$logContent = $jsonData . "\n\n";

// Append to the log file
if( file_put_contents( $logFile, $logContent, FILE_APPEND | LOCK_EX ) === FALSE )
{
    error_log("Failed to write to $logFile in abc.php");
}

if( ! is_file( $logFile ) OR ! is_writable( $logFile ) )
{
    error_log("$logFile is not writable in abc.php");
}

// Send a simple response (optional, for AJAX confirmation)
header('Content-Type: application/json');
echo json_encode([
    'status' => 'logged'
]);
ob_flush();
flush();

Also, to make things more complicated, it appears that a WooCommerce session related problem is the root cause of all of this. I say that because of the error messages that the customer sees (or sometimes doesn't see), and the fact that applying a recaptcha error uses the woocommerce_checkout_process action and wc_add_notice doesn't stop the process_checkout method of class-wc-checkout.php from creating the order. That combined with the fact that if we disable the reCaptcha completely, the customer will more than likely just see the "Sorry, your session has expired." error!

Another interesting fact is that I can place infinite orders in our development environment without the problem happening to me. Although the production website is hosting on a Cloudways managed Linode server, the development environment is a Ubuntu server that nearly matches the LAMP stack portion of what Cloudways has. For reference, the Cloudways stack is currently Varnish on top of nginx on top of apache.

Keep in mind that the problem is intermittent. Only about 1 in 10 orders are affected. Why would it just start one day, when we hadn't done any code changes in days? Why doesn't it affect all orders? What should I do now?

本文标签: