Multipart Upload from ServiceNow

I recently created an integration to upload attachments from ServiceNow to a remote REST web service, that only accepts multipart file uploads. This all needed to happen from within a scoped application, so not all global APIs would be available.

The completed implementation of these scripts is available on ServiceNow Share.

ServiceNow supports inbound multipart uploads, using the Attachment API. However, outbound multipart uploads are not natively supported.

Doing some research to see how others might have solved this problem resulted in finding yet more questions from people who are trying to do similar things with various solutions and amount of success.
The closest I was able to come to a complete solution was some documentation for a class that has been written, but not available to download freely.

Multipart uploads are defined in RFC7578 which tells that a multipart/form-data request body contains a series of parts separated by a boundary. The boundary value must be supplied along with the request, in the "boundary" parameter in the Content-Type header.

At this point, I had an idea of how this integration could be achieved, some documentation of an existing solution, and the knowledge that this was indeed possible within a scoped ServiceNow application.

I started by bootstrapping the function names that I would require in the MultipartHelper class. This allowed me to better understand the script's flow and other components that I would require, here is a snapshot of the script I started with, enumerating the functions I knew I would need.

var MultipartHelper = Class.create();
MultipartHelper.prototype = {
    
    // I will need to initialize some class variables here.
    initialize: function() {
    },
    
    // I need a record to link the temporary attachment to.
    // This is where the loopback API will upload the formatted Multipart attachment.
    setHostRecord: function( grHostRecord ) {
    },
    setHostDetails: function( hostTable, hostSysId ) {
    },
    
    // When adding an attachment we need to specify two things:
    // - The name of the form field that the target is expecting. (ie. field's `name`)
    // - The sys_id of the existing system attachment to add. (this could be on any record)
    addAttachment: function( formName, attachmentId ) {
    },
    
    // A function to perform the generation of the multipart-formatted attachment.
    createBody: function() {
    },
    
    // getContentType returns the content-type string including the boundary value.
    // e.g. multipart/form-data; boundary="xxxxxxxxxxxx"
    getContentType: function() {
    },
    
    // getBodyId returns the sys_id of the generated multipart attachment.
    getBodyId: function() {
    },

    // Once we have sent the body we can delete the temporary attachment.
    deleteBody: function() {
    },
    
    type: 'MultipartHelper'
};

After this I started fleshing the functions out, one at a time, with testing in Xplore at each change to ensure the functions are working as expected.

The main logic of the integration was centered around the loopback API, so I will show the code that handles uploading the non-multipart attachment, and how the API actually encodes the attachment as a multipart form.

The loopback API is a simple scripted REST API running at /api/11117/multipart_helper/get. It takes several parameters in order to generate the attachment's body:

  • attachment_id - The file that needs to be made multipart.
  • boundary_id - The generated boundary ID that should be used for this request.
  • form_name - The name of the field that the target webservice is expecting.

ServiceNow scripted API's support getting a StreamWriter for the request response  using response.getStreamWriter(); within the script field. This allows us to manually create the exact payload that is defined in the MultiPart RFC:

// Retrieve the attachment that needs to be multiparterized.
var attachmentGR = this._getAttachmentGR( attachmentID );
if( attachmentGR ) {
    // Gets the response's StreamWriter.
    var writer = response.getStreamWriter();

    // write the initial boundary to the stream.
    writer.writeString("--" + boundaryValue + "\r\n");

    // set the metadata for this data part. (we only support one attachment currently)
    var fileName = attachmentGR.file_name.toString();
    writer.writeString('Content-Disposition: form-data; name="' + formName + '"; filename="' + fileName + '"\r\n');

    // set the content type for this part of the datastream
    var contentType = attachmentGR.content_type.toString();
    writer.writeString('Content-Type: ' + contentType +'\r\n');
    writer.writeString('\r\n');

    // get the input stream for the sys attachment & stream the file into the datastream
    var inputStream = new GlideSysAttachment().getContentStream( attachmentID );
    writer.writeStream(inputStream);

    // finalize the payload by ending the boundary
    writer.writeString("\r\n--" + boundaryValue + "--\r\n");
}

Once I was able to successfully create the multipart attachment in ServiceNow, uploading it was achieved by using restMessage.setRequestBodyFromAttachment(...); in my POST request that is expecting the multipart attachment.