How to enforce POST idempotency in DRF?
Question:
I have an API using Django Rest Framework that I’d like to protect against duplicate POST
requests (in the spirit of Post Once Exactly (POE)). The specific scenario I’m trying to handle is:
- Client sends HTTP POST to create object.
- API backend creates the object and commits it to the database.
- Client loses network connectivity.
- API backend tries to send back a success response, but is unable to do
so since the client lost network.
- The client never gets the "success" response, so assumes that the
request fails. Client retries the request, creating a duplicate
object.
There was some discussion about this on the mailing list but no code materialized. How are people solving this problem right now?
Answers:
I solved this by adding support for an X-Idempotency-Key
http header which can be set by the client. I then check for non-idempotent requests using a custom permission class that checks if the idempotency key has been seen recently (in the cache):
class IsIdempotent(permissions.BasePermission):
message = 'Duplicate request detected.'
def has_permission(self, request, view):
if request.method != 'POST':
return True
ival = request.META.get('HTTP_X_IDEMPOTENCY_KEY')
if ival is None:
return True
ival = ival[:128]
key = 'idemp-{}-{}'.format(request.user.pk, ival)
is_idempotent = bool(cache.add(key, 'yes',
settings.IDEMPOTENCY_TIMEOUT))
if not is_idempotent:
logger.info(u'Duplicate request (non-idempotent): %s', key)
return is_idempotent
which I can add to my views like so:
class MyViewSet(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet):
permission_classes = [permissions.IsAuthenticated,
IsIdempotent]
I have an API using Django Rest Framework that I’d like to protect against duplicate POST
requests (in the spirit of Post Once Exactly (POE)). The specific scenario I’m trying to handle is:
- Client sends HTTP POST to create object.
- API backend creates the object and commits it to the database.
- Client loses network connectivity.
- API backend tries to send back a success response, but is unable to do
so since the client lost network. - The client never gets the "success" response, so assumes that the
request fails. Client retries the request, creating a duplicate
object.
There was some discussion about this on the mailing list but no code materialized. How are people solving this problem right now?
I solved this by adding support for an X-Idempotency-Key
http header which can be set by the client. I then check for non-idempotent requests using a custom permission class that checks if the idempotency key has been seen recently (in the cache):
class IsIdempotent(permissions.BasePermission):
message = 'Duplicate request detected.'
def has_permission(self, request, view):
if request.method != 'POST':
return True
ival = request.META.get('HTTP_X_IDEMPOTENCY_KEY')
if ival is None:
return True
ival = ival[:128]
key = 'idemp-{}-{}'.format(request.user.pk, ival)
is_idempotent = bool(cache.add(key, 'yes',
settings.IDEMPOTENCY_TIMEOUT))
if not is_idempotent:
logger.info(u'Duplicate request (non-idempotent): %s', key)
return is_idempotent
which I can add to my views like so:
class MyViewSet(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet):
permission_classes = [permissions.IsAuthenticated,
IsIdempotent]