Maximiliano Padulo
Maximiliano Padulo

Reputation: 1522

django rest framework PUT returns 404 instead of creating an object

I want to be able to create or update an object using the same request. The operation should be idempotent.

Sending a PUT request to DRF work as expected if the object exists but if the object doesn't exists I get a 404 instead of creating it.

models.py:

class Btilog(models.Model):
    md5hash = models.CharField(primary_key=True, max_length=32)
    vteip = models.ForeignKey('vte.VTE')
    timestamp = models.DateTimeField(blank=False)
    source = models.TextField()
    code = models.CharField(max_length=10, blank=False)
    msg = models.TextField(blank=False)

api.py:

class BtilogSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Btilog

class BtilogVSet(viewsets.ModelViewSet):
    queryset = models.Btilog.objects.all()
    serializer_class = BtilogSerializer
    permission_classes = (permissions.AllowAny,)

urls.py:

...
router = routers.DefaultRouter()
router.register(r'btilog', api.BtilogVSet)

urlpatterns = patterns('',
    url(r'^api/', include(router.urls)),
    ...
)

Failing request

http --form PUT http://192.168.10.121:8888/logger/api/btilog/60c6b9e99c43c0bf4d8bc22d671169b1/ vteip='172.25.128.85' 'code'='Test' 'md5hash'='60c6b9e99c43c0bf4d8bc22d671169b1' 'timestamp'='2015-05-31T13:34:01' msg='Test' source='Test'
HTTP/1.0 404 NOT FOUND
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Type: application/json
Date: Mon, 09 Feb 2015 15:16:47 GMT
Server: WSGIServer/0.1 Python/2.7.6
Vary: Accept, Cookie

{
    "detail": "Not found"
    }

As described here: http://restcookbook.com/HTTP%20Methods/put-vs-post/ the correct behaviour of put should be to create the object if it doesn't exists.

The same error occurs using The Browsable API Tool from DRF to make the request. Is the behaviour of DRF also alike? What I'm doing wrong?

Upvotes: 5

Views: 5011

Answers (2)

KenBuckley
KenBuckley

Reputation: 600

Yes, in general with DRF you will create an object using a POST and update an object using PUT. Http PUTs should be idempotent, whereas POSTs are not necessarily so -and POSTs will never be idemponent if you have an automatically created field like a timestamp in the created object. To get the effect the OP wishes above you need to place the create functionality of the POST http method into the PUT method. The issue is that PUTs are mapped to the "update" action only (when using DefaultRouter in urls.py) and the update action does expect the object to exist. So you have to slightly amend the update function (from rest_framework.mixins.UpdateModelMixin) to handle creating objects that do not currently exist.

I am arriving somewhat late to this question so perhaps this may assist someone working on later versions of Django Rest Framework, my version is v3.9.4 .

if you are using a ModelViewSet, then I would suggest inserting the following update function within your views.py file, within your class viewset : It is simply a blend of the DRF´s existing update and create mixins -and you get some extra checking thrown in with those mixins (permission checking, get_serializer_class etc.) Plus it is a bit more portable as it does not contain references to models, - well done to DRF developers (yet again). You will need to import Http404 and ValidationError as shown below.

from django.http import Http404
from rest_framework import status
from rest_framework.exceptions import ValidationError


class BtilogVSet(viewsets.ModelViewSet):
    queryset = models.Btilog.objects.all()
    serializer_class = BtilogSerializer
    permission_classes = (permissions.AllowAny,)        

    def update(self, request, *args, **kwargs):  
        partial = kwargs.pop('partial', False)
        try:
            instance = self.get_object()  #throws a Http404 if instance not found
            serializer = self.get_serializer(instance, data=request.data, partial=partial)
            serializer.is_valid(raise_exception=True)
            self.perform_update(serializer)
            if getattr(instance, '_prefetched_objects_cache', None):
                # If 'prefetch_related' has been applied to a queryset, we need to
                # forcibly invalidate the prefetch cache on the instance.
                instance._prefetched_objects_cache = {}
            return Response(serializer.data)
        except Http404:
            #create the object if it has not been found
            serializer = self.get_serializer(data=request.data)
            serializer.is_valid(raise_exception=True) # will throw ValidationError
            self.perform_create(serializer)
            headers = self.get_success_headers(serializer.data)
            return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
        except ValidationError:  # typically serializer is not valid
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        except:
            raise

Note, PATCH is also mapped to the update() function indirectly via the function partial_update().You don't need to include the partial_update code below, it is supplied by default from the file rest_framework.mixins.UpdateModelMixin, which is a mixin to the ModelViewSet. I show it here for purely illustrative purposes, you do not need to do anything to it.

def partial_update(self, request, *args, **kwargs):
   kwargs['partial'] = True
   return self.update(request, *args, **kwargs)

Upvotes: 0

Matúš Bartko
Matúš Bartko

Reputation: 2457

Well, maybe you should try to overwrite update method inside your modelviewset, which handle the PUT http method:

class BtilogVSet(viewsets.ModelViewSet):
    queryset = models.Btilog.objects.all()
    serializer_class = BtilogSerializer
    permission_classes = (permissions.AllowAny,)

    def update(self, request, *args, **kwargs):
        try:
            instance = Btilog.objects.get(pk=kwargs['pk'])
            serializer = serializers.BtilogSerializer(instance=instance,data=request.data)
            if serializer.is_valid():
                btilog=serializer.save()
                return Response(serializer.data,status=status.HTTP_200_OK)
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        except Btilog.DoesNotExist:
            serializer = serializers.BtilogSerializer(data=request.data)
        if serializer.is_valid():
            btilog=serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Upvotes: 5

Related Questions