As I see you're improperly set the boundary. You set it in the headers but not tell to requests
library to use custom boundary. Let me show you an example:
>>> import requests
>>> post_url = 'https://api.xero.com/files.xro/1.0/Files/'
>>> files = {'file': open('/tmp/test.txt', 'rb')}
>>> headers = {
... 'Authorization': 'Bearer secret',
... 'Xero-tenant-id': '42',
... 'Accept': 'application/json',
... 'Content-type': 'multipart/form-data; boundary=JLQPFBPUP0',
... 'Content-Length': '1068',
... }
>>> print(requests.Request('POST', post_url, files=files, headers=headers).prepare().body.decode('utf8'))
--f3e21ca5e554dd96430f07bb7a0d0e77
Content-Disposition: form-data; name="file"; filename="test.txt"
--f3e21ca5e554dd96430f07bb7a0d0e77--
As you can see the real boundary (f3e21ca5e554dd96430f07bb7a0d0e77
) is different from what was passed in the header (JLQPFBPUP0
).
You can actually directly use the requests module to controll boundary like this:
Let's prepare a test file:
$ touch /tmp/test.txt
$ echo 'Hello, World!' > /tmp/test.txt
Test it:
>>> import requests
>>> post_url = 'https://api.xero.com/files.xro/1.0/Files/'
>>> files = {'file': open('/tmp/test.txt', 'rb')}
>>> headers = {
... 'Authorization': 'Bearer secret',
... 'Xero-tenant-id': '42',
... 'Accept': 'application/json',
... 'Content-Length': '1068',
... }
>>> body, content_type = requests.models.RequestEncodingMixin._encode_files(files, {})
>>> headers['Content-type'] = content_type
>>> print(requests.Request('POST', post_url, data=body, headers=headers).prepare().body.decode('utf8'))
--db57d23ff5dee7dc8dbab418e4bcb6dc
Content-Disposition: form-data; name="file"; filename="test.txt"
Hello, World!
--db57d23ff5dee7dc8dbab418e4bcb6dc--
>>> headers['Content-type']
'multipart/form-data; boundary=db57d23ff5dee7dc8dbab418e4bcb6dc'
Here boundary is the same as in the header.
Another alternative is using requests-toolbelt
; below example taken from this GitHub issue thread:
from requests_toolbelt import MultipartEncoder
fields = {
# your multipart form fields
}
m = MultipartEncoder(fields, boundary='my_super_custom_header')
r = requests.post(url, headers={'Content-Type': m.content_type}, data=m.to_string())
But it is better not to pass bundary
by hand at all and entrust this work to the requests library.
Update:
A minimal working example using Xero Files API and Python request:
from os.path import abspath
import requests
access_token = 'secret'
tenant_id = 'secret'
filename = abspath('./example.png')
post_url = 'https://api.xero.com/files.xro/1.0/Files'
files = {'filename': open(filename, 'rb')}
values = {'name': 'Xero'}
headers = {
'Authorization': f'Bearer {access_token}',
'Xero-tenant-id': f'{tenant_id}',
'Accept': 'application/json',
}
response = requests.post(
post_url,
headers=headers,
files=files,
data=values
)
assert response.status_code == 201