UnzerClient._request retries a request whenever it runs into a read timeout:
except (TimeoutError, requests.exceptions.ReadTimeout):
logger.exception("Caught TimeoutError")
continue
The retry does not distinguish between HTTP methods. For a GET that is harmless, but a
timed out POST may well have been carried out by the API -- only the response never made
it back in time. The next attempt then repeats an operation that already happened.
How it shows
Seen with POST /v1/payments/authorize for paylater-installment, whose credit check
regularly takes longer than the former 5 s timeout:
- attempt 1 times out, while the API completes the authorization
- attempt 2 sends the same payload and is rejected with
API.320.200.152 (the basket has
already been used)
_request raises, so the caller sees a failed checkout -- although a successful
authorization exists on the Unzer side
With authorize this leaves an orphaned reservation behind. With charge it would be a
payment the caller does not know about, and a further attempt could charge twice.
The timeout has meanwhile been raised to 30 s, which makes the situation less likely but
does not remove it: any slow response still ends in the same place.
Options to evaluate
- retry only idempotent methods (
GET, HEAD) after a timeout and let the exception
through for POST/PUT/DELETE, so the caller can decide
- the same question applies to the
5xx branch a few lines below, which also continues
- if the API offers an idempotency key, send one and keep retrying as before
- alternatively: on a timed out payment call, look the payment up by
orderId before
giving up, and hand back what is already there
UnzerClient._requestretries a request whenever it runs into a read timeout:The retry does not distinguish between HTTP methods. For a
GETthat is harmless, but atimed out
POSTmay well have been carried out by the API -- only the response never madeit back in time. The next attempt then repeats an operation that already happened.
How it shows
Seen with
POST /v1/payments/authorizeforpaylater-installment, whose credit checkregularly takes longer than the former 5 s timeout:
API.320.200.152(the basket hasalready been used)
_requestraises, so the caller sees a failed checkout -- although a successfulauthorization exists on the Unzer side
With
authorizethis leaves an orphaned reservation behind. Withchargeit would be apayment the caller does not know about, and a further attempt could charge twice.
The timeout has meanwhile been raised to 30 s, which makes the situation less likely but
does not remove it: any slow response still ends in the same place.
Options to evaluate
GET,HEAD) after a timeout and let the exceptionthrough for
POST/PUT/DELETE, so the caller can decide5xxbranch a few lines below, which alsocontinuesorderIdbeforegiving up, and hand back what is already there