Django REST framework: method PUT not allowed in ViewSet with def update()

Question:

In DRF, I have a simple ViewSet like this one:

class MyViewSet(viewsets.ViewSet):       

    def update(self, request):
        # do things...
        return Response(status=status.HTTP_200_OK)

When I try a PUT request, I get an error like method PUT not allowed. If I use def put(self, request): all things work fine. Accordingly to the docs I should use def update(): not def put():, why does it happen?

Asked By: floatingpurr

||

Answers:

This is because the APIView has no handler defined for .put() method so the incoming request could not be mapped to a handler method on the view, thereby raising an exception.

(Note: viewsets.ViewSet inherit from ViewSetMixin and APIView)

The dispatch() method in the APIView checks if a method handler is defined for the request method.If the dispatch() method finds a handler for the request method, it returns the appropriate response. Otherwise, it raises an exception MethodNotAllowed.

As per the source code of dispatch() method in the APIView class:

def dispatch(self, request, *args, **kwargs):       
        ...
        ...    
        try:
            self.initial(request, *args, **kwargs)

            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                 # here handler is fetched for the request method
                 # `http_method_not_allowed` handler is assigned if no handler was found
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed 

            response = handler(request, *args, **kwargs) # handler is called here

        except Exception as exc:
            response = self.handle_exception(exc)

        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response

Since .put() method handler is not defined in your view, DRF calls the fallback handler .http_method_not_allowed. This raises an MethodNotAllowed exception.

The source code for .http_method_not_allowed() is:

def http_method_not_allowed(self, request, *args, **kwargs):
    """
    If `request.method` does not correspond to a handler method,
    determine what kind of exception to raise.
    """
    raise exceptions.MethodNotAllowed(request.method) # raise an exception 

Why it worked when you defined .put() in your view?

When you defined def put(self, request): in your view, DRF could map the incoming request method to a handler method on the view. This led to appropriate response being returned without an exception being raised.

Answered By: Rahul Gupta

Had a similar “Method PUT not allowed” issue with this code, because ‘id’ was missing in the request:

class ProfileStep2Serializer(serializers.ModelSerializer):
    class Meta:
        model = Profile
        fields = ('middle_initial', 'mobile_phone', 'address', 'apt_unit_num', 'city', 'state', 'zip')

class Step2ViewSet(viewsets.ModelViewSet):
    serializer_class = ProfileStep2Serializer

    def get_queryset(self):
        return Profile.objects.filter(pk=self.request.user.profile.id)

Turned out that i have missed ‘id’ in the serializer fields, so PUT request was NOT able to provide an id for the record. The fixed version of the serializer is below:

class ProfileStep2Serializer(serializers.ModelSerializer):
    class Meta:
        model = Profile
        fields = ('id', 'middle_initial', 'mobile_phone', 'address', 'apt_unit_num', 'city', 'state', 'zip')
Answered By: Kostyantyn

PUT needs id in URL by default

Sometimes there is the difference between POST and PUT, because PUT needs id in URL
That’s why you get the error: "PUT is not Allowed".

Example:

  • POST: /api/users/
  • PUT: /api/users/1/

Hope it’ll save a lot of time for somebody

Answered By: yestema

This answer is right, Django REST framework: method PUT not allowed in ViewSet with def update(), PUT is not allowed, because DRF expects the instance id to be in the URL. That being said, using this mixin in your ViewSet is probably the best way to fix it (from https://gist.github.com/tomchristie/a2ace4577eff2c603b1b copy pasted below)

class AllowPUTAsCreateMixin(object):
    """
    The following mixin class may be used in order to support PUT-as-create
    behavior for incoming requests.
    """
    def update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        instance = self.get_object_or_none()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)

        if instance is None:
            lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
            lookup_value = self.kwargs[lookup_url_kwarg]
            extra_kwargs = {self.lookup_field: lookup_value}
            serializer.save(**extra_kwargs)
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        serializer.save()
        return Response(serializer.data)

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

    def get_object_or_none(self):
        try:
            return self.get_object()
        except Http404:
            if self.request.method == 'PUT':
                # For PUT-as-create operation, we need to ensure that we have
                # relevant permissions, as if this was a POST request.  This
                # will either raise a PermissionDenied exception, or simply
                # return None.
                self.check_permissions(clone_request(self.request, 'POST'))
            else:
                # PATCH requests where the object does not exist should still
                # return a 404 response.
                raise
Answered By: felix
def update(self, request, pk=None):
    data_in = request.data
    print(data_in)

    instance = self.get_object()
    serializer = self.get_serializer(instance, data=request.data, partial=False)
    serializer.is_valid(raise_exception=True)

    if instance is None:
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
        lookup_value = self.kwargs[lookup_url_kwarg]
        extra_kwargs = {self.lookup_field: lookup_value}
        serializer.save(**extra_kwargs)
        return Response(serializer.data, status=status.HTTP_201_CREATED)
    serializer.save()
    data_out = serializer.data
    return Response(serializer.data)
Answered By: Rodrigo Grossi

Using Django viewsets you can easily map the method in the url e.g.

path('createtoken/', CreateTokenView.as_view({'post': 'create', 'put':'update'}))

Then in your class override the methods as you please:

class CreateTokenView(viewsets.ModelViewSet):
    queryset = yourSet.objects.all()
    serializer_class = yourSerializer

    def create(self, request, *args, **kwargs):
        #any method you want here
        return Response("response")

    def update(self, request, *args, **kwargs):
        # any method you want here
        return Response("response")
Answered By: Josh

I have multiple objects that are working with ModelViewSet and all have different (unique) fields for lookup.

So I came up with another solution for this question by defining put on a parent class and a new field lookup_body_field that can be used to associate payload with existing object:

class CustomViewSet(viewsets.ModelViewSet):
    lookup_body_field = 'id'

    def put(self, pk=None):
        lookup_value = self.request.data.get(self.lookup_body_field)
        if not lookup_value:
            raise ValidationError({self.lookup_body_field: "This field is mandatory"})

        obj = self.get_queryset().filter(**{self.lookup_body_field: lookup_value}).last()
        if not obj:
            return self.create(request=self.request)
        else:
            self.kwargs['pk'] = obj.pk
            return self.update(request=self.request)


class MyViewSetA(CustomViewSet)
    model = ModelA
    lookup_body_field = 'field_a'  # Unique field on ModelA

class MyViewSetB(CustomViewSet)
    model = ModelB
    lookup_body_field = 'field_b'  # Unique field on ModelA


    
Answered By: Egor Wexler

Suppose

you have registered a route like this (in urls.py)

router = DefaultRouter()
router.register(r'users', UserViewSet, basename='user-viewset')
urlpatterns += router.urls

and your restapi routs starts with /api/.

ViewSets generates follow routs.

create a user (send user object in body)
POST
/api/users/

get list of users
GET
/api/users/

get a user
GET
/api/users/{id}/

update a user (full object update)
PUT
/api/users/{id}/

partial update for a user
PATCH
/api/users/{id}/

delete a user
DELETE
/api/users/{id}/

Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.