-
Notifications
You must be signed in to change notification settings - Fork 158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
User reports network failure to load Stripe.js fails again after network is restored #518
Conversation
src/shared.ts
Outdated
.catch((error) => { | ||
throw error; | ||
}) | ||
.then((stripePromise = null)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This essentially removes the cache after each successful call. We should cache success and not cache failures. Did you intend to say .catch((stripePromise = null));
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm using @fruchtose-stripe's suggestion now.
src/shared.ts
Outdated
return stripePromise; | ||
// set stripePromise to null on error | ||
return stripePromise | ||
.catch((error) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this catch redundant? Can it be removed?
src/shared.ts
Outdated
const reloadScript = (params: null | LoadParams): HTMLScriptElement => { | ||
const queryString = | ||
params && !params.advancedFraudSignals ? '?advancedFraudSignals=false' : ''; | ||
const script = [...document.getElementsByTagName('script')].filter( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it possible that we're altering script element that the merchant added themselves here? Is that what we want to be doing? Should we only affect scripts/DOM elements that we added ourselves?
Further, if this is the approach we want, why not use findScript
to find the script?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, I don't think we are since the filtering is specific to https://js.stripe.com/v3
. But either way, I'm using @fruchtose-stripe's suggestion of using the existing script and removing it from it's parent node. Didn't think of that!
src/shared.ts
Outdated
@@ -54,6 +54,23 @@ const injectScript = (params: null | LoadParams): HTMLScriptElement => { | |||
return script; | |||
}; | |||
|
|||
const reloadScript = (params: null | LoadParams): HTMLScriptElement => { | |||
const queryString = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The query param logic should be shared with injectScript
so it gets updated together.
src/shared.ts
Outdated
} else { | ||
// if script exists, but we are reloading due to an error, | ||
// reload script to trigger 'load' event | ||
script = reloadScript(params); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only retrying once right after the failure. Should we implement a backoff mechanism of some kind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's actually not retrying at all unless the user re-calls loadStripe
. We can plan to add automatic retries in the future, but for now, fixing the primary issue is the focus.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this. I have a couple change requests.
src/shared.ts
Outdated
// set stripePromise to null on error | ||
return stripePromise | ||
.catch((error) => { | ||
throw error; | ||
}) | ||
.then((stripePromise = null)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend unsetting the stripePromise
variable in the catch
block. Putting it in a then
block could cause the variable to be unset for a resolved promise.
// set stripePromise to null on error | |
return stripePromise | |
.catch((error) => { | |
throw error; | |
}) | |
.then((stripePromise = null)); | |
// Resets stripePromise on error | |
return stripePromise | |
.catch((error) => { | |
stripePromise = null; | |
return Promise.reject(error); | |
}); |
src/shared.ts
Outdated
} else { | ||
// if script exists, but we are reloading due to an error, | ||
// reload script to trigger 'load' event | ||
script = reloadScript(params); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of querying the DOM for the script again, it would be simpler to remove it from the DOM and then reassign it. Here's my suggestion.
} else { | |
// if script exists, but we are reloading due to an error, | |
// reload script to trigger 'load' event | |
script = reloadScript(params); | |
} else { | |
// If `stripePromise` is unresolved but a script exists, there was an error loading it. | |
// We remove and reload the script anew. | |
script.parentNode.removeChild(script); | |
script = injectScript(params); |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
@maxwelly-stripe @fruchtose-stripe is there still hope of potentially getting this change in? Does someone need to pick it up to get it over the finish line? I'd be more than willing if nobody else is working on this. |
@fxfilmxf yes, i'll pick it back up next week, there was just some internal discussion on whether to additionally include automatic retries, but for now, we'll just address the caching issue and address automatic retries at a later date. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there! Some additional changes are needed.
src/shared.ts
Outdated
script.addEventListener('load', onLoad(resolve, reject)); | ||
|
||
script.addEventListener('error', onError(reject)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmmm, I forgot that resolve
and reject
need to be passed as arguments. You'll need to persist the listeners at the module level alongside the let stripePromise
declaration. E.g.
let stripePromise = /* initializer */;
let onLoadListener; // <- Add type here
let onErrorListener; // <- Add type here
// loadScript
onLoadListener = onLoad(resolve, reject);
onErrorListener = onError(reject);
script.addEventListener('load', onLoadListener);
script.addEventListener('error', onErrorListener;
When calling removeEventListener()
, you would then reference the onLoadListener
and onErrorListener
variables already defined. (And definitely check to make sure they're truthy.) Does that make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay and just to clarify onLoadListener = onLoad(resolve, reject);
the instances of onLoadListener
would stay equivalent even when it's assigned again on retry?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The key here is only assigning onLoadListener
and onErrorListener
immediately before script.addEventListener()
. If you assign "lazily," then they will be successfully removed on the next invocation in case of an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incredible work!
@maxwelly-stripe When is the planned release date for the new version that includes this fix? |
@srakhmanchuk new release should be later today. |
@srakhmanchuk New release should be up if you haven't seen it yet! |
@maxwelly-stripe thank you! |
@maxwelly-stripe Thank you. But I'm afraid only half of the problem was solved. The cache problem for the index.ts
|
@imasalygin yes, you are correct that if you only run |
@maxwelly-stripe if the const stripePromise = Promise.resolve().then(() => {
console.log('try to load');
// return loadScript(null);
return Promise.reject();
});
const loadStripe = () => {
return stripePromise.then(() => {
console.log('init');
});
};
loadStripe();
loadStripe();
loadStripe(); 'try to load' will be printed only once, it means that |
@imasalygin The stripePromise variable is cleared when there's a rejection |
@fxfilmxf no, you show me code of https://github.com/stripe/stripe-js/blob/master/src/index.ts#L15 |
@imasalygin ah yes, you are correct. Apologies for the misunderstanding. The @maxwelly-stripe what are your thoughts? |
@fxfilmxf @imasalygin thanks for flagging this issue! i'll discuss more about this with my team and get back to you as soon as possible! |
Summary & motivation
When stripe-js fails to load, previously we would persist the error even on reload. Now, set
stripePromise = null
on error and if the load script is called again, we retrigger theload
event to attempt to load stripe again.Testing & documentation
I tested the repro stated here and verified that on reload we no longer see a persisting error and stripe gets reloaded properly.