Stripe JS making duplicate requests & new requests on state change

Viewed 4425

I'm using the stripe JS library on my frontend and just setting the token, not actually using the library for anything. Odd thing is that when I load any page stripe will make a bunch of odd requests and lots of them are duplicates. Often it'll look like this:

https://m.stripe.com/4
https://m.stripe.com/4
https://stripensrq.global.ssl.fastly.net/s/e
https://stripensrq.global.ssl.fastly.net/s/o
https://m.stripe.com/4

Then if I change the page state using the History API it makes all these calls again even though this is a single page webapp. Is this normal?

3 Answers

This behavior caught me by surprise, too. If you have import { loadStripe } from '@stripe/stripe-js anywhere in your SPA, Stripe will begin phoning home on every page load from the moment your app opens.

Option 1: Deferring Stripe's library load

As of @stripe/stripe-js v1.4.0, you can use the /pure import path, which defers the load of Stripe's library until the app actually calls loadStripe:

import { loadStripe } from '@stripe/stripe-js/pure';

Once you call loadStripe, Stripe will continue sending requests to https://m.stripe.com/4 on every URL change until the browser navigates to a new page through an HTTP request (not through a JavaScript route change) or until the browser reloads.

Option 2: Disabling Stripe's fraud detection mechanisms

stripe.js makes requests to https://m.stripe.com/4 as part of its fraud detection mechanisms. As of @stripe/stripe-js v1.5.0, you can disable this behavior by setting {advancedFraudSignals: false}:

import {loadStripe} from '@stripe/stripe-js/pure';

loadStripe.setLoadParameters({advancedFraudSignals: false})
const stripe = await loadStripe('pk_test_TYooMQauvdEDq54NiTphI7jx');

Note that disabling this feature increases your risk of receiving fraudulent transactions.

More details

I wrote a blog post about this if you're interested in additional details: https://mtlynch.io/stripe-recording-its-customers/

EDIT: since writing this, Stripe have updated their library to address these concerns, in large part thanks to @mtlynch's great investigative work. See his answer above for the most up to date answer.


For those wanting to avoid this, it seems that instead of importing the Stripe library as the docs describe:

import { loadStripe } from "@stripe/stripe-js";

// when wanting to actually load Stripe this method is called,
// but `m.stripe.com` was fired on page load, before this was called
const publicKey = "yourPublicKey";
const stripe = await loadStripe(publicKey);

...which will automatically call the m.stripe.com endpoint, you can dynamically import the library so that this is only called when you actually want Stripe functionality (not on every page, if using redux or vuex, for example):

// create a new async function `stripeJs` that returns the library
const stripeJs = async () => await import("@stripe/stripe-js");
// later, we can call this before we need to use the library
// (e.g. in a vuex/redux action)
// STRIPE's TRACKING SCRIPT WILL BE CALLED NOW, RATHER THAN ON LOAD
const { loadStripe } = await stripeJs();
const publicKey = "yourPublicKey";
const stripe = await loadStripe(publicKey);

// example Stripe call
stripe.redirectToCheckout(...)

Fair warning, I'm not sure what m.stripe.com does, and so there could be unintended side effects to only importing the library before needing to execute it, but this seems to work in my testing.

Agree fully with the other answers here re. importing dynamically.

To take things a step further you can consider running your payment processing in a same-origin frame, which means you can force the library to unload once you're finished with it.

// load this from wherever you want
const html = `
  <!-- your external scripts and styles go here -->
  <script src="https://js.stripe.com/v3/"></script>
  <script>
    const stripe = Stripe('{STRIPE_KEY}');
    /**
     * STRIPE IMPLEMENTATION GOES HERE
     */
    // Once we've got a token we send it back to our app
    stripe.createToken(/*{STRIPE_CARD}*/).then(({ token }) =>
      parent.postMessage({ type: 'stripe-token', token }, '*'));
  </script>
`;

// wait for the frame to load then write our body
iframe.addEventListener('load', () =>
  iframe.contentWindow.document.write(html));
}

// add our iframe to the page and Stripe.js will load (and start pinging its server!)
document.body.append(iframe);

// wait for confirmation from the frame that we're finished with Stripe.js (see the html snippet above)
window.addEventListener('message', ({ data }) => {
  if (data.type === 'stripe-token') {
    console.log(data.token);

    // we've got a token! We can remove our frame (and unload Stripe.js and all its telemetry!)
    if (hasSrcdoc) iframe.srcdoc = '';
    else iframe.src = '';
    document.body.removeChild(iframe);
  }
});

Full details here: https://codepen.io/theprojectsomething/full/ExVNEoZ

Related