【问题标题】:MultiPartParserError :- Invalid boundaryMultiPartParserError :- 无效边界
【发布时间】:2016-03-23 10:09:33
【问题描述】:

我正在尝试使用 Python 请求模块向我的 django rest 应用程序发送一些数据和文件,但出现以下错误。

    raise MultiPartParserError('Invalid boundary in multipart: %s' % boundary)
MultiPartParserError: Invalid boundary in multipart: None

代码:-

import requests
payload={'admins':[
                    {'first_name':'john'
                    ,'last_name':'white'
                    ,'job_title':'CEO'
                    ,'email':'test1@gmail.com'
                    },
                    {'first_name':'lisa'
                    ,'last_name':'markel'
                    ,'job_title':'CEO'
                    ,'email':'test2@gmail.com'
                    }
                    ],
        'company-detail':{'description':'We are a renowned engineering company'
                    ,'size':'1-10'
                    ,'industry':'Engineering'
                    ,'url':'http://try.com'
                    ,'logo':''
                    ,'addr1':'1280 wick ter'
                    ,'addr2':'1600'
                    ,'city':'rkville'
                    ,'state':'md'
                    ,'zip_cd':'12000'
                    ,'phone_number_1':'408-393-254'
                    ,'phone_number_2':'408-393-221'
                    ,'company_name':'GOOGLE'}
        }
files = {'upload_file':open('./test.py','rb')}
import json
headers = {'content-type' : 'application/json'}      
headers = {'content-type' : 'multipart/form-data'}      

#r = requests.post('http://127.0.0.1:8080/api/create-company-profile/',data=json.dumps(payload),headers=headers,files=files)
r = requests.post('http://127.0.0.1:8080/api/create-company-profile/',data=payload,headers=headers,files=files)
print r.status_code
print r.text

Django 代码:-

class CompanyCreateApiView(CreateAPIView):
    parser_classes = (MultiPartParser, FormParser,)
    def post(self, request, *args, **kwargs):
        print 'request ==', request.data

【问题讨论】:

  • 您已经明确添加了多部分标头,它应该有一个 ;boundary=<value> - 因此 Django 失败了。 requests 不会覆盖您的标题,因此使用了不正确的标题。我会让requests 去做,并删除headers = 代码。
  • 我在您的问题中添加了django-rest-framework 标签。希望你不要介意。

标签: python django django-rest-framework python-requests


【解决方案1】:

好的,我忘记了你的标题。根据the spec

Content-Type   = "Content-Type" ":" media-type

MIME 提供了许多“多部分”类型——封装 单个消息正文中的一个或多个实体。 所有多部分类型共享一个通用语法,...并且必须包含一个边界参数作为媒体类型的一部分 价值。

下面是包含 multipart/form-data 的请求的样子:

POST /myapp/company/ HTTP/1.1
Host: localhost:8000
Content-Length: 265
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.9.0
Connection: keep-alive
Content-Type: multipart/form-data; boundary=63c5979328c44e2c869349443a94200e   

--63c5979328c44e2c869349443a94200e
Content-Disposition: form-data; name="hello"

world
--63c5979328c44e2c869349443a94200e
Content-Disposition: form-data; name="mydata"; filename="data.txt"

line 1
line 2
line 3
line 4

--63c5979328c44e2c869349443a94200e--

查看数据部分如何被边界分隔:

--63c5979328c44e2c869349443a94200e--

这个想法是为不太可能出现在数据中的边界使用一些东西。请注意,边界包含在请求的 Content-Type 标头中。

这个请求是由这个代码产生的:

import requests

myfile = {'mydata': open('data.txt','rb')}

r = requests.post(url, 
        #headers = myheaders
        data = {'hello': 'world'}, 
        files = myfile
) 

您似乎仔细注意了 django-rest-framework docs 中的以下注释:

注意:在开发客户端应用程序时,请务必记住确保 在 HTTP 中发送数据时,您正在设置 Content-Type 标头 请求。

如果您不设置内容类型,大多数客户端将默认使用 'application/x-www-form-urlencoded',这可能不是你想要的。

但是当您使用requests 时,如果您自己指定Content-Type 标头,那么requests 假定您知道自己在做什么,并且它不会用@ 覆盖您的Content-Type 标头它会提供 987654332@ 标头。

您没有按要求在 Content-Type 标头中提供边界。你怎么能?您没有组装请求的主体并创建边界来分隔各种数据,因此您不可能知道边界是什么。

django-rest-framework 注释说您应该在请求中包含Content-Type 标头时,真正的意思是:

您或您用于创建请求的任何程序都需要包含 Content-Type 标头。

所以@AChampion 在 cmets 中是完全正确的:让 requests 提供 Content-Type header,毕竟 requests 文档做广告:

Requests 完成了 Python HTTP/1.1 的所有工作

requests 的工作方式如下:如果您提供 files 关键字 arg,则 requests 使用 Content-Type 标头 multipart/form-data 并在标头中指定边界;然后requests 使用边界组装请求的主体。如果您提供 data 关键字参数,则 requests 使用 application/x-www-form-urlencodedContent-Type,它只是将字典中的所有键和值组合成这种格式:

x=10&y=20

不需要边界。

而且,如果您同时提供 files 关键字 arg 和 data 关键字 arg,则 requests 使用 Content-Typemultipart/form-data

【讨论】:

    【解决方案2】:

    上传带参数的文件时:

    1. 不要覆盖标题

    2. 将其他参数与upload_file一起放在files dict中。

      input ={"md5":"xxxx","key":"xxxxx","sn":"xxxx"}
      files = {"pram1":"abc",
               "pram2":json.dumps(input),
               "upload_file": open('/home/gliu/abc', 'rb')}    
      res = requests.post(url, files=files)
      

    【讨论】:

      【解决方案3】:

      我使用来自django-rest-frameworkCreateAPIView 尝试了您的代码。在修复您的代码产生的所有初步错误后,我无法重现边界错误。

      目录结构:

      my_site/ 
           myapp/
               views.py
               serializers.py
               urls.py
               models.py
           mysite/
               settings.py
               urls.py
      

      my_app/views.py:

      from rest_framework.generics import CreateAPIView
      from rest_framework.response import Response
      from rest_framework.parsers import MultiPartParser, FormParser
      
      from myapp.serializers import CompanySerializer 
      from myapp.models import Company
      
      class CompanyCreateApiView(CreateAPIView):
          parser_classes = (MultiPartParser, FormParser,)  #Used to parse the Request.
      
          queryset = Company.objects.all()  #The contents of the Response.
          serializer_class = CompanySerializer  #Determines how the contents of the Response will be converted to json. 
                                                #Required. Defined in myapp/serializers.py
      
          def post(self, request, *args, **kwargs):
              print('data ==', request.data)
              print('myjson ==', request.data["myjson"].read())
              print('mydata ==', request.data["mydata"].read())
      
              queryset = self.get_queryset()
              serializer = CompanySerializer(queryset, many=True)
      
              return Response(serializer.data)
      

      myapp/serializers.py:

      from rest_framework import serializers
      from myapp.models import Company
      
      #Directions for converting a model instance into json:
      class CompanySerializer(serializers.ModelSerializer):
          class Meta:
              model = Company  #The model this serializer applies to.
      
              #By default all fields are converted to json.  
              #To limit which fields should be converted:
              fields = ("name", "email")
              #Or, if its easier you can do this:
              #exclude = ('id',)
      

      myapp/urls.py:

      from django.conf.urls import url
      
      from . import views
      
      urlpatterns = (
          url(r'^company/', views.CompanyCreateApiView.as_view() ),
      )
      

      my_site/urls.py:

      from django.conf.urls import include, url
      from django.contrib import admin
      
      
      urlpatterns = [
          url(r'^admin/', include(admin.site.urls) ),
      
          url(r'^myapp/', include("myapp.urls") ),
      ]
      

      myapp/models.py:

      from django.db import models
      
      # Create your models here.
      
      class Company(models.Model):
          name = models.CharField(max_length=50)
          email = models.CharField(max_length=50)
      
          def __str__(self):
              return "{} {}".format(self.name, self.email)
      

      my_site/settings.py:

      ...
      ...
      DEFAULT_APPS = (
          'django.contrib.admin',
          'django.contrib.auth',
          'django.contrib.contenttypes',
          'django.contrib.sessions',
          'django.contrib.messages',
          'django.contrib.staticfiles',
      )
      
      THIRD_PARTY_APPS = (
          'rest_framework',
      
      )
      
      LOCAL_APPS = (
          'myapp',
      
      )
      
      INSTALLED_APPS = DEFAULT_APPS + THIRD_PARTY_APPS + LOCAL_APPS
      
      REST_FRAMEWORK = {
          # Use Django's standard `django.contrib.auth` permissions,
          # or allow read-only access for unauthenticated users.
          #'DEFAULT_PERMISSION_CLASSES': [
          #    'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
          #]
      }
      
      MIDDLEWARE_CLASSES = (
          'django.contrib.sessions.middleware.SessionMiddleware',
          'django.middleware.common.CommonMiddleware',
          'django.middleware.csrf.CsrfViewMiddleware',
          'django.contrib.auth.middleware.AuthenticationMiddleware',
          'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
          'django.contrib.messages.middleware.MessageMiddleware',
          'django.middleware.clickjacking.XFrameOptionsMiddleware',
          'django.middleware.security.SecurityMiddleware',
      )
          ...
          ...
      

      请注意,我不必禁用 csrf 令牌。

      requests_client.py:

      import requests
      
      import json
      from io import StringIO 
      
      my_dict = {
          'admins': [
                      {'first_name':'john'
                      ,'last_name':'white'
                      ,'job_title':'CEO'
                      ,'email':'test1@gmail.com'
                      },
      
                      {'first_name':'lisa'
                      ,'last_name':'markel'
                      ,'job_title':'CEO'
                      ,'email':'test2@gmail.com'
                      }
      
          ],
          'company-detail': {
                      'description': 'We are a renowned engineering company'
                      ,'size':'1-10'
                      ,'industry':'Engineering'
                      ,'url':'http://try.com'
                      ,'logo':''
                      ,'addr1':'1280 wick ter'
                      ,'addr2':'1600'
                      ,'city':'rkville'
                      ,'state':'md'
                      ,'zip_cd':'12000'
                      ,'phone_number_1':'408-393-254'
                      ,'phone_number_2':'408-393-221'
                      ,'company_name':'GOOGLE'
          }
      }
      
      url = 'http://localhost:8000/myapp/company/'
      
      #StringIO creates a file-like object in memory, rather than on disk:
      
      #python3.4:
      #with StringIO(json.dumps(my_dict)) as json_file, open("data.txt", 'rb') as data_file:
      
      #python2.7:
      with StringIO(json.dumps(my_dict).decode('utf-8')) as json_file, open("data.txt", 'rb') as data_file:
      
          myfiles = [
              ("mydata", ("data.txt", data_file, "text/plain")),
              ("myjson", ("json.json", json_file, "application/json")),
          ]
      
          r = requests.post(url, files=myfiles) 
      
      print(r.status_code)
      print(r.text)
      

      request_client.py 终端窗口中的输出:

      200 
      [{"name":"GE","email":"ge@ge.com"},{"name":"APPL","email":"appl@appl.com"}]
      

      django 服务器窗口中的输出:

      ...
      ...
      Quit the server with CONTROL-C.
      
      data == <QueryDict: {'mydata': [<InMemoryUploadedFile: data.txt (text/plain)>], 
      'myjson': [<InMemoryUploadedFile: json.json (application/json)>]}>
      
      myjson == b'{"admins": [{"first_name": "john", "last_name": "white", "email": "test1@gmail.com", "job_title": "CEO"}, 
      {"first_name": "lisa", "last_name": "markel", "email": "test2@gmail.com", "job_title": "CEO"}], "company-detail": 
      {"description": "We are a renowned engineering company", "phone_number_2": "408-393-221", "phone_number_1": "408-393-254",
       "addr2": "1600", "addr1": "1280 wick ter", "logo": "", "size": "1-10", "city": "rkville", "url": "http://try.com", 
      "industry": "Engineering", "state": "md", "company_name": "GOOGLE", "zip_cd": "12000"}}'
      
      mydata == b'line 1\nline 2\nline 3\nline 4\n'
      
      [18/Dec/2015 13:41:57] "POST /myapp/company/ HTTP/1.1" 200 75
      

      【讨论】:

        【解决方案4】:

        顺便说一句,在开发过程中使用requests 时,阅读 django 大量的 html 错误响应是一种不那么痛苦的方法:

        $ python requests_client.py > error.html  (send output to the file error.html)
        

        然后在您的浏览器中执行:

        File > Open File
        

        并导航到error.html。阅读浏览器中的错误描述,然后找出 django 项目中的问题。

        然后返回终端窗口并按键盘上的向上箭头键返回:

        $ python requests_client.py > error.html 
        

        点击Return,然后刷新显示error.html 的浏览器窗口。根据需要重复。 (不要犯反复刷新浏览器并认为这是发送新请求的错误——您必须再次运行 request_client.py 才能发送新请求)。

        【讨论】:

          【解决方案5】:

          为了使用requests,我必须在我的django项目中禁用csrf tokens,否则发送post请求时会出错:

          CSRF 验证失败。请求中止。

          您正在看到此消息 因为这个站点在提交表单时需要一个 CSRF cookie。这 出于安全原因需要 cookie,以确保您的浏览器 没有被第三方劫持。

          settings.py:

          MIDDLEWARE_CLASSES = (
              'django.contrib.sessions.middleware.SessionMiddleware',
              'django.middleware.common.CommonMiddleware',
              #'django.middleware.csrf.CsrfViewMiddleware',
              'django.contrib.auth.middleware.AuthenticationMiddleware',
              'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
              'django.contrib.messages.middleware.MessageMiddleware',
              'django.middleware.clickjacking.XFrameOptionsMiddleware',
          )
          

          接下来,您的 payload 不是 json 数据。 json 数据是一个字符串,但您的 payload 是一个 python 字典。而且,您不能只在字典周围加上引号,因为 json 数据不能包含单引号 - 您专门使用了单引号。所以,你需要把你的python字典转换成json。

          这是requests 客户端:

          import requests
          import json
          from io import StringIO 
          
          my_dict = {
              'admins': [
                          {'first_name':'john'
                          ,'last_name':'white'
                          ,'job_title':'CEO'
                          ,'email':'test1@gmail.com'
                          },
          
                          {'first_name':'lisa'
                          ,'last_name':'markel'
                          ,'job_title':'CEO'
                          ,'email':'test2@gmail.com'
                          }
          
              ],
              'company-detail': {
                          'description': 'We are a renowned engineering company'
                          ,'size':'1-10'
                          ,'industry':'Engineering'
                          ,'url':'http://try.com'
                          ,'logo':''
                          ,'addr1':'1280 wick ter'
                          ,'addr2':'1600'
                          ,'city':'rkville'
                          ,'state':'md'
                          ,'zip_cd':'12000'
                          ,'phone_number_1':'408-393-254'
                          ,'phone_number_2':'408-393-221'
                          ,'company_name':'GOOGLE'
              }
          }
          
          url = 'http://localhost:8000/myapp/upload/'
          
          
          #StringIO creates a file-like object in memory, rather than on disk:
          with StringIO(json.dumps(my_dict)) as json_file, open("data.txt", 'rb') as data_file:
          
              myfiles = [
                  ("mydata", ("data.txt", data_file, "text/plain")),
                  ("myjson", ("json.json", json_file, "application/json")),
              ]
          
              r = requests.post(url, files=myfiles) 
          
          print(r.text)
          

          请参阅requests 文档的Advanced Usage 部分。

          这里是基于 django 类的视图(虽然不是 Django Rest Framework 类视图):

          from django.shortcuts import render
          from django.http import HttpResponse
          
          # Create your views here.
          
          from django.views.generic import View
          
          from django.views.generic import View
          
          class FileUploadView(View):
              def post(self, request):
                  file_dict = request.FILES
                  print(file_dict)
          
                  for name in file_dict:
                      print(name)
                      print(file_dict[name].read())
                      print('*' * 50)
          
                  return HttpResponse('thanks')
          

          在客户端窗口中输出:

          django186p34)~/python_programs$ python django_client.py 
          thanks
          (django186p34)~/python_programs$ 
          

          django 服务器窗口中的输出:

          ...
          ...
          Quit the server with CONTROL-C.
          
          <MultiValueDict: {'myjson': [<InMemoryUploadedFile: json.json (application/json)>], 
          'mydata': [<InMemoryUploadedFile: data.txt (text/plain)>]}>
          **************************************************
          myjson
          b'{"admins": [{"job_title": "CEO", "last_name": "white", "first_name": "john", "email": "test1@gmail.com"}, 
          {"job_title": "CEO", "last_name": "markel", "first_name": "lisa", "email": "test2@gmail.com"}], "company-detail": 
          {"description": "We are a renowned engineering company", "city": "rkville", "state": "md", "company_name": "GOOGLE", 
          "addr1": "1280 wick ter", "url": "http://try.com", "phone_number_2": "408-393-221", "industry": "Engineering", "logo": "", "addr2": "1600",
           "phone_number_1": "408-393-254", "size": "1-10", "zip_cd": "12000"}}'
          **************************************************
          mydata
          b'line 1\nline 2\nline 3\nline 4\n'
          **************************************************
          [16/Dec/2015 07:34:06] "POST /myapp/upload/ HTTP/1.1" 200 6
          

          显然,requests &lt;--&gt; django mind meld 允许您将 POST 数据与 multipart/form-data 混合,您可以这样做:

          with open('data.txt', 'rb') as f:
              myfile = {'myfile': f}
              r = requests.post(url, data={"hello": "world"}, files=myfile) 
          
          print(r.text)
          

          加上这个观点:

          from django.http import HttpResponse
          from django.views.generic import View
          
          class FileUploadView(View):
              def post(self, request):
                  x = request.POST
                  print(x)
                  print(x["hello"])
          
                  file_dict = request.FILES
                  print(file_dict)
                  print('*' * 50)
          
                  for name in file_dict:
                      print(name)
                      print(file_dict[name].read())
                      print('*' * 50)
          
                  return HttpResponse('thanks')
          

          我在服务器窗口中得到以下输出:

          ...
          ...
          Quit the server with CONTROL-C.
          
          <QueryDict: {'hello': ['world']}>
          world
          <MultiValueDict: {'myfile': [<InMemoryUploadedFile: data.txt ()>]}>
          **************************************************
          myfile
          b'line 1\nline 2\nline 3\nline 4\n'
          **************************************************
          [16/Dec/2015 8:04:17] "POST /myapp/upload/ HTTP/1.1" 200 6
          

          为了在您的字典中允许使用 UTF-8 字符,您在转换为 json 时必须采取一些额外的步骤。见here

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2018-06-30
            • 1970-01-01
            • 2011-01-29
            • 2019-05-07
            • 1970-01-01
            • 2013-09-06
            • 2016-03-17
            • 1970-01-01
            相关资源
            最近更新 更多