djangorestdjango-formsput

How to parse multipart/form-data from a put request in django


I want to submit a form to my backend and use the form data as the initial value for my form. Simple stuff if you are using a POST request:

def intervals(request, **kwargs):
  form = MyForm(initial=request.POST)

However, I am sending a form that should replace the current resource, which should idiomatically be a PUT request (I am using HTMX which allows you to do that). The problem is that I cannot find out how I can parse the form data from a put request. request.PUT does not exist and QueryDict only works for query params. What am I missing here?


Solution

  • The request.POST is constructed by a method:

    def _load_post_and_files(self):
        """Populate self._post and self._files if the content-type is a form type"""
        if self.method != "POST":
            self._post, self._files = (
                QueryDict(encoding=self._encoding),
                MultiValueDict(),
            )
            return
        if self._read_started and not hasattr(self, "_body"):
            self._mark_post_parse_error()
            return
    
        if self.content_type == "multipart/form-data":
            if hasattr(self, "_body"):
                # Use already read data
                data = BytesIO(self._body)
            else:
                data = self
            try:
                self._post, self._files = self.parse_file_upload(self.META, data)
            except (MultiPartParserError, TooManyFilesSent):
                # An error occurred while parsing POST data. Since when
                # formatting the error the request handler might access
                # self.POST, set self._post and self._file to prevent
                # attempts to parse POST data again.
                self._mark_post_parse_error()
                raise
        elif self.content_type == "application/x-www-form-urlencoded":
            # According to RFC 1866, the "application/x-www-form-urlencoded"
            # content type does not have a charset and should be always treated
            # as UTF-8.
            if self._encoding is not None and self._encoding.lower() != "utf-8":
                raise BadRequest(
                    "HTTP requests with the 'application/x-www-form-urlencoded' "
                    "content type must be UTF-8 encoded."
                )
            self._post = QueryDict(self.body, encoding="utf-8")
            self._files = MultiValueDict()
        else:
            self._post, self._files = (
                QueryDict(encoding=self._encoding),
                MultiValueDict(),
            )
    

    so essentially replace this, but then without the guard for self.method:

    def load_post_and_files(request):
        if request._read_started and not hasattr(request, "_body"):
            request._mark_post_parse_error()
            return
    
        if request.content_type == "multipart/form-data":
            if hasattr(request, "_body"):
                # Use already read data
                data = BytesIO(request._body)
            else:
                data = request
            try:
                return request.parse_file_upload(request.META, data)
            except (MultiPartParserError, TooManyFilesSent):
                # An error occurred while parsing POST data. Since when
                # formatting the error the request handler might access
                # request.POST, set request._post and request._file to prevent
                # attempts to parse POST data again.
                request._mark_post_parse_error()
                raise
        elif request.content_type == "application/x-www-form-urlencoded":
            # According to RFC 1866, the "application/x-www-form-urlencoded"
            # content type does not have a charset and should be always treated
            # as UTF-8.
            if request._encoding is not None and request._encoding.lower() != "utf-8":
                raise BadRequest(
                    "HTTP requests with the 'application/x-www-form-urlencoded' "
                    "content type must be UTF-8 encoded."
                )
            return QueryDict(request.body, encoding="utf-8"), MultiValueDict()
        else:
            return = (
                QueryDict(encoding=request._encoding),
                MultiValueDict(),
            )
    

    and thus work with:

    def intervals(request, **kwargs):
      data, files = load_post_and_files(request)
      form = MyForm(initial=request.POST)
    

    You can also use the Django REST framework [drf-doc], which does the parsing for all HTTP methods by essentially overriding the ._load_post_and_files() method, and then call a method named ._parse(…) [GitHub].