iwai
Giga Sage

I've successfully submitted a multi-part Rest API, so I'll post an example Script.
This Script is sent using an attachment as a multi-part body.
Attachments are multi-part data that includes text and binary data. Examples include JSON and PDF.

I use "ByteArrayOutputStream" and "GlideSysAttachment.write" to create attachments.
It does not use "MultipartHelper". It only works with the example Script.

To execute the example Script, create the following Script in sys_script_fix (or Script include), rewrite the SYSID to it. (attachmentTableSysid)
Next, rewrite the SYSID of the PDF to be sent. (pdfAttachmentSysid)

Please rewrite and execute according to your instance.

Thank you.

--- Note ---
I've used FlowDesigner's multipart and have failed to create complex transmissions containing text and binaries.  So I used the example Script to send the multipart as a single attachment and it finally succeeded.

// Example RestAPI Multi part body 
var bodyList = [
    // Please set the characters and data you want to send
    '--xXxxxxxxxxxxxxxxxxXx',
    'Content-Type: application/json; charset=UTF-8',
    'Content-Disposition: form-data; name="metadata"',
    '',
    // Example JSON
    JSON.stringify({
        "name": "ExamplePDF.pdf",
        "mimeType": "application/pdf",
        "parents": "folder_id"
    }),
    '',
    '--xXxxxxxxxxxxxxxxxxXx',
    'Content-Type: application/pdf',
    'Content-Disposition: form-data; name="file"',
    '',
    // Example PDF Binary Data
    function(byteOutStrm) {
        var pdfAttachmentSysid = 'ae95a9522f233010828ad6c6f699b638';
        var attaInStrm = new GlideSysAttachmentInputStream(pdfAttachmentSysid);
        attaInStrm.writeTo(byteOutStrm, 0, 0);
    },
    '',
    '--xXxxxxxxxxxxxxxxxxXx--'
];
// Create a byte array output stream
var byteOutStrm = new Packages.java.io.ByteArrayOutputStream();
for (var i = 0; i < bodyList.length; i++) {
    if (typeof bodyList[i] != 'function') {
        var bytes = Packages.java.lang.String('' + bodyList[i] + '\n').getBytes();
        byteOutStrm.write(bytes, 0, bytes.length);
    } else {
        bodyList[i].call(this, byteOutStrm);
    }
}
// Create an attachment from a byte array 
var attachment = new GlideSysAttachment();
// Table for storing work data. (any)
var attachmentTable = 'sys_script_fix';
var attachmentTableSysid = 'd07269162f233010828ad6c6f699b6bd';
var grAttachment = new GlideRecord(attachmentTable);
grAttachment.get(attachmentTableSysid);
var byteArray = byteOutStrm.toByteArray();
/*
# GlideSysAttachment.write
# Create an attachment.
# (GlideRecord, File Name, Content Type, Data)
*/
var attachmentSysID = attachment.write(grAttachment, 'PostBody', 'application/octet-stream', byteArray);
var attachmentLength = byteArray.length;
byteArray = null;

// Example RestAPI Multi part
if (attachmentSysID) {
    var request = new sn_ws.RESTMessageV2();
    request.setRequestHeader("Accept", "Application/json");
    request.setRequestHeader("Content-Type", 'multipart/related; boundary="xXxxxxxxxxxxxxxxxxXx"');
    request.setRequestHeader("Content-length", '' + attachmentLength);
    var authId = 'AccountID';
    var authPassword = 'Password';
    request.setBasicAuth(authId, authPassword);
    request.setHttpMethod('post');

    // simple HTTP Request & Response Service. (https://httpbin.org)
    request.setEndpoint('https://httpbin.org/post');

    // Set Request Body From Attachment
    request.setRequestBodyFromAttachment(attachmentSysID);
    // Save Response Body As Attachment
    request.saveResponseBodyAsAttachment(attachmentTable, attachmentTableSysid, 'ResponseJSON.txt');
    // Sending a request
    var response = request.execute();
    var statusCode = response.getStatusCode();
    gs.info('statusCode ' + statusCode);
}
Comments
Max Thomsson
Tera Contributor

Hi @iwai! I just wanted to let you know that this script works great and was a big help for us. We were setting up an integration towards TopDesk, which required attachments to be sent using multi-part. Your post had all the required parts nicely added together as a working example. Nicely done!

Tobi3
Tera Contributor

It's nearly 2025 and we're still relying on scripts like this..
@iwai Thank you!

SD_Akshay
Giga Guru

 

 

 

MrinalKumaD
Tera Explorer

Hi, I have tons of headers, instead of sending in headers section can I send all the headers in multipart section where provide the body as well?

iwai
Giga Sage
Refactored on 2024-12-04: ServiceNow script for uploading a file to Google Drive

 

 

// Refactored on 2024-12-04: ServiceNow script for uploading a file to Google Drive

/**
 * Note: The `GoogleCloudAPI_AUTH()` function is a proprietary, non-public implementation.
 * It is used internally for obtaining Google Cloud API tokens.
 */

/**
 * Function to obtain Google Drive API access token
 * @returns {string} Access token
 * @throws {Error} Throws an error if access token retrieval fails
 */
function getAccessToken() {
    var responseBody = new global.GoogleCloudAPI_AUTH().requestToken(
        '******', // Service account
        '******', // Google user account
        '******', // Required scopes
        '******' // Token ID
    );
    var responseJson = JSON.parse(responseBody);
    if (responseJson['access_token']) {
        return responseJson['access_token'];
    }
    throw new Error('Failed to retrieve access token');
}

/**
* Function to upload a ServiceNow attachment to Google Drive
*  {string} sysAttachmentSysId - sys_id of the ServiceNow attachment
*  {string} folderId - Google Drive folder ID for the upload destination
* @returns {Object} Response from the Google Drive API after upload
* @throws {Error} Throws an error if the attachment is not found or the API request fails
*/
function uploadFileToGoogleDrive(sysAttachmentSysId, folderId) {
    var accessToken = getAccessToken();

    // Retrieve attachment information from ServiceNow
    var grAttachment = new GlideRecord('sys_attachment');
    if (!grAttachment.get(sysAttachmentSysId)) {
        throw new Error('Specified attachment does not exist');
    }

    // Get the file name and MIME type
    var fileName = grAttachment.getValue('file_name');
    var contentType = grAttachment.getValue('content_type');

    // Construct the multipart request body for Google Drive
    var bodyList = [
        '--xXxxxxxxxxxxxxxxxxXx',
        'Content-Type: application/json; charset=UTF-8',
        '',
        JSON.stringify({
            name: fileName, // File name
            mimeType: contentType, // MIME type
            parents: [folderId], // Google Drive folder ID for upload destination
        }),
        '',
        '--xXxxxxxxxxxxxxxxxxXx',
        'Content-Type: ' + contentType,
        '',
        function (byteOutStr) {
            var attachmentStream = new GlideSysAttachmentInputStream(sysAttachmentSysId);
            attachmentStream.writeTo(byteOutStr, 0, 0);
        },
        '',
        '--xXxxxxxxxxxxxxxxxxXx--',
    ];

    // Build the binary data
    var byteOutStr = new Packages.java.io.ByteArrayOutputStream();
    for (var i = 0; i < bodyList.length; i++) {
        if (typeof bodyList[i] !== 'function') {
            var bytes = Packages.java.lang.String(bodyList[i] + '\r\n').getBytes();
            byteOutStr.write(bytes, 0, bytes.length);
        } else {
            bodyList[i].call(this, byteOutStr);
        }
    }

    // Create a temporary record and attachment
    var attachment = new GlideSysAttachment();
    var tempRecord = new GlideRecord('sys_poll');
    tempRecord.initialize();
    tempRecord.message = 'Temporary attachments for file transfers';
    tempRecord.insert();

    var attachmentSysID = attachment.write(
        tempRecord,
        'PostBody',
        'application/octet-stream',
        byteOutStr.toByteArray()
    );

    byteOutStr.close();

    // Send the request to Google Drive API
    var request = new sn_ws.RESTMessageV2();
    request.setRequestHeader('Accept', 'Application/json');
    request.setRequestHeader('Content-Type', 'multipart/related; boundary="xXxxxxxxxxxxxxxxxxXx"');
    request.setRequestHeader('Authorization', 'Bearer ' + accessToken);
    request.setHttpMethod('POST');
    request.setEndpoint('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&supportsAllDrives=true');
    request.setRequestBodyFromAttachment(attachmentSysID);

    var response = request.execute();
    var statusCode = response.getStatusCode();
    var responseBody = JSON.parse(response.getBody());

    // Remove temporary attachment and record after upload
    try {
        attachment.deleteAttachment(attachmentSysID); // Delete temporary attachment
        tempRecord.deleteRecord(); // Delete temporary record
        gs.info('Temporary attachment and record successfully removed');
    } catch (e) {
        gs.error('Error while deleting temporary files: ' + e.message);
    }

    // Log the upload result
    gs.info('Google Drive upload completed - Status Code: ' + statusCode);
    gs.info('Response: ' + JSON.stringify(responseBody, null, 2));

    return responseBody; // Return the upload result if needed
}

// Example usage
try {
    var result = uploadFileToGoogleDrive('******', '******');
    gs.info('Upload result: ' + JSON.stringify(result, null, 2));
} catch (e) {
    gs.error('Error: ' + e.message);
}

 

SandyNow
Tera Contributor

@iwai Thanks for the refactoring and original script. I am trying to send the attachment file from ServiceNow to 'Manage engine Service Desk Plus system' through outbound REST API and it is expecting 'input_file' parameter with attachment (refer the screenshot from Postman). How can I accommodate 'input_file' parameter in body ? Help is appreciated.  Screenshot 2025-03-05 220953.png

iwai
Giga Sage

@SandyNow , There are two possible ways to include the input_file parameter in the request body, depending on how the API interprets the input_file value. Below are both versions:

 

  • Version 1: Directly Reproducing Key and Value from Postman
    If we strictly follow the Key (input_file) and Value (image10.jpg) as described in your Postman setup, the request body would look like this:
var boundary = 'xXxxxxxxxxxxxxxxxxXx';
var bodyList = [
    '--' + boundary,
    'Content-Disposition: form-data; name="input_file"', // Key: input_file
    '',
    'image10.jpg', // Value: image10.jpg
    '',
    '--' + boundary + '--',
];
  •  Strictly matches the Key and Value from Postman
  •  This sends "image10.jpg" as a text value, rather than the actual file
  •  Use this if the API only expects a file name reference, not an actual file upload

-----------------

  • Version 2: Standard File Upload Format (Common REST API Implementation)
    If the API expects an actual file upload, the request should include filename and file content, following standard multipart/form-data conventions:
var boundary = 'xXxxxxxxxxxxxxxxxxXx';
var bodyList = [
    '--' + boundary,
    'Content-Disposition: form-data; name="input_file"; filename="image10.jpg"', // Key: input_file, Value: actual file
    'Content-Type: application/octet-stream', // Generic MIME type for binary files
    '',
    function (byteOutStr) {
        var attachmentStream = new GlideSysAttachmentInputStream(sysAttachmentSysId);
        attachmentStream.writeTo(byteOutStr, 0, 0);
    },
    '',
    '--' + boundary + '--',
];
  •  Includes filename="image10.jpg" so the API recognizes the file
  •  Includes Content-Type to ensure correct file processing
  •  Sends the actual file content from ServiceNow
  •  Use this if the API expects an actual file to be uploaded

-----------------

Final Recommendation

  • If the API only needs a file name reference, Version 1 is correct.
  • If the API expects a real file upload, Version 2 should be used.
  • If unsure, test both versions to see how the API responds.
SandyNow
Tera Contributor

@iwai Thanks for your response. I have written a BR on Attachment Table and below is my current script (Note : It's in Scoped App) so where can I add you code as I was using ''setRequestBodyFromAttachment()" method.

 

 

var resultGr = new GlideRecord("sn_customerservice_case");
resultGr.addQuery("sys_id", current.table_sys_id);
resultGr.addQuery("account", '<account sys id>'); // running for specific account
resultGr.query();
if (resultGr.next()) {
var caseid = resultGr.sys_id.toString();
gs.info("Case sys id : " + caseid);
try {
var r = new sn_ws.RESTMessageV2('New Integration', 'UploadAttachment');
r.setRequestBodyFromAttachment(current.sys_id);
r.setRequestHeader("Content-Type", resultGr.content_type);
r.setStringParameterNoEscape('request_id', resultGr.correlation_id);

 

var response = r.execute();
var responseBody = response.getBody();
var httpStatus = response.getStatusCode();
gs.info("The value of the POST Response is " + httpStatus + "Body : " + responseBody);
} catch (ex) {
var message = ex.message;
}
}

 

Appreciate your help..

SandyNow
Tera Contributor

@iwai Hello, I'll mostly go with your 2nd recommendation but unsure about the code you suggested to place it in a correct place. Appreciate your response.

iwai
Giga Sage

@SandyNow , To adapt the function for your scenario where the Attachment Table Business Rule is handling the process, and current represents the attachment record, the following changes should be made:

 

Changes to uploadFileToGoogleDrive (Renamed for ManageEngine)

 

  1. Remove the explicit retrieval of the attachment record (var grAttachment = new GlideRecord('sys_attachment'))
    1. Since current already represents the attachment in the Business Rule context, grAttachment = current; should be used instead.
    2. This eliminates the need for sysAttachmentSysId as a function parameter.
  2. Modify references to grAttachment to use current directly
    1. fileName = current.file_name;
    2. contentType = current.content_type;

Corrected Code Section

// No need to retrieve sys_attachment separately, use current directly
var grAttachment = current; // current represents the attachment record in the Business Rule

// Get the file name and MIME type from the current attachment record
var fileName = grAttachment.getValue('file_name');
var contentType = grAttachment.getValue('content_type');

// Construct the multipart request body for ManageEngine ServiceDesk Plus
var bodyList = [
    '--xXxxxxxxxxxxxxxxxxXx',
    'Content-Disposition: form-data; name="input_file"; filename="' + fileName + '"',
    'Content-Type: ' + contentType,
    '',
    function (byteOutStr) {
        var attachmentStream = new GlideSysAttachmentInputStream(current.sys_id);
        attachmentStream.writeTo(byteOutStr, 0, 0);
    },
    '',
    '--xXxxxxxxxxxxxxxxxxXx--',
];

These changes ensure the function works within the Attachment Table Business Rule context without redundant queries.

SandyNow
Tera Contributor

@iwai Thanks. I have modified the code as per your suggestion. But I am getting below error : Evaluator.evaluateString() problem: java.lang.SecurityException: Use of Packages calls is not permitted in scoped applications: com.glide.script.PackageScope.get(PackageScope.java:39)

 

Are you aware about the replacement of Java packages call in scoped app OR any other way to achieve this ?

 

This BR is written in Customer Service Scope.

(function executeRule(current, previous /*null when async*/ ) {

    // Add your code here

    var grAttachment = current; // current represents the attachment record in the Business Rule

    // Get the file name and MIME type from the current attachment record
    var fileName = grAttachment.getValue('file_name');
    var contentType = grAttachment.getValue('content_type');

    // Construct the multipart request body for ManageEngine ServiceDesk Plus
    var bodyList = [
        '--xXxxxxxxxxxxxxxxxxXx',
        'Content-Disposition: form-data; name="input_file"; filename="' + fileName + '"',
        'Content-Type: ' + contentType,
        '',
        function(byteOutStr) {
            var attachmentStream = new GlideSysAttachmentInputStream(current.sys_id);
            attachmentStream.writeTo(byteOutStr, 0, 0);
        },
        '',
        '--xXxxxxxxxxxxxxxxxxXx--',
    ];

    var byteOutStr = new Packages.java.io.ByteArrayOutputStream();
    for (var i = 0; i < bodyList.length; i++) {
        if (typeof bodyList[i] !== 'function') {
            var bytes = Packages.java.lang.String(bodyList[i] + '\r\n').getBytes();
            byteOutStr.write(bytes, 0, bytes.length);
        } else {
            bodyList[i].call(this, byteOutStr);
        }
    }

    // Create a temporary record and attachment
    var attachment = new GlideSysAttachment();
    var tempRecord = new GlideRecord('sys_poll');
    tempRecord.initialize();
    tempRecord.message = 'Temporary attachments for file transfers';
    tempRecord.insert();

    var attachmentSysID = attachment.write(tempRecord, 'PostBody', 'application/octet-stream', byteOutStr.toByteArray());
    byteOutStr.close();

    var resultGr = new GlideRecord("sn_customerservice_case");
    resultGr.addQuery("sys_id", current.table_sys_id);
    resultGr.addQuery("account", 'f67195a7db72fd1036fbb6e4e2961955'); // specific account
    resultGr.query();
    if (resultGr.next()) {
        var fileid = resultGr.sys_id.toString();
        gs.info("Case sys id : " + fileid);

        // Send the request 
        var request = new sn_ws.RESTMessageV2('SDP Integration', 'UploadAttachment');
        request.setRequestHeader('Accept', 'Application/json');
        request.setRequestHeader('Content-Type', 'multipart/related; boundary="xXxxxxxxxxxxxxxxxxXx"');
        requestr.setStringParameterNoEscape('request_id', resultGr.correlation_id);
        request.setRequestBodyFromAttachment(attachmentSysID);

        var response = request.execute();
        var statusCode = response.getStatusCode();
        var responseBody = JSON.parse(response.getBody());

        // Remove temporary attachment and record after upload
        try {
            attachment.deleteAttachment(attachmentSysID); // Delete temporary attachment
            tempRecord.deleteRecord(); // Delete temporary record
            gs.info('Temporary attachment and record successfully removed');
        } catch (e) {
            gs.error('Error while deleting temporary files: ' + e.message);
        }
        gs.info("The value of the POST Response is " + statusCode + "Body : " + responseBody);
        // Log the upload result
        gs.info('upload completed - Status Code: ' + statusCode);
        gs.info('Response: ' + JSON.stringify(responseBody, null, 2));

        return responseBody; // Return the upload result if needed
    }


})(current, previous);

 

iwai
Giga Sage

@SandyNow , The error occurs because Packages.java calls are not permitted in scoped applications. To resolve this, you should create the logic for generating the attachment file in a Global Scope Script Include.

Additionally, ensure that the Script Include is accessible from all scopes. This way, your Scoped Application can call the Global Scope Script, where the necessary processing will take place. Once the process is completed, you can continue handling the data within your scoped table as needed.

https://www.servicenow.com/docs/csh?topicname=c_ScriptIncludes.html&version=latest

SandyNow
Tera Contributor

@iwai Thanks for the direction. I had a doubt, in your script, I see we are creating a record in sys_poll table and deleting it. May I know the strategy behind this ?

iwai
Giga Sage

@SandyNow , 

The sys_poll table is commonly used for exporting processes, where export attachments are temporarily stored. In this case, the process involves sending data via REST API, and the request body needs to be converted into a binary file.

To ensure that the attachment file has an associated record (since attachments cannot exist without a parent record), a temporary sys_poll record is created. The binary data is then attached to this record. Once the data transmission is complete, the temporary record and its attachment are no longer needed, so they are deleted to keep the database clean.

Akshay35
Tera Expert

Hi @iwai ,

 

Thank you for this script. It would be helpful.

 

I need an urgent help on ServiceNow JIRA integration, without Integration Hub I am unable to send MultiPost/Multipart  document to JIRA.

Below is my script, please let me know what corrections needs to be done.

 

Also if any query Parameter needs to be changed.

 

 

var issueKey = '25'; //  Jira Issue
var boundary = 'xXxxxxxxxxxxxxxxxxXx'; // We need to ensure that this matches the boundary in the REST Message headers

// To Get the Incident Record
var incidentSysId = '88b82da29398a2103eccfd277bba1079'; // Actual incident Sys ID
var tableName = 'incident'; // Incident Table

// To Get the attachment from the incident
var grAttachment = new GlideRecord('sys_attachment');
grAttachment.addEncodedQuery('file_nameNOT LIKEPostBody');
grAttachment.addQuery('table_sys_id', incidentSysId);
grAttachment.addQuery('table_name', tableName);
grAttachment.orderByDesc('sys_created_on');
grAttachment.query();

if (!grAttachment.next()) {
    gs.info("No attachment found for the incident: " + incidentSysId);
} else {
    var pdfAttachmentSysid = grAttachment.getValue('sys_id');
    var fileName = grAttachment.getValue('file_name');
    var contentType = grAttachment.getValue('content_type');

    // a ByteArrayOutputStream for multipart data
    var byteOutStrm = new Packages.java.io.ByteArrayOutputStream();

    //  the multipart body
    var bodyList = [
        '--' + boundary,
        'Content-Disposition: form-data; name="file"; filename="' + fileName + '"',
        'Content-Type: ' + contentType,
        '',
        ''
    ];

    //  metadata to byte array
    for (var i = 0; i < bodyList.length; i++) {
        var bytes = Packages.java.lang.String(bodyList[i] + '\r\n').getBytes();
        byteOutStrm.write(bytes, 0, bytes.length);
    }

    //  binary PDF data
    var attaInStrm = new GlideSysAttachmentInputStream(pdfAttachmentSysid);
    attaInStrm.writeTo(byteOutStrm, 0, 0); 

    // Close boundary
    var closingBoundary = '\r\n--' + boundary + '--\r\n';
    var closingBytes = Packages.java.lang.String(closingBoundary).getBytes();
    byteOutStrm.write(closingBytes, 0, closingBytes.length);

    // Convert byte array to attachment in ServiceNow (optional, if needed)
    var byteArray = byteOutStrm.toByteArray();
    var attachment = new GlideSysAttachment();
    var grIncident = new GlideRecord(tableName); // Use the table name as a string
    grIncident.get(incidentSysId);
    var attachmentSysID = attachment.write(grIncident, 'PostBody', 'application/octet-stream', byteArray);

    // Send REST Request to JIRA
    if (attachmentSysID) {
        try {
            var request = new sn_ws.RESTMessageV2('jira_incident', 'Create Attachment');
            gs.info('REST Message initialized successfully.');

            request.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
            // request.setRequestHeader("Accept", "application/json");
            //  request.setStringParameter("issueKey", issueKey);
            request.setRequestBodyFromAttachment(attachmentSysID);
            gs.print('attachmentSysID' + attachmentSysID);

            var response = request.execute();
            var statusCode = response.getStatusCode();
            var responseBody = response.getBody();
            gs.info('Jira Upload Status Code: ' + statusCode);
            gs.info('Jira Upload Response: ' + responseBody);

            if (statusCode === 200) {
                gs.info('Attachment uploaded successfully to JIRA.');
            } else if (statusCode === 400) {
                gs.error('Bad Request: Check the request payload and headers.');
            } else {
                gs.error('Unexpected status code: ' + statusCode);
            }
        } catch (ex) {
            gs.error('Error during Jira attachment upload: ' + ex.message);
        }
    }
}

 

 

Akshay35_0-1742392900133.png

iwai
Giga Sage

Uploading a ServiceNow Attachment to JIRA via REST API

I have created a script to upload an attachment from ServiceNow to a JIRA issue using the REST API. The implementation is based on the following cURL command:

curl -D- -u {username}:{password} -X POST -H "X-Atlassian-Token: nocheck" -F "file=@{path/to/file}" http://{base-url}/rest/api/2/issue/{issue-key}/attachments

 This script retrieves an attachment from the ServiceNow sys_attachment table and posts it to the specified JIRA issue using the JIRA REST API.

/**
 * Function to upload a ServiceNow attachment to JIRA
 * @Param {string} sysAttachmentSysId - sys_id of the ServiceNow attachment
 * @Param {string} issueKey - JIRA issue key where the file should be attached
 * @returns {Object} Response from the JIRA API after upload
 * @throws {Error} Throws an error if the attachment is not found or the API request fails
 */
function uploadFileToJira(sysAttachmentSysId, issueKey) {
    var jiraBaseUrl = "http://{base-url}"; // JIRA
    var jiraUsername = "{username}"; // JIRA
    var jiraPassword = "{password}"; // JIRA

    // Build the request for JIRA API
    var request = new sn_ws.RESTMessageV2();
    request.setHttpMethod('POST');
    request.setEndpoint(jiraBaseUrl + '/rest/api/2/issue/' + issueKey + '/attachments');
    request.setBasicAuth(jiraUsername, jiraPassword);
    request.setRequestHeader('X-Atlassian-Token', 'nocheck');
    request.setRequestHeader('Content-Type', 'multipart/form-data');

    // Retrieve attachment information from ServiceNow
    var grAttachment = new GlideRecord('sys_attachment');
    if (!grAttachment.get(sysAttachmentSysId)) {
        return;
    }

    // Get the file name and MIME type
    var fileName = grAttachment.getValue('file_name');
    var contentType = grAttachment.getValue('content_type');

    // Retrieve the attachment data
    var attachment = new GlideSysAttachment();
    var fileBytes = attachment.getBytes(grAttachment);

 

    // Construct multipart request body
    var boundary = "xXxxxxxxxxxxxxxxxxXx";
    var body = [
        '--' + boundary,
        'Content-Disposition: form-data; name="file"; filename="' + fileName + '"',
        'Content-Type: ' + contentType,
        '',
        function (byteOutStr) {
            byteOutStr.write(fileBytes, 0, fileBytes.length);
        },
        '',
        '--' + boundary + '--'
    ];

    // Build the binary data
    var byteOutStr = new Packages.java.io.ByteArrayOutputStream();
    for (var i = 0; i < body.length; i++) {
        if (typeof body[i] !== 'function') {
            var bytes = Packages.java.lang.String(body[i] + '\r\n').getBytes();
            byteOutStr.write(bytes, 0, bytes.length);
        } else {
            body[i].call(this, byteOutStr);
        }
    }

    // Create a temporary record and attachment
    var tempRecord = new GlideRecord('sys_poll');
    tempRecord.initialize();
    tempRecord.message = 'Temporary attachment for JIRA';
    tempRecord.insert();

    var attachmentSysID = attachment.write(
        tempRecord,
        'PostBody',
        'application/octet-stream',
        byteOutStr.toByteArray()
    );

    byteOutStr.close();

 

I'm sharing this in case it helps others working on similar integrations. While I do my best to contribute as a fellow Community member, I am not in a dedicated support role and may not be able to provide further explanations or modifications to the script.

Community is a place for knowledge sharing and collaboration, not for requesting work. If you need additional tasks or in-depth investigations, please consider making a formal request to my company.

I hope this is helpful!

BhartiY
Tera Explorer

Hi ,

 

We have a requirement where we need to send multiple attachment using multipart ,does this also works for multiple attachments on a single record ?

Do we have anything that works for zip attachments?

 

Thanks,

Bharti

iwai
Giga Sage

@Akshay35 

Hello, 

Based on the information you provided, I gathered various details and came to the conclusion that you are likely trying to communicate using the API at the following URL:
https://developer.atlassian.com/server/jira-servicedesk/rest/v1001/api-group-request-attachment/

 

1. Use the "Attach temporary files" endpoint in Request Attachment to upload a file and obtain a temporaryAttachmentId.
2. Then, use the "Add attachment" endpoint to set the temporaryAttachmentId in a JIRA request, registering the attachment to the request.


This implementation can be achieved by combining the methods I have already posted on this page. Please give it a try.

 

I regularly handle such customizations as part of my professional work. If you are considering this as a business request, feel free to reach out.

iwai
Giga Sage

@BhartiY 

Yes, in ServiceNow, it is possible to send multiple attachments from a single record using multipart in a script. You can also send a ZIP file if needed. To implement this, you will need to clearly define the receiving system’s interface specifications, as well as the structure of the ServiceNow table and records. Based on that, script development, integration design, and proper testing should be carried out.

saikatmitra6919
Tera Contributor

@iwai Would really be helpful if you share more on this. We are also trying to send attachment to TopDesk following your code but everytime we are getting 415 as error. Could you please help.

iwai
Giga Sage

@saikatmitra6919 ,

The HTTP 415 Unsupported Media Type error indicates that the server cannot process the request because the media type is not supported.
This error can occur in several situations, especially when sending files using multipart/form-data:

  1. The overall Content-Type header is missing, incorrect, or lacks the required boundary parameter.
  2. Individual parts of a multipart request have unsupported or incorrectly specified Content-Type headers.
  3. The declared Content-Type for a file does not match the actual content of the file. For example, uploading a PDF file while specifying Content-Type: image/png may trigger a 415 if the server performs MIME-type validation.

To troubleshoot this issue, using the curl command-line tool is highly recommended. With curl, you can:

  • Explicitly set headers and inspect the raw HTTP request.
  • Use the -F option to send files with multipart/form-data and specify the correct MIME type.
  • Determine whether the problem lies in the client-side implementation or the server’s acceptance rules.
  • Confirm whether the server accepts a correctly structured request when sent manually.

Example:

curl -X POST https://example.com/api/upload \

  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \

  -F "file=@/path/to/test.pdf;type=application/pdf"

If the request succeeds with curl, it’s a strong indication that the issue lies within the application’s implementation.

 

SasiChanthati
Giga Guru

Sending a Multi-part REST API Request (Text + Binary) from ServiceNow

This script demonstrates how to successfully send a multi-part/form-data REST API request from ServiceNow, including both JSON metadata and binary (PDF) content as a single body using a custom boundary. This approach avoids MultipartHelper and works directly with a ByteArrayOutputStream.

 

Key Steps Covered:

  • Construct multi-part body manually with ByteArrayOutputStream

  • Insert JSON block as metadata part

  • Attach PDF as binary using GlideSysAttachmentInputStream

  • Convert to byte array, attach it to a record (e.g. sys_script_fix)

  • Send request with sn_ws.RESTMessageV2, using .setRequestBodyFromAttachment()

  • Save response body as an attachment for easy review

Authentication:

Uses Basic Auth (setBasicAuth) for simplicity, but can be adapted for OAuth if needed.

Endpoint Example:

Sent to https://httpbin.org/post for testing purposes.

 

This is particularly useful when standard Flow Designer or out-of-box REST capabilities fall short for complex multipart submissions, especially when mixing structured text and binary files in one call.

saikatmitra6919
Tera Contributor

@iwai  and @SasiChanthati 

 

Thank you for your valuable feedback. I have managed to achieve it using Integration Hub as documented in the below link. Many a thanks for all the suggestions.

 

https://hi.service-now.com/kb_view.do?sysparm_article=KB0745010

 

Version history
Last update:
‎11-03-2021 02:04 AM
Updated by: