Hi!
I've just been discussing a situation devs frequently encounter when developing apps that need to ensure prevention of duplicate charges. In summary, I'm proposing that the idempotency key is hampered because it will return a failed first charge when you try to create a second updated charge with a different source (using the same key).
I'm proposing a success key version of the idempotency key that will only return an error when there is a another successful charge with that same key.
Allow me to explain via the chat thread pasted below:
[18:23] <jonthewayne> quick q - we just discovered that if we create a charge with an idempotency key that fails, and then make another charge with a different source but the same key...that second charge will not try a new charge, it just shows the first bad charge in the logs and returns a stripe error
[18:23] <jonthewayne> is this expected? because it would totally destroy the usefullness of an idempotency key, wouldn't it? shouldn't they only match for previously successful transactions??
[18:56] <praboud> jonthewayne: that's expected behaviour
[18:56] <praboud> why are you reusing idempotency keys for different requests?
[18:57] <praboud> how the idempotency keys work is that if a request comes in to the API, and has an idempotency key that's been seen before, it returns the previous response, and does nothing else
[18:57] <jonthewayne> well, imagine I'm trying to make a charge for an order...I try one charge with the key representing the order....it fails. I should be able to select a different source and retry the charge with the same key...it should go through if a successful charge with that key doesn't exist.
[18:58] <jonthewayne> otherwise, how would I know a charge hasn't gone through before for the order? isn't that the entire point of an idempotency key?
[18:58] <praboud> all idempotency keys are supposed to protect against is the network dropping requests on the floor
[18:58] <praboud> as in: you make a request, and then the network dies
[18:58] <praboud> at that point
[18:59] <praboud> you have no idea if the network died after stripe got the request and made the charge, or before the request got to stripe (so no charge was made)
[18:59] <praboud> the key allows you to retry *exactly* the same request, and know that it won't get executed twice
[19:00] <praboud> it's not meant to solve the situation you're describing
[19:01] <praboud> tbc, you're trying to prevent somebody from clicking the same button twice, and getting charged twice, right?
[19:01] <jonthewayne> right, so if I make the request again, it won't create a new charge. totally get that. I'm arguing that in the case of the first charge failing, it also needs to not match a new charge with that same key
[19:02] <jonthewayne> I think there's a real use case here
[19:02] <praboud> I think the problem you're trying to solve is not quite the problem that idempotent requests solve
[19:02] <jonthewayne> if I store my key in let's say an order parent...and that order needs a charge...I should be able to use the same key until I get one successful charge
[19:03] <praboud> what you want is idempotency on *your* server, not idempotency with the interaction with stripe's api
[19:04] <jonthewayne> I want to store the key on my server in the order...but I want to use that same key until I get a successul charge...
[19:04] <jonthewayne> there needs to be something built into stripe for this exact purpose.
[19:05] <praboud> just so I understand what's going on here, can you describe your workflow a bit more?
[19:05] <jonthewayne> like an idempotency key, but its own thing.
[19:05] <jonthewayne> can I search stripe for a charge with certain meta key/value?
[19:06] <markin> jonthewayne: the dashboard alllows that
[19:06] <jonthewayne> I need it via the api
[19:06] <praboud> I don't believe you can, but that wouldn't really fix your problem, because the check => charge operation is not atomic
[19:07] <praboud> can you please clarify what you're trying to do so I can better understand?
[19:07] <praboud> are multiple http requests coming into your server to cause the charge to be tried with multiple cards?
[19:09] <jonthewayne> Here's more about my situation: basically, I have an order I need to create a charge for. when I create a "charge", I also do some other updates in my system. If any of those fail, it will rollback all local db changes. Now, we can't roll back the stripe charge, but if I was able to pass in a key that was unique to the order, then in the event of an error in my system, I could run the whole charge again with the same key and it would not create a separate charge.
[19:09] <jonthewayne> that's currently how the idempotency key works, and for a successful charge, it fills my needs perfectly.
[19:10] <jonthewayne> but let's say the first charge fails, which raises an error in my code and does a rollback...in that case when I retry the charge with the same key, it won't work because it's grabbing the unsuccessful charge instead of running a new one.
[19:10] <jonthewayne> in this case, it would be wonderful if the key only matched successful transactions.
[19:11] <praboud> there are a couple ways to fix this
[19:11] <praboud> the one most obvious to me is to bump the stored idempotency key when the charge comes back as a 402
[19:11] <praboud> then the next time you attempt the charge, it will actually be attempted
[19:11] <praboud> however
[19:12] <praboud> it sounds like with this system, some other failure could get you in a state where a charge has been made successfully, but the local db changes get rolled back
[19:12] <praboud> this is almost certainly not what you (or your customer) wants
[19:14] <jonthewayne> with good error handling (which I have) that would totally work...thanks for the idea
[19:16] <praboud> getting stuff like this right is unfortunately a bit of a minefield :(
[19:16] <praboud> (unfortunately) I know this by experience
[19:16] == gwendall [~gwendall@2a01:e35:8a6b:bc10:8850:b132:32ab:7bc6] has quit [Remote host closed the connection]
[19:16] <jonthewayne> can you see where my suggestion for the idempotency key on stripe would work? maybe we can have a success idempotency key that only matches for successful transactions. that would make dev lives so much easier
[19:16] == gwendall [~gwendall@2a01:e35:8a6b:bc10:8850:b132:32ab:7bc6] has joined #stripe
[19:17] <praboud> jonthewayne: it's definitely something worth considering, but I'm not certain how many people are in exactly the situation you're in
[19:18] <praboud> (not in that people don't want to prevent dupe charges)
[19:18] <jonthewayne> dude, this is a super common situation....
[19:18] <jonthewayne> it's exactly what you just said...everyone faces the issue of dupe charges
[19:19] <jonthewayne> sure you can wing it, but without a nice success key on your side, it's not perfect
[19:19] <markin> by definition thats how indepotence keys are supposed to work, they return the exact same response
[19:19] <praboud> but that I suspect that most people's systems aren't set up to completely roll back side effects if the charge fails
[19:20] <praboud> most people don't just take the "bomb out and roll back everything" approach - they need to have some kind of error-specific error handling anyway
[19:20] <jonthewayne> right. but if the goal is a successful transaction, which it usually is, then we need to tweak the definition to suit your goal. what am I missing here?
[19:21] == nifjif [~nifjif@2602:306:cf96:fe0:7102:4679:428:f19c] has joined #stripe
[19:21] <jonthewayne> the whole purpose of the key is to not charge multiple times....or at least one of the main purposes.
[19:21] <praboud> I can think of at least a few places where setting up api keys to not count failed responses is absolutely not what you want
[19:21] == gwendall [~gwendall@2a01:e35:8a6b:bc10:8850:b132:32ab:7bc6] has quit [Ping timeout: 272 seconds]
[19:22] <praboud> the one that immediately jumps to mind is synthetic declines when the cvc doesn't match
[19:22] <jonthewayne> allow me then to offcially propose a success key that behaves as I've described. pretty please?? :)
[19:22] <jonthewayne> it'd be a hell of a feature on your end. make it optional of course
[19:23] <praboud> duly noted, although I'm probably the wrong person to request this from
Ok, so what do you all think? Wouldn't this be a great optional property/feature?
Thanks!
-Jon