You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
An invalid idempotency key runs the action unprotected instead of raising #51
coordinate() runs the action without any protection when the idempotency key fails validation. A key containing :, a key longer than 255 characters or a whitespace-only key makes _try_reserve catch IdempotencyValidationError (core/services/aio/coordinator.py:273), log it and return None, which _claim treats like a storage failure: the action runs unreserved and nothing is stored. Every retry with that key repeats the effect. An empty string goes through if not self._enabled or not idempotency_key (coordinator.py:134) and runs without even a log record.
The key usually comes straight from a client (Idempotency-Key). A storage outage is the library's problem, and degrading to running the action is a documented choice. An invalid key is the caller's problem: silently dropping protection hides it, and the client believes its retries are safe. The IETF Idempotency-Key draft answers a malformed key with 400.
Proposal
For a non-None key that fails validation, including "", raise IdempotencyValidationError from coordinate() before running the action, so the boundary can map it to 400. Keep None meaning "no key, no protection".
If changing the default is too abrupt, make it an option, and document the current behaviour next to in_flight.
Today the workaround is to validate with IdempotencyIdentifiers before calling coordinate().
Reproduced on master with your script (plus a whitespace-only key): effects=2 for '', 'order:42', 'x' * 256 and ' '.
Plan: coordinate() validates the operation and the key with IdempotencyIdentifiers right after the enabled check and raises IdempotencyValidationError before anything is read, reserved or run. None stays the one way to say "no key". That covers both flows, the reserved one and in_flight="run", which today degrades the same way through the failed read and the rejected SET NX. The decorator's if not idempotency_key becomes is None, so an empty string reaches the coordinator and is refused there instead of skipping it. enabled=False stays a pure pass-through with no validation, as documented.
The non-empty invalid keys are a plain bug: rule 12 on the agents page already says an over-long key raises. The empty string is different, because rule 4 documents it as "no idempotency", and code that does headers.get("Idempotency-Key", "") relies on that. I am still going with your proposal rather than an option: a whitespace-only key raising while an empty one silently disables protection would be an odd line to draw, and an empty Idempotency-Key header is a malformed one. So the PR carries a ! and the migration note is one line: pass None, not "", when there is no key. No org library depends on idempotency-kit, so there is no cap to widen.
Rejected: an opt-in flag (a second way to call the same API, and the default would keep the silent hole), and validating inside the domain service (the coordinator's own check keeps the decision in one place and does not depend on how the service was built). No new metric or log line for the refusal: the exception goes back to the caller, who maps it to 400.
coordinate() checks the operation and any non-None key before it reads, reserves or runs anything, in all three in_flight modes, and the decorator now hands an empty key to the coordinator instead of skipping it. Map IdempotencyValidationError to 400 at the boundary and you have the IETF behaviour; the IdempotencyIdentifiers pre-check is no longer needed.
On your side the one thing to change is "" meaning "no key": pass None. That was documented (rule 4 on the agents page), so the PR carries a ! and the release will be 0.5.0.
Activity
AlexeyShalaev commentedon Oct 16, 2026
Reproduced on master with your script (plus a whitespace-only key):
effects=2for'','order:42','x' * 256and' '.Plan:
coordinate()validates the operation and the key withIdempotencyIdentifiersright after theenabledcheck and raisesIdempotencyValidationErrorbefore anything is read, reserved or run.Nonestays the one way to say "no key". That covers both flows, the reserved one andin_flight="run", which today degrades the same way through the failed read and the rejectedSET NX. The decorator'sif not idempotency_keybecomesis None, so an empty string reaches the coordinator and is refused there instead of skipping it.enabled=Falsestays a pure pass-through with no validation, as documented.The non-empty invalid keys are a plain bug: rule 12 on the agents page already says an over-long key raises. The empty string is different, because rule 4 documents it as "no idempotency", and code that does
headers.get("Idempotency-Key", "")relies on that. I am still going with your proposal rather than an option: a whitespace-only key raising while an empty one silently disables protection would be an odd line to draw, and an emptyIdempotency-Keyheader is a malformed one. So the PR carries a!and the migration note is one line: passNone, not"", when there is no key. No org library depends on idempotency-kit, so there is no cap to widen.Rejected: an opt-in flag (a second way to call the same API, and the default would keep the silent hole), and validating inside the domain service (the coordinator's own check keeps the decision in one place and does not depend on how the service was built). No new metric or log line for the refusal: the exception goes back to the caller, who maps it to 400.
AlexeyShalaev commentedon Oct 16, 2026
#54 is up, as planned. Your script against that branch (plus a whitespace-only key):
coordinate()checks the operation and any non-Nonekey before it reads, reserves or runs anything, in all threein_flightmodes, and the decorator now hands an empty key to the coordinator instead of skipping it. MapIdempotencyValidationErrorto 400 at the boundary and you have the IETF behaviour; theIdempotencyIdentifierspre-check is no longer needed.On your side the one thing to change is
""meaning "no key": passNone. That was documented (rule 4 on the agents page), so the PR carries a!and the release will be 0.5.0.