Search code examples
pythondiscord.pymultipartform-dataaiohttp

Upload multipart/form-data from memory in Python with aiohttp


I am trying to work with Discord.py to upload an attachment to Ballchasing's API. Here are the relevant API sections:

https://discordpy.readthedocs.io/en/latest/api.html#discord.Attachment.read

https://ballchasing.com/doc/api#upload-upload-post

The example in the docs suggest using requests, but I've read over and over again that this isn't best practice for a Discord bot because you want asynchronous code to avoid anything that could block the execution of your script.

Here is what I have:

@commands.Cog.listener()        
async def on_message(self, message):
    headers = {'Authorization':self.upload_key_bc}
    for attachment in message.attachments:
        file = io.BytesIO(await attachment.read())
        action = {'file': ('replay.replay', file.getvalue())}
        async with aiohttp.ClientSession() as session:
            async with session.post(self.api_upload_bc, headers=headers, data=action) as response:
                print(response.status)
                print(await response.text())

I'm getting this response:

failed to get multipart form: request Content-Type isn't multipart/form-data

I tried forcing the Content-Type header to multiparth/form-data and I get a different error:

failed to get multipart form: no multipart boundary param in Content-Type

I think the way I'm sending the data is the problem. What am I missing?


Solution

  • Manually create a FormData object and specify the filename explicitly to enable multipart/form-data.

    If digging into the code, {'file': ('replay.replay', file.getvalue())} is treated as non-special value in FormData and render as str(('replay.replay', file.getvalue()) in urllib.parse.urlencode().

    from aiohttp import FormData
    
    @commands.Cog.listener()
    async def on_message(self, message):
        headers = {'Authorization': self.upload_key_bc}
        for attachment in message.attachments:
            
            formdata = FormData()
            formdata.add_field('file', BytesIO(await attachment.read()), filename='replay.replay')
    
            async with aiohttp.ClientSession() as session:
                async with session.post(self.api_upload_bc, headers=headers, data=formdata) as response:
                    print(response.status)
                    print(await response.text())