Strange Laravel/Stripe/Cartalyst bug where a charge randomly runs twice using the same Stripe token

729 views Asked by At

I've created an Android mobile app which has integrated Stripe to accept card payments. In my Android app, I send to my Laravel API, a bunch of parameters, one of which is a Stripe token.

9 times out of 10, my server handles this as expected. It will create a charge object using Stripe-Cartalyst and create a charge, assuming no exceptions are thrown (such as bad card details, or a server error), my api will then proceed to handle the payment as a success and do some inserts into a DB, after which it will return a 201 HTTP status code back to the client, where I then handle this on the Android side.

The bug (or something I'm doing incorrectly) I've encountered though, happens randomly. Occasionally my API will throw an uncaught exception from Stripe explaining a token (encrypted Stripe card details), can only be used once. I've done some debugging on the Android side and can verify I'm only making 1 HTTP request where I send the token, and have done some Logging on my Laravel API, and have found some strange goings on.

What I've found On the API, I printed some key moments of the process to a log file.

  1. Print the token variable.
  2. When entering the try block to create the charge object.
  3. If 'Delivery' is selected on the Android app (probably a pointless log here).
  4. Checking some items (this is an array passed from the Android app.
  5. At the end of the charge, before returning the 201 back to the client, print a success message of some kind.

I then proceeded to recreate the bug, after about 20/30 orders it happened, and this is the process that occurred:

First, on the Android app, the following token was sent (and printed out to the console);

tok_1DiK0uKIdjSiVG8mn8CV2iim

After checking, this HTTP request was only triggered once, so I don't think it's an Android issue.

Next, on the API log file, the following occurred:

2018-12-17 11:11:56] local.DEBUG: Token: tok_1DiK0uKIdjSiVG8mn8CV2iim [2018-12-17 11:11:56] local.DEBUG: Creating Stripe Charge

[2018-12-17 11:11:59] local.DEBUG: Token: tok_1DiK0uKIdjSiVG8mn8CV2iim

[2018-12-17 11:11:59] local.DEBUG: Creating Stripe Charge

[2018-12-17 11:12:00] local.ERROR: There is currently another in-progress request using this Stripe token (that probably means you clicked twice, and the other charge is still going through): tok_1DiK0uKIdjSiVG8mn8CV2iim. This token can not be used again if that charge is successful.' {"exception":"[object] (Cartalyst\Stripe\Exception\MissingParameterException(code: 400): There is currently another in-progress request using this Stripe token (that probably means you clicked twice, and the other charge is still going through): tok_1DiK0uKIdjSiVG8mn8CV2iim. This token can not be used again if that charge is successful.' at /home/rbfs6nkk73qi/api/prototype/vendor/cartalyst/stripe/src/Exception/Handler.php:123) [stacktrace]

[2018-12-17 11:12:00] local.DEBUG: Delivery order

[2018-12-17 11:12:00] local.DEBUG: Checking single items

[2018-12-17 11:12:00] local.DEBUG: Card delivery - Order placed via Android V0.5 app

This shows that from the steps I listed 1-5, the API did: 1,2,1,2,error,3,4,5.

I'm at a complete loss as to why my API would randomly run twice. I've pasted the relevant parts of my API below, to see if there is something obviously wrong that I'm doing. Appreciate any help on this.

One last thing I've tried: I've tried setting the $token variable to null directly after creating the $stripe charge object, which makes me wonder if this is some Cartalyst bug.

if($card) {
            $token = $request->token; //Stripe token

            Log::debug("Token: ".$token); //Step 1

            try {
                Log::debug("Creating Stripe Charge"); //Step 2
                $stripe->charges()->create([
                    'currency' => $currency,
                    'amount'   => $amount,
                    'source' => $token
                ]);

                if(strcmp($delivery,"Delivery") == 0) {
                    Log::debug("Delivery order"); //Step 3

                    if($singleItems != null) {
                        Log::debug("Checking single items"); //Step 4
                        foreach($singleItems as $key => $value) {
                            $singleItem = DB::table('items')
                                ->select('name','category','price')
                                ->where('item_id', '=', $key)
                                ->get();

                            foreach($singleItem as $item) {
                                DB::table('single_order_items')->insert(['order_number' => $id, 'item'=>$key, 'quantity'=>$value, 'name'=>$item->name, 'category'=>$item->category, 'price'=>$item->price]);
                            }
                        }
                    }

                    Log::debug("Card delivery - ".$description); //Step 5
                    return response("Order placed successfully", 201)
                        ->header('Content-Type', 'text/plain');
                }
               } catch(\Cartalyst\Stripe\Exception\BadRequestException $e) {
                //This exception will be thrown when the data sent through the request is mal formed.
                $message = $e->getMessage();
                Log::debug($message);
                return response($message, 306)
                    ->header('Content-Type', 'text/plain');
            } catch(\Cartalyst\Stripe\Exception\UnauthorizedException $e) {
                //This exception will be thrown if your Stripe API Key is incorrect.
                $message = $e->getMessage();
                Log::debug($message);
                return response($message, 307)
                    ->header('Content-Type', 'text/plain');
            } catch(\Cartalyst\Stripe\Exception\InvalidRequestException $e) {
                //This exception will be thrown whenever the request fails for some reason.
                $message = $e->getMessage();
                Log::debug($message);
                return response($message, 308)
                    ->header('Content-Type', 'text/plain');
            } catch(\Cartalyst\Stripe\Exception\NotFoundException $e) {
                //This exception will be thrown whenever a request results on a 404.
                $message = $e->getMessage();
                Log::debug($message);
                return response($message, 309)
                    ->header('Content-Type', 'text/plain');
            } catch(\Cartalyst\Stripe\Exception\CardErrorException $e) {
                //This exception will be thrown whenever the credit card is invalid.
                $message = $e->getMessage();
                Log::debug($message);
                return response($message, 310)
                    ->header('Content-Type', 'text/plain');
            } catch(\Cartalyst\Stripe\Exception\ServerErrorException $e) {
                //This exception will be thrown whenever Stripe does something wrong.
                $message = $e->getMessage();
                Log::debug($message);
                return response($message, 311)
                    ->header('Content-Type', 'text/plain');
            }
}
1

There are 1 answers

0
JamMan9 On

After further debugging, a solution was found. It turns out the HTTP library I'm using on the Android side called Volley was sending the request more than once.

Setting the Volley request to have 0 retries seems to have solved the issue:

MyStringRequest.setRetryPolicy(new DefaultRetryPolicy(0,DefaultRetryPolicy.DEFAULT_MAX_RETRIES,DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));