Beware of This Race Condition with Stripe Payments March 27, 2018

Last year, we improved nano.js to avoid duplicate XHR requests from being sent to the server in the first place. The introduction of the "run once" flag effectively stops double submissions. Or in the case of a payment form, "run once" stops duplicate charges.

We have made a demo page that shows different locking scenarios:

Check out the demo here

Make sure you switch on the developer tools panel in your browser and watch the XHR requests from being sent. Keep in mind that even an aborted request leads to a committed transaction on the server side.

Imagine this scenario: a web form contains input fields for credit card payment information. A JavaScript function gathers these fields and sends an AJAX (XHR) request to the server. Each time the XHR link is called, the credit card is charged.

In all the charging scenarios, the button is greyed out at the end of a transaction. The demo shows whether multiple charges can take place before the transaction finishes. We have intentionally slowed down the "payment processing" to 2 seconds.

In the first scenario, no protection is provided. This is the default ajxpgn call. The button can be clicked many times before the first transaction concludes:

function charge1(d){

    ajxpgn('transport','ajx_echo.php?wait=2',0,0,null,

        function(){d.disabled='disabled';}

    );

}

Scenario Two places a run-once flag so that subsequent clicks are blocked off:

function charge2(d){

    ajxpgn('transport','ajx_echo.php?wait=2',0,0,null,

        function(){d.disabled='disabled';},

        null, 1

    );

}

Processing a web payment with Stripe however, complicates the matter. For security, no sensitive payment information should ever be sent to the merchant's server directly. Such information is instead sent to Stripe server. A one-time token is returned. We then send the token to the server. Using Stripe Elements, the code looks like this:

var stripe=Stripe('your_public_key');

var elements=stripe.elements();

var card = elements.create('card');

card.mount('#cardinput');

stripe.createToken(card).then(function(res){

  if (!res.error){

    var token=res.token.id;

    ajxpgn(...&token='+token...);

  }

});

In essence, the payment processing request is sent only upon the Stripe token promise succeeds. This is no different than a call back, or a simple timeout as far as concurrency is concerned. We have thus devised the following simulation:

function charge3(d){
    setTimeout(function(){
        ajxpgn('transport','ajx_echo.php?wait=2',

            0,0,null,

            function(){d.disabled='disabled';},

            null,1);
    },4000);
}

Start clicking on the V3 button in a one-second interval. The XHR requests are eventually logged in the console. And there are multiple of them!

The issue here is that the timeout defers the checking of the run-once flag until a later time when the flag could no longer be there. The fact that there is a current transaction scheduled doesn't prevent more calls from being planned.

The ajxpgn call automatically leaves a reqobj attribute on the target container.

function charge4(d){
    if (gid('transport').reqobj!=null) return;

    setTimeout(function(){

        ajxpgn('transport','ajx_echo.php?wait=2',

            0,0,null,

            function(){d.disabled='disabled';},

            null,1);
    },4000);
}

From the demo page we can see this is not sufficient to block duplicate transactions. The code is actually behaving as designed. The lock is in place to prevent concurrent transactions from happening. However, in this case we one a single request to go through. The only time we would need to release the lock is when the first request encountered an error. For example, when the credit card is declined, and the user needs to give a second chance.

Now we place a manual lock:

function charge5(d){
    if (gid('transport').processing) return;

    gid('transport').processing=true;

    setTimeout(function(){

        ajxpgn('transport','ajx_echo.php?wait=2',

            0,0,null,

            function(){d.disabled='disabled';},

            null,1);
    },4000);
}

In reality, this kind of race condition doesn't happen often. Therefore, they can be very elusive to track down. It's always a good idea to place a global lock to prevent both concurrent and subsequent payment calls.

Our Services

Targeted Crawlers

Crawlers for content extraction, restoration and competitive intelligence gathering.

Learn More

Gyroscope™ ERP Solutions

Fully integrated enterprise solutions for rapid and steady growth.

Learn More

E-Commerce

Self-updating websites with product catalog and payment processing.

Learn More