[BUG-305] Trying to send multipart/form-data

I am trying to integrate Ignition with a knowledgebase software, BookStack, and would like to send it some of the reports that we generate.

Bookstack’s HTTP API allows this, but requires the data comes through as Content-Type of multipart/form-data.

From what I have now read from other forum posts and the Ignition docs, neither system.net.httpPost or system.net.httpClient and the corresponding .post() function do not support multipart/form-data.

Is there a function or a library that I can use to send multipart/form-data?

1 Like

You’re correct that system.net.httpPost and system.net.httpClient with .post() do not support multipart/form-data at this time. We currently have an internal ticket for this, but there is not a specific timeline for when it will be done.

You can use the python 2.7.0 requests module to run an HTTP request for multipart/form-data. It can be downloaded here:
Python 2.7.0 Requests Module: requests · PyPI

To import this module, you would take the “requests” folder from the source distribution and add it to the Ignition install directory and restart the gateway.
For more information on those steps, you can reference this article.

Sorry to reply so late to this, we have added the requests module and restarted the gateway and are getting an
ImportError: No module named urllib3
when trying to import the requests module

The Python requests module requires the urllib3 module and depending on which version you downloaded, you’ll have to add it yourself.
Around version 2.16.0, urllib3 stopped being bundled with the requests module according to their release logs (Community Updates — Requests 2.27.1 documentation).

Based on those release notes, if you download a version prior to that one then you won’t encounter that error as the urlib3 module will be included in the requests folder under a folder named “packages” otherwise you’ll need to downloaded the respective urllib3 module that is supported by the request module as stated in the release notes. In addition to urlib3, you would also require the chardet module to use requests.

As Python 3.4 support ended for requests at 2.22.0, you may run into some issues with any version of requests above that. For your information as well, adding a python module in pylib doesn’t require you to restart the gateway, but you will have to restart any active Designer or Vision Clients to receive any updates to the pylib folder.

Hello, curious if the change that would accept multipart/form-data has been implemented yet. Thanks!

Here is an example function for performing "multipart/form-data" POST (file upload) using the urllib2 library (it uses only built-in libraries, does not require any external packages).

Note that the urllib2 documentation suggests "The Requests package is recommended for a higher-level HTTP client interface" but I was aiming for a solution that did not require import of any external packages, and this works.

This version is hard-coded for a content type of "audio/mpeg" but could be tweaked to use another content type.

Fill disclosure, this code was created with the assistance of ChatGPT after a good amount of coaxing (updated prompts) and manual tweaks.

My specific use case is uploading sound files to an Axis network speaker using it's Media Clip API within Ignition v8.1.28+.

import os
import urllib2
import base64

def upload_file(url, file_path, username, password):
    if not os.path.isfile(file_path):
        print("Error: File not found at {}".format(file_path))
        return

    try:
        # Open the file for reading
        with open(file_path, 'rb') as file_stream:
            file_data = file_stream.read()

        # Prepare the request
        request = urllib2.Request(url)
        boundary = "-----" + hex(hash(file_data))[2:]
        request.add_header("Content-Type", "multipart/form-data; boundary={}".format(boundary))

        # Encode credentials for basic authentication
        credentials = base64.b64encode("{}:{}".format(username, password))
        request.add_header("Authorization", "Basic {}".format(credentials))

        # Build the request body
        body = b''
        body += b"--" + boundary + b"\r\n"
        body += b"Content-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\n".format(os.path.basename(file_path))
        body += b"Content-Type: audio/mpeg\r\n\r\n"
        body += file_data
        body += b"\r\n--" + boundary + b"--\r\n"

        # Set the request method and data
        request.get_method = lambda: 'POST'
        request.add_data(body)

        # Send the request
        response = urllib2.urlopen(request)

        # Print debug information, see https://docs.python.org/2/library/urllib2.html#urllib2.urlopen
        print "** DEBUG INFO - START, comment out in final code **"
        print "** URL:  ",  response.geturl()
        print "** Code: ", response.getcode()
        print "** Info: \n", response.info()
        print "** DEBUG INFO - END\n"

        # Get the response code
        response_code = response.getcode()
        if response_code == 200:
            print("File uploaded successfully.")
        else:
            print("Error: Failed to upload file. Response code: {}".format(response_code))
		
    except Exception as e:
        print("Error: {}".format(e))

Hello bennett, I tried using the above code to add attachments to the jira issues using jira's rest api. I am able add images that are below 50kb where as above 50kb i see the image broken in jira's issue. Could anyone help what's going wrong with the code.

from base64 import b64encode
import os
import urllib2

jira_url = "https://domain-name/rest/api/2/issue/{issueID}/attachments"
username = "email.com"
api_token = "api_token"
auth =b64encode(username +":"+api_token)

file_path = system.file.openFile()
with open(file_path, 'rb') as file_stream:
file_data = file_stream.read()

Prepare the request

request = urllib2.Request(jira_url)
boundary = "----" + hex(hash(file_data))[2:]
request.add_header("Content-Type", "multipart/form-data; boundary={}".format(boundary))

Encode credentials for basic authentication

request.add_header("Authorization", "Basic {}".format(auth))
request.add_header("X-Atlassian-Token","no-check")

Build the request body

body = b''
body += b"--" + boundary + b"\r\n"
body += b"Content-Disposition: form-data; name="file"; filename="{}"\r\n".format(os.path.basename(file_path))
body += b"Content-Type: multipart/form-data;boundary:{}\r\n\r\n".format(boundary)
body += file_data
body += b"\r\n--" + boundary + b"--\r\n"

Set the request method and data

request.get_method = lambda: 'POST'
request.add_data(body)

Send the request

response = urllib2.urlopen(request)
print response.getcode()
print response.info()

Hi @tyler.bennett , Could you please let me know you're opinion on the above code. I am not finding a way to send images that are above 50kb.

Thanks!

For someone in the future... I was able to overcome this problem with a microsoft authentication server by using the Content Type 'x-www-form-urlencoded'.

This may not be a solution for OP's problem, but it worked well in my situation.

Using Postman can help a ton in formatting this string, since it is not as readable as JSON or multipart form data