Search code examples
pythonpython-requestscontent-disposition

how to set Content-disposition header as attachment for file part?


I am using the Python requests module to send a multi-part HTTP POST request that contains both form-data and a file attachment.

The "Content-disposition" header for each multi-part object is set to "form-data", including the file part.

I need the "Content-disposition" header for the form-data parts to still say "form-data", but the "Content-disposition" header for the file part must say "attachment" and not "form-data".

How do I change the content-disposition header for the file-part only?

My code:

#Python 3.7.3 (default, Apr 24 2019, 13:20:13) [MSC v.1915 32 bit (Intel)]
import requests

#USER PARAMETERS
user_name = 'user_account'
password = 'user_password'
token = '45Hf4xGhj'

#REQUESTS PARAMETERS
url = '192.168.0.2'
headers = {'content-type': 'multi-part/form-data'}
data = {'Username':user_name, 'Password':password, 'Token':token}
files = {'settings': ('settings.xml', open('settings.xml', 'rb'), 'app/xml')}

#POST
response = requests.post(url, headers=headers, data=data, files=files)

This is what the file-part's header looks like with Python requests:

Content-Type: app/xml
Content-Disposition: form-data; name="settings"; filename="settings.xml"

and this is what I need the header of the file-part to look like:

Content-Type: app/xml
Content-Disposition: attachment; name="settings"; filename="settings.xml"

I also tried to change the header by adding a header parameter to the file:

files = {'settings': ('settings.xml', open('settings.xml', 'rb'),
         'app/xml', {'Content-Disposition':'attachment'})}

but that had no effect. I can specify any other custom header and it will add it, but it does not change the "Content-Disposition" header if I use the approach.

Any ideas?


Using the toolbelt:

m = MultipartEncoder( fields={'Username': user_name, 
                              'Password': password, 
                              'Token': token, 
                    'settings': ('settings', open('settings.xml', 'rb'), 
                                 'app/xml', 
                                {'Content-Disposition':'attachment'}
                                )
                             } 
                    ) 

r = requests.post('http://httpbin.org/post', 
                   data=m, 
                   headers={'Content-Type': m.content_type}) 

results in

...--2ba9624051854b6d961bad262a1792fc 
Content-Disposition: form-data; name="settings"; filename="settings"
Content-Type: app/xml 
<?xml version="1.0" encoding="utf-16"?>...

Solution

  • Question: Set Content-disposition header as attachment for file part?

    The short answer: Using python-requests, it's not possible, the way it is implemented now.

    Explanation:

    requests/models.py

    class RequestEncodingMixin(object):
        ...
        def _encode_files(files, data):
            ...
            rf = RequestField(name=k, data=fdata, filename=fn, headers=fh)
            rf.make_multipart(content_type=ft)
    

    Variable fh holdes the 4th tuple item passed from

    files = {'settings': (filename, io.BytesIO(b'some,data,to,send\nanother,row,to,send\n'),
             'app/xml', {'Content-Disposition':'attachment'} )}
    

    The rf.header dict get updated passing headers=fh with 'Content-Disposition':....
    Calling rf.make_multipart(content_type=ft), at the next line, only passing the 3trd tuple item.

    The method make_multipart - urllib3/fields.py is defined as

    def make_multipart(
        self, content_disposition=None, content_type=None, content_location=None
    ):
        self.headers["Content-Disposition"] = content_disposition or u"form-data"
        ...
    

    which replaces self.headers["Content-Disposition"] with the default u"form-data".


    Possible Solutions:

    1. Use only urllib3 there you can do

      rf.make_multipart(content_disposition=fh.get("Content-Disposition"), content_type=ft)
      
    2. File a request to urllib3 and/or python-requests to fix this issue.

    3. Patch yourself, either requests/models.py or urlib3/fields.


    Patch: def make_multipart

    Add only the default Content-Disposition: form-data if not already in self.headers.

    from urllib3 import fields
    
    def make_multipart(
            self, content_disposition=None, content_type=None, content_location=None
        ):
            if self.headers.get("Content-Disposition") is None:
                self.headers["Content-Disposition"] = content_disposition or u"form-data"
    
            self.headers["Content-Disposition"] += u"; ".join(
                [
                    u"",
                    self._render_parts(
                        ((u"name", self._name), (u"filename", self._filename))
                    ),
                ]
            )
            self.headers["Content-Type"] = content_type
            self.headers["Content-Location"] = content_location
    
    fields.RequestField.make_multipart = make_multipart
    

    Resulting multipart:

    --e96a4935b8d5b2355f1da3070faa4b28
    Content-Disposition: attachment; name="settings"; filename="settings.xml"
    Content-Type: app/xml
    
    some,data,to,send
    another,row,to,send
    
    --e96a4935b8d5b2355f1da3070faa4b28--
    

    Tested with Python: 3.5 - urllib3: 1.23 - requests: 2.19.1