9

I have following POST-request form (simplified):

POST /target_page HTTP/1.1  
Host: server_IP:8080
Content-Type: multipart/form-data; boundary=AaaBbbCcc

--AaaBbbCcc
Content-Disposition: form-data; name="json" 
Content-Type: application/json

{ "param_1": "value_1", "param_2": "value_2"}

--AaaBbbCcc
Content-Disposition: form-data; name="file"; filename="..." 
Content-Type: application/octet-stream

<..file data..>
--AaaBbbCcc--

I try to send POST-request with requests:

import requests
import json

file = "C:\\Path\\To\\File\\file.zip"
url = 'http://server_IP:8080/target_page'


def send_request():
    headers = {'Content-type': 'multipart/form-data; boundary=AaaBbbCcc'}

    payload = { "param_1": "value_1", "param_2": "value_2"}

    r = requests.post(url, files={'json': (None, json.dumps(payload), 'application/json'), 'file': (open(file, 'rb'), 'application/octet-stream')}, headers=headers)

    print(r.content)

if __name__ == '__main__':
    send_request()

but it returns status 400 with following comment:

Required request part \'json\' is not present.
The request sent by the client was syntactically incorrect.

Please point on my mistake. What should I change to make it work?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Andersson
  • 51,635
  • 17
  • 77
  • 129
  • You need to indicate `Content-Type: application/json` – hungneox Mar 11 '16 at 12:02
  • 2
    @noctilux: not for a multipart post you don't. – Martijn Pieters Mar 11 '16 at 12:03
  • 1
    Do **not** set the `Content-type` header yourself, leave that to `requests` to generate. – Martijn Pieters Mar 11 '16 at 12:05
  • In http://stackoverflow.com/questions/19439961/python-requests-post-json-and-file-in-single-request is said not to encode the json part as json as a workaround – ralf htp Mar 11 '16 at 12:19
  • See this thread http://stackoverflow.com/questions/9733638/post-json-using-python-requests try in python shell what r.json() prints – ralf htp Mar 11 '16 at 12:48
  • If you are not fixed on *pyrequests* you can use *libcurl* and *PycURL* (http://pycurl.io/docs/latest/). In his thread is a working example for multipart POST with json in cURL: http://stackoverflow.com/questions/29231926/curl-how-to-post-multipart-form-data-data-and-how-to-read-multipart-form-data-in – ralf htp Mar 11 '16 at 12:30
  • @ralfhtp: this isn't about posting *just* JSON. This is a multipart post. The `json` parameter cannot be used for a multipart post, that only applies to posting *just* a JSON payload. – Martijn Pieters Mar 11 '16 at 13:00
  • It may be possible to use *multipart/mixed* (https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html) however it is not relevant if the json is posted as a file (the error message was **'syntactically incorrect'** therefore it is very likely that the MIME type was wrong, see the spec above) – ralf htp Mar 11 '16 at 13:14
  • @ralfhtp, the problem was in boundaries (hardcoded in headers and generated by `requests`) mismatch – Andersson Mar 11 '16 at 13:18
  • See my answer https://stackoverflow.com/a/53544343/639616 – wannik Nov 29 '18 at 17:18

3 Answers3

27

You are setting the header yourself, including a boundary. Don't do this; requests generates a boundary for you and sets it in the header, but if you already set the header then the resulting payload and the header will not match. Just drop you headers altogether:

def send_request():
    payload = {"param_1": "value_1", "param_2": "value_2"}
    files = {
         'json': (None, json.dumps(payload), 'application/json'),
         'file': (os.path.basename(file), open(file, 'rb'), 'application/octet-stream')
    }

    r = requests.post(url, files=files)
    print(r.content)

Note that I also gave the file part a filename (the base name of the file path`).

For more information on multi-part POST requests, see the advanced section of the documentation.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • If anything the fact the O.P. is passing the boundary pattern inside an opaque string, and still expecting the library to reuse that pattern, should be a goog clue that something is wrong. – jsbueno Mar 11 '16 at 13:04
  • Using this code snippet i was able to send request. However at django backend says `AttributeError: 'WSGIRequest' object has no attribute 'data'`. Do you have any idea why? – Khamidulla Oct 02 '18 at 07:45
  • @Khamidulla: that's not a problem with this code. Something on the Django server has an error, there is no `data` attribute on the [Django Request object](https://docs.djangoproject.com/en/2.1/ref/request-response/#httprequest-objects). – Martijn Pieters Oct 02 '18 at 10:32
  • @MartijnPieters I will publish my question and send you link later today. Thank you for you response. – Khamidulla Oct 02 '18 at 12:11
  • hello, is the data supposed to be received via `body` in the server? it's arriving empty, thank you – Scaramouche Apr 04 '20 at 05:06
  • 1
    @Scaramouche: yes, POST data (however it is encoded) is sent in the request body. Replace your URL with `https://httpbin.org/post` to receive a JSON response that echoes what you sent to verify that you are sending a valid request; this could be a server-side issue rather than a Requests issue. – Martijn Pieters Apr 04 '20 at 14:59
  • @MartijnPieters; how this request would look if I have multiple files with one metadata json? – JavaSa Jun 29 '21 at 13:51
  • 1
    @JavaSa: Use a tuple with `(partname, (filename, filedata, content_type))` elements, that way part names don't need to be unique . Do look at the documentation I linked to from my answer, which includes that format. – Martijn Pieters Jul 02 '21 at 17:12
0

In case if someone searches ready to use method to transform python dicts to multipart-form data structures here is a simple gist example to do such transformation:

{"some": ["balls", "toys"], "field": "value", "nested": {"objects": "here"}}
    ->
{"some[0]": "balls", "some[1]": "toys", "field": "value", "nested[objects]": "here"}

To send some data you may want to use the multipartify method from this gist like this:

import requests  # library for making requests

payload = {
    "person": {"name": "John", "age": "31"},
    "pets": ["Dog", "Parrot"],
    "special_mark": 42,
}  # Example payload

requests.post("https://example.com/", files=multipartify(payload))

To send same data along with any file (as OP wanted) you may simply add it like this:

converted_data = multipartify(payload)
converted_data["attachment[0]"] = ("file.png", b'binary-file', "image/png")

requests.post("https://example.com/", files=converted_data)

Note, that attachment is a name defined by server endpoint and may vary. Also attachment[0] indicates that it is first file in you request - this is also should be defined by API documentation.

0

I totally agree with @Martijn's answer... but you can also do this way:

1. Sending request

import json
import requests

cover   = 'superneat.jpg'
payload = {'title': 'The 100 (2014)', 'episodes': json.dumps(_episodes)}
files   = [
            ('json', ('payload.json', json.dumps(payload), 'application/json')),
            ('cover', (cover, open(cover, 'rb')))
          ]
r       = requests.post("https://superneatech.com/store/series", files=files)

print(r.text)

2. Receiving request

You will receive the JSON data as a file, get the content and continue...

Reference: View Here

Superneat
  • 67
  • 3