Urgent Help Needed: ServiceNow to Jira Attachment Issue (Corrupted Files)

Andrea34
Tera Contributor

Good afternoon everyone, I hope you're doing well.

I'm reaching out for urgent assistance regarding an issue with a business rule that triggers when an attachment is added to a change request in ServiceNow. The rule is supposed to transfer the attachment to the corresponding issue in Jira, but it's not working correctly.

The main problem is that when the attachment is uploaded to Jira, its size doubles, making the file corrupted. I have attempted the following solutions without success:

  • GlideScriptedStream
  • Uint8Array
  • GlideBufferedInputStream
  • Setting Content-Type: application/octet-stream with setRequestBodyFromStream() (throws an error)
  • Using FORM DATA results in a 500 error

I urgently need your help in resolving this issue. Below is my current script:

 

 

 

 

 

(function executeRule(current, previous /*null when async*/) {
    gs.info("La Business Rule se está ejecutando. Adjunto: " + current.file_name);

    if (current.table_name.toLowerCase() == "change_request") {
        var changeTaskGR = new GlideRecord("change_request");

        if (changeTaskGR.get(current.table_sys_id)) {
            var jiraIssueId = changeTaskGR.getValue("u_id_hu");

            if (jiraIssueId) {
                gs.info("Jira Issue ID encontrado: " + jiraIssueId);

                // Enviar solo adjuntos nuevos
                sendNewAttachmentsToJira(
                    current.table_sys_id,
                    jiraIssueId,
                    "change_request",
                    current.sys_created_on
                );
            } else {
                gs.warn("El campo u_nreferens no tiene valor en la change_task con sys_id: " + current.table_sys_id);
            }
        } else {
            gs.warn("No se encontró un registro change_task con sys_id: " + current.table_sys_id);
        }
    }
})(current, previous);

function sendNewAttachmentsToJira(sysId, jiraIssueId, tableName, currentCreatedOn) {
    var attachmentGR = new GlideRecord("sys_attachment");
    attachmentGR.addQuery("table_name", tableName);
    attachmentGR.addQuery("table_sys_id", sysId);
    attachmentGR.addQuery("sys_created_on", ">=", currentCreatedOn);
    attachmentGR.query();

    while (attachmentGR.next()) {
        var fileName = attachmentGR.file_name;
        var contentType = attachmentGR.content_type;
        var attachmentSize = attachmentGR.size_bytes; // Tamaño del archivo

        // 🔍 Obtener el contenido del archivo en formato binario
        var attachmentContent = new GlideSysAttachment().getBytes(attachmentGR);

        if (!attachmentContent || attachmentContent.length === 0) {
            gs.warn("El archivo está vacío o no se pudo leer, no se enviará.");
            continue;
        }

        gs.info("📌 Archivo: " + fileName);
        gs.info("📏 Tamaño original en ServiceNow (bytes): " + attachmentSize);
        gs.info("📏 Tamaño obtenido con getBytes() (bytes): " + attachmentContent.length);

        var boundary = "----WebKitFormBoundary" + gs.generateGUID();
        var newLine = "\r\n";

        //  **Construcción del cuerpo multipart correctamente**
        var requestBody = "";
        requestBody += "--" + boundary + newLine;
        requestBody += 'Content-Disposition: form-data; name="file"; filename="' + fileName + '"' + newLine;
        requestBody += "Content-Type: " + contentType + newLine + newLine;

        var endBoundary = newLine + "--" + boundary + "--" + newLine;

        // 🟢 Crear un flujo de datos binario correctamente
        var combinedRequestBody = new GlideScriptedStream();
        combinedRequestBody.writeString(requestBody);
        combinedRequestBody.writeBytes(attachmentContent);
        combinedRequestBody.writeString(endBoundary);

        // 🔍 Medir el tamaño del cuerpo de la solicitud
        var requestSize = requestBody.length + attachmentContent.length + endBoundary.length;
        gs.info("📏 Tamaño final del request en binario (bytes): " + requestSize);

        var attachmentRequest = new sn_ws.RESTMessageV2();
        attachmentRequest.setHttpMethod("POST");
        attachmentRequest.setEndpoint(
            "https://{user_instance}.atlassian.net/rest/api/3/issue/" +
            jiraIssueId +
            "/attachments"
        );

        var base64Auth = GlideStringUtil.base64Encode(
            "user" + ":" + "user_token"
        );

        attachmentRequest.setRequestHeader("Authorization", "Basic " + base64Auth);
        attachmentRequest.setRequestHeader("X-Atlassian-Token", "no-check");
        attachmentRequest.setRequestHeader("Accept", "application/json");
        attachmentRequest.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary);

        //  **Enviar el archivo en formato binario puro**
        attachmentRequest.setBinaryBody(combinedRequestBody, "multipart/form-data");

        try {
            var attachmentResponse = attachmentRequest.execute();
            var status = attachmentResponse.getStatusCode();
            var responseBody = attachmentResponse.getBody();

            if (status == 200 || status == 201) {
                gs.info(
                    ' Attachment "' + fileName + '" subido exitosamente a Jira issue ID: ' + jiraIssueId
                );
            } else {
                gs.warn("⚠️ No se pudo subir el attachment. Status: " + status + ", Respuesta: " + responseBody);
            }
        } catch (ex) {
            gs.error(" Error al subir el attachment a Jira: " + ex.message);
        }
    }
}

 

 

 

7 REPLIES 7

Carlos Petrucio
Mega Sage

The issue occurs because the attachment's binary data is being handled incorrectly when constructing the multipart/form-data request, which may be causing the file to grow in size and become corrupt when uploaded to Jira.

 

I have adjusted your code to correctly send the binary data to Jira without causing file duplication or corruption.

function sendNewAttachmentsToJira(sysId, jiraIssueId, tableName, currentCreatedOn) {
    var attachmentGR = new GlideRecord("sys_attachment");
    attachmentGR.addQuery("table_name", tableName);
    attachmentGR.addQuery("table_sys_id", sysId);
    attachmentGR.addQuery("sys_created_on", ">=", currentCreatedOn);
    attachmentGR.query();

    while (attachmentGR.next()) {
        var fileName = attachmentGR.file_name;
        var contentType = attachmentGR.content_type;
        var attachmentContent = new GlideSysAttachment().getBytes(attachmentGR);

        if (!attachmentContent || attachmentContent.length === 0) {
            gs.warn("O arquivo está vazio ou não pôde ser lido, não será enviado.");
            continue;
        }

        var boundary = "----WebKitFormBoundary" + gs.generateGUID();
        var newLine = "\r\n";
        var bodyStream = new GlideScriptedStream();

        // **Montando o cabeçalho do multipart**
        var multipartHeader =
            "--" + boundary + newLine +
            'Content-Disposition: form-data; name="file"; filename="' + fileName + '"' + newLine +
            "Content-Type: " + contentType + newLine + newLine;
        
        bodyStream.writeString(multipartHeader);
        bodyStream.writeBytes(attachmentContent);
        bodyStream.writeString(newLine + "--" + boundary + "--" + newLine);

        var attachmentRequest = new sn_ws.RESTMessageV2();
        attachmentRequest.setHttpMethod("POST");
        attachmentRequest.setEndpoint("https://{user_instance}.atlassian.net/rest/api/3/issue/" + jiraIssueId + "/attachments");

        var base64Auth = GlideStringUtil.base64Encode("user" + ":" + "user_token");
        attachmentRequest.setRequestHeader("Authorization", "Basic " + base64Auth);
        attachmentRequest.setRequestHeader("X-Atlassian-Token", "no-check");
        attachmentRequest.setRequestHeader("Accept", "application/json");
        attachmentRequest.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary);

        // **Enviando os dados binários corretamente**
        attachmentRequest.setRequestBodyFromStream(bodyStream, "multipart/form-data; boundary=" + boundary);

        try {
            var attachmentResponse = attachmentRequest.execute();
            var status = attachmentResponse.getStatusCode();
            var responseBody = attachmentResponse.getBody();

            if (status == 200 || status == 201) {
                gs.info(' O anexo "' + fileName + '" foi enviado com sucesso para o Jira (ID: ' + jiraIssueId + ").");
            } else {
                gs.warn(" Falha ao enviar o anexo. Status: " + status + ", Resposta: " + responseBody);
            }
        } catch (ex) {
            gs.error(" Erro ao enviar o anexo para o Jira: " + ex.message);
        }
    }
}

 

If my answer helped you, remember to mark it as the correct answer, this helps everyone who has a similar problem and encourages me to continue.

Thanks !

 

Carlos Petrucio

Hi Carlos, 

 

Hope you are doing well. Thanks for your answer. I tried the solution but the logs shows me the next message:  com.glide.script.RhinoEcmaError: "GlideScriptedStream" is not defined.
sys_script.16bbddfc33f71610ab0585434d5c7bf5.script : Line(48) column(0)
45:
46: var boundary = "----WebKitFormBoundary" + gs.generateGUID();
47: var newLine = "\r\n";
==> 48: var bodyStream = new GlideScriptedStream();
49:
50: // **Montando o cabeçalho do multipart**
51: var multipartHeader =   

 

Can you help me please? 

The version of the adjusted code that I sent you was based on what you already used, but it seems that this does not exist within the scope that you are working with, try using the code below:

 

 

function sendNewAttachmentsToJira(sysId, jiraIssueId, tableName, currentCreatedOn) {
    var attachmentGR = new GlideRecord("sys_attachment");
    attachmentGR.addQuery("table_name", tableName);
    attachmentGR.addQuery("table_sys_id", sysId);
    attachmentGR.addQuery("sys_created_on", ">=", currentCreatedOn);
    attachmentGR.query();

    while (attachmentGR.next()) {
        var fileName = attachmentGR.file_name;
        var contentType = attachmentGR.content_type;
        var attachmentSysId = attachmentGR.sys_id;
        var attachmentContent = new GlideSysAttachment().getBytes(attachmentSysId);

        if (!attachmentContent || attachmentContent.length === 0) {
            gs.warn("O arquivo está vazio ou não pôde ser lido, não será enviado.");
            continue;
        }

        var boundary = "----WebKitFormBoundary" + gs.generateGUID();
        var newLine = "\r\n";

        // Criando o corpo multipart
        var ByteArrayOutputStream = Packages.java.io.ByteArrayOutputStream;
        var bodyStream = new ByteArrayOutputStream();

        // **Montando o cabeçalho multipart**
        var multipartHeader = "--" + boundary + newLine;
        multipartHeader += 'Content-Disposition: form-data; name="file"; filename="' + fileName + '"' + newLine;
        multipartHeader += "Content-Type: " + contentType + newLine + newLine;

        // **Escrevendo os dados no stream**
        bodyStream.write(multipartHeader.getBytes());
        bodyStream.write(attachmentContent);
        bodyStream.write((newLine + "--" + boundary + "--" + newLine).getBytes());

        // Convertendo para um array de bytes
        var requestBody = bodyStream.toByteArray();

        var attachmentRequest = new sn_ws.RESTMessageV2();
        attachmentRequest.setHttpMethod("POST");
        attachmentRequest.setEndpoint("https://{user_instance}.atlassian.net/rest/api/3/issue/" + jiraIssueId + "/attachments");

        var base64Auth = GlideStringUtil.base64Encode("user" + ":" + "user_token");
        attachmentRequest.setRequestHeader("Authorization", "Basic " + base64Auth);
        attachmentRequest.setRequestHeader("X-Atlassian-Token", "no-check");
        attachmentRequest.setRequestHeader("Accept", "application/json");
        attachmentRequest.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary);

        // **Enviando os dados binários corretamente**
        attachmentRequest.setRequestBody(requestBody);

        try {
            var attachmentResponse = attachmentRequest.execute();
            var status = attachmentResponse.getStatusCode();
            var responseBody = attachmentResponse.getBody();

            if (status == 200 || status == 201) {
                gs.info('O anexo "' + fileName + '" foi enviado com sucesso para o Jira (ID: ' + jiraIssueId + ").");
            } else {
                gs.warn("Falha ao enviar o anexo. Status: " + status + ", Resposta: " + responseBody);
            }
        } catch (ex) {
            gs.error("Erro ao enviar o anexo para o Jira: " + ex.message);
        }
    }
}

If it doesn't work, help me understand better what you need to do is take a ServiceNow attachment and pass it to another API?

Does this API accept any type of file?

What data do you need to send from the file?

I see that you speak Spanish, if you want you can write in Spanish and I can help you!

Hola Carlos, 

 

Espero te encuentres bien. Te explico un poco mejor que es lo que deseo hacer:

 

Deseo que, cada vez que un adjunto sea subido a un change request, esta sea enviado a su respectivo issue asociado en jira. Como te comenté, he intentado un montón de soluciones y todas dan error o, si logro subir los archivos, se suben corruptos y con el doble de peso. La primero solución que me compartiste me indico el error que te dije en mi respuesta anterior y esta nueva solución se detiene en la construcción del cuerpo, es decir, no envia la solicitud hacía jira. Te comparto el código que acabo de usar: 

(function executeRule(current, previous /*null when async*/) {
    gs.info("📌 Business Rule ejecutándose. Adjunto: " + current.file_name);

    if (current.table_name.toLowerCase() == "change_request") {
        var changeTaskGR = new GlideRecord("change_request");

        if (changeTaskGR.get(current.table_sys_id)) {
            var jiraIssueId = changeTaskGR.getValue("u_id_hu");

            if (jiraIssueId) {
                gs.info(" Jira Issue ID encontrado: " + jiraIssueId);

                // 🔥 Enviar adjuntos nuevos a Jira
                sendNewAttachmentsToJira(
                    current.table_sys_id,
                    jiraIssueId,
                    "change_request",
                    current.sys_created_on
                );
            } else {
                gs.warn("⚠️ No se encontró un Jira Issue ID en la change_request con sys_id: " + current.table_sys_id);
            }
        } else {
            gs.warn("⚠️ No se encontró un registro change_request con sys_id: " + current.table_sys_id);
        }
    }
})(current, previous);

function sendNewAttachmentsToJira(sysId, jiraIssueId, tableName, currentCreatedOn) {
    var attachmentGR = new GlideRecord("sys_attachment");
    attachmentGR.addQuery("table_name", tableName);
    attachmentGR.addQuery("table_sys_id", sysId);
    attachmentGR.addQuery("sys_created_on", ">=", currentCreatedOn);
    attachmentGR.query();

    while (attachmentGR.next()) {
        var fileName = attachmentGR.file_name;
        var contentType = attachmentGR.content_type;
        var attachmentSysId = attachmentGR.sys_id;

        gs.info("📌 Procesando archivo: " + fileName);

        //  Verificar si se obtiene el contenido del archivo
        var attachmentContent = new GlideSysAttachment().getBytes(attachmentSysId);
        if (!attachmentContent || attachmentContent.length === 0) {
            gs.error(" El archivo está vacío o no se pudo leer: " + fileName);
            continue;
        }

        gs.info("📏 Tamaño del archivo en bytes: " + attachmentContent.length);

        var boundary = "----WebKitFormBoundary" + gs.generateGUID();
        var newLine = "\r\n";

        //  Usar `ByteArrayOutputStream` para construir `multipart/form-data`
        var ByteArrayOutputStream = Packages.java.io.ByteArrayOutputStream;
        var bodyStream = new ByteArrayOutputStream();

        //  Construcción del encabezado multipart
        var multipartHeader = "--" + boundary + newLine;
        multipartHeader += 'Content-Disposition: form-data; name="file"; filename="' + fileName + '"' + newLine;
        multipartHeader += "Content-Type: " + contentType + newLine + newLine;

        try {
            //  Escribiendo los datos en el stream
            bodyStream.write(multipartHeader.getBytes());
            bodyStream.write(attachmentContent);
            bodyStream.write((newLine + "--" + boundary + "--" + newLine).getBytes());

            gs.info("📏 Tamaño total del request en bytes: " + bodyStream.size());

            //  Convertir a un array de bytes
            var requestBody = bodyStream.toByteArray();
        } catch (e) {
            gs.error(" Error al escribir en el stream: " + e.message);
            continue;
        }

        // 🔥 Configurar la solicitud REST a Jira
        var attachmentRequest = new sn_ws.RESTMessageV2();
        attachmentRequest.setHttpMethod("POST");
        attachmentRequest.setEndpoint("https:/instance.atlassian.net/rest/api/3/issue/" + jiraIssueId + "/attachments");

        //  Verificar la autenticación
        var base64Auth = GlideStringUtil.base64Encode("user" + ":" + "token_user");
        attachmentRequest.setRequestHeader("Authorization", "Basic " + base64Auth);
        attachmentRequest.setRequestHeader("X-Atlassian-Token", "no-check");
        attachmentRequest.setRequestHeader("Accept", "application/json");
        attachmentRequest.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary);

        //  Verificar que `requestBody` no sea nulo antes de enviarlo
        if (!requestBody || requestBody.length === 0) {
            gs.error(" requestBody está vacío. No se enviará el archivo.");
            continue;
        }

        //  Enviar el request
        try {
            gs.info("📡 Enviando solicitud a Jira...");
            
            attachmentRequest.setRequestBody(requestBody);
            gs.info(" requestBody configurado correctamente.");

            var attachmentResponse = attachmentRequest.execute();
            var status = attachmentResponse.getStatusCode();
            var responseBody = attachmentResponse.getBody();

            if (status == 200 || status == 201) {
                gs.info(" Archivo enviado correctamente a Jira: " + fileName + " (ID: " + jiraIssueId + ")");
            } else {
                gs.warn("⚠️ Error al subir el archivo. Status: " + status + ", Respuesta: " + responseBody);
            }
        } catch (ex) {
            gs.error(" Error en la solicitud a Jira: " + ex.message);
        }
    }
}