Bad request in multipart/form-data Mirakl API request

Viewed 608

I am trying to make an API call in Python 3.8.

I have implemented the request in curl and it is as follows:

curl --location --request POST 'https://url.mirakl.net/api/orders/1720178-A/documents' --header 'Authorization: 9xxxxxxxx-4xxx-4xxx-8xxx-xxxxxxxxxxx6' --header 'Accept: application/json' --header 'Content-Type: multipart/form-data' --frm 'files=@"1720178-A.pdf"' --form 'order_documents="<body><order_documents><order_document><file_name>1720178-A.pdf</file_name><type_code>CUSTOMER_INVOICE</type_code> </order_document></order_documents></body>";type=application/xml'

It works, but Python code does not:

    url = "https://url.mirakl.net/api/orders/1720178-A/documents"
    payload = {"order_documents": '<body><order_documents><order_document><file_name>1720178-A.pdf</file_name>'
                                  '<type_code>CUSTOMER_INVOICE</type_code></order_document></order_documents></body>'}
    files = [
        ("files", ('1720178-A.pdf', open('1720178-A.pdf', 'rb')))
    ]

    headers = {
        'Authorization': 'xxxxxxxxx-xxx5-4xxx-xxxx-xxxxxxxxx6',
        'Accept': 'application/json',
        'Content-Type': 'multipart/form-data'
    }

    response = requests.request("POST", url, headers=headers, data=payload, files=files)
    print(response.text)

Returning the following error: {"status":400,"message":"Bad Request"}

The error is obvious in how I'm implementing the request, but I don't know what I'm missing.

I have also tried with json payload with same result:

payload = {"order_documents":[{"file_name":"1720178-A.pdf","type_code":"CUSTOMER_INVOICE"}]}

In my day to day, I use Python for other tasks, but since I'm using it for this one, I want to do it the right way, without relying on running curl from python.

P.S. Looking at the body of the petition I can't see anything wrong.

print(requests.Request("POST", url, headers=headers, files=files, data=payload).prepare().body.decode('US-ASCII', errors='ignore'))
--b54bd40f2809e798c2a04069686c35fc
Content-Disposition: form-data; name="order_documents"

{'file_name': '1720178-A.pdf', 'type_code': 'CUSTOMER_INVOICE'}
--b54bd40f2809e798c2a04069686c35fc
Content-Disposition: form-data; name="files"; filename="1720178-A.pdf"
Content-Type: application/pdf

%PDF-1.7
%
....
....
....
....
%%EOF

--b54bd40f2809e798c2a04069686c35fc--


2 Answers

I have finally found the source of the problem.

To implement the API call (not just one, but several), I downloaded the Postman collection that the retailer makes available to consumers.

What was my failure after checking that the Postman call worked correctly? Using the auto code generation provided by the software. Although it is an implementation for the request library, it sets the content-type header, which causes the error. requests autogenerate it correctly:

{
  "User-Agent": "sitename.app",
  "Accept-Encoding": "gzip, deflate",
  "Accept": "application/json",
  "Connection": "keep-alive",
  "Authorization": "xxx-xxxx-xxx-xxx",
  "Content-Length": "22680",
  "Content-Type": "multipart/form-data; boundary=aea8e12ea4fd0c7d9debb64f69ad0718"
}

In the following screenshot, I show how the collection includes the content type and takes it to the implementation even though it makes use of requests:

enter image description here

I had similar problems very recently with multipart/form sending. Here is how I solved it.

I generated the json part as follows:

import os

data = {
            'title': (None, "testFiles", 'application/json'),
            "short": (None, "testFileShort", 'application/json'),
            "ref_name_1": (None, "TestRef2", 'application/json'),
            "upload_file_1" = (os.path.basename("test.txt"), open("test.txt", 'rb'), 'application/octet-stream')
}

I am not entirely sure, why is it so fuzzy about the json, or if this is the most elegant way, but it works perfectly for me.

And the request code is the following:

import requests

def POST(self, address, json=None, files=None):
    url = self.URL + address
    return requests.post(url=url, json=json, files=files)

try:
    r = httpBackend.POST(
        "/resources/create-files-item",
        files=data)
except Exception:
    return None
Related