可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Im trying to send some data and file using Python requests module to my django rest application but get the below error.
raise MultiPartParserError('Invalid boundary in multipart: %s' % boundary)
MultiPartParserError: Invalid boundary in multipart: None
Code:-
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 code:-
class CompanyCreateApiView(CreateAPIView):
parser_classes = (MultiPartParser, FormParser,)
def post(self, request, *args, **kwargs):
print 'request ==', request.data
回答1:
Okay, I forgot about your headers. According to the spec:
Content-Type = "Content-Type" ":" media-type
MIME provides for a number of "multipart" types -- encapsulations of
one or more entities within a single message-body.
All multipart types share a common syntax, ... and MUST include a boundary parameter as part of the media type
value.
Here is what a request containing multipart/form-data looks like:
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--
See how the sections of data are separated by the boundary:
--63c5979328c44e2c869349443a94200e--
The idea is to use something for a boundary that is unlikely to appear in the data. Note that the boundary was included in the Content-Type
header of the request.
That request was produced by this code:
import requests
myfile = {'mydata': open('data.txt','rb')}
r = requests.post(url,
#headers = myheaders
data = {'hello': 'world'},
files = myfile
)
It looks like you were paying careful attention to the following note in the django-rest-framework docs:
Note: When developing client applications always remember to make sure
you're setting the Content-Type header when sending data in an HTTP
request.
If you don't set the content type, most clients will default to using
'application/x-www-form-urlencoded', which may not be what you wanted.
But when you are using requests
, if you specify the Content-Type
header yourself, then requests
assumes that you know what you're doing, and it doesn't overwrite your Content-Type
header with the Content-Type
header it would have provided.
You didn't provide the boundary in your Content-Type
header--as required. How could you? You didn't assemble the body of the request and create a boundary to separate the various pieces of data, so you couldn't possibly know what the boundary is.
When the django-rest-framework
note says that you should include a Content-Type
header in your request, what that really means is:
You or any programs you use to create the request need to include a
Content-Type
header.
So @AChampion was exactly right in the comments: let requests
provide the Content-Type header
, after all the requests
docs advertise:
Requests takes all of the work out of Python HTTP/1.1
requests
works like this: if you provide a files
keyword arg, then requests uses a Content-Type
header of multipart/form-data
and also specifies a boundary in the header; then requests
assembles the body of the request using the boundary. If you provide a data
keyword argument then requests uses a Content-Type
of application/x-www-form-urlencoded
, which just assembles all the keys and values in the dictionary into this format:
x=10&y=20
No boundary required.
And, if you provide both a files
keyword arg and a data
keyword arg, then requests uses a Content-Type
of multipart/form-data
.
回答2:
I tried your code with a CreateAPIView
from the django-rest-framework. After fixing all the preliminary errors that your code produces, I could not reproduce a boundary error.
Directory structure:
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',
)
...
...
Note that I didn't have to disable csrf tokens.
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)
Output in request_client.py terminal window:
200
[{"name":"GE","email":"ge@ge.com"},{"name":"APPL","email":"appl@appl.com"}]
Output in django server window:
...
...
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
回答3:
By the way, here is a less painful way to read django's voluminous html error responses when using requests
while developing:
$ python requests_client.py > error.html (send output to the file error.html)
Then in your browser do:
File > Open File
and navigate to error.html
. Read the error description in your browser, then track down what's wrong in your django project.
Then come back to your terminal window and hit the up arrow key on your keyboard to get back to:
$ python requests_client.py > error.html
Hit Return
, then refresh the browser window that is displaying error.html
. Repeat as necessary. (Don't make the mistake of repeatedly refreshing your browser and thinking that is sending a new request--you have to run request_client.py again to send a new request).
回答4:
In order to use requests
, I had to disable csrf tokens
in my django project, otherwise I got errors when sending post requests:
CSRF verification failed. Request aborted.
You are seeing this message
because this site requires a CSRF cookie when submitting forms. This
cookie is required for security reasons, to ensure that your browser
is not being hijacked by third parties.
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',
)
Next, your payload
is not json data. json data is a string, but your payload
is a python dictionary. And, you can't just wrap quotes around your dictionary because json data cannot contain single quotes--which you used exclusively. So, you need to convert your python dictionary to json.
Here's the requests
client:
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)
See the Advanced Usage section of the requests
docs.
Here it is with a django class based view (albeit not a Django Rest Framework
class view):
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')
Output in client window:
django186p34)~/python_programs$ python django_client.py
thanks
(django186p34)~/python_programs$
Output in django server window:
...
...
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
Apparently, the requests <--> django mind meld
allows you to mix POST data with multipart/form-data, and you can do this:
with open('data.txt', 'rb') as f:
myfile = {'myfile': f}
r = requests.post(url, data={"hello": "world"}, files=myfile)
print(r.text)
Coupled with this view:
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')
I get the following output in the server window:
...
...
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
To allow for UTF-8 characters in your dictionary, you will have to take some extra steps when converting to json. See here.
回答5:
When uploading file with parameters:
Don't overwrite the headers
Put other parameters together with upload_file in 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)