Inbound SOAP/Base64 attachments stopped working

An issue with inbound attachments being saved using this mechanism has been seen in ServiceNow instances upgrading to Quebec and later which are using base64 encoded attachments. An example of this error is shown below:

InternalError: The choice of Java method com.glide.ui.SysAttachment.write matching JavaScript argument types (object,org.mozilla.javascript.ConsString,string,null) is ambiguous; candidate methods are:

class java.lang.String write(com.glide.script.GlideRecord,java.lang.String,java.lang.String,java.io.InputStream)

class java.lang.String write(com.glide.script.GlideRecord,java.lang.String,java.lang.String,java.io.File)

class java.lang.String write(com.glide.script.GlideRecord,java.lang.String,java.lang.String,java.lang.String)

class java.lang.String write(com.glide.script.GlideRecord,java.lang.String,java.lang.String,byte[])

Unifi comes packaged with a utility for helping save attachments:

AttachmentHandler.saveAttachment(record, filename, content_type, data)

There has been a change to the GlideSysAttachment API being used by this method. Now, passing the base64 decoded string as a byte array to the global GlideSysAttachment.write() method via the Unifi AttachmentHandler appears to only work when Unifi trace mode is on. Internal investigation has revealed that there is some interaction between ServiceNow and the Unifi scoped app which is causing the byte array to be converted to null when Unifi trace mode is off. We suspect this is because ServiceNow routes the variable through some coercion in the Rhino environment when passing it through Unifi and that forces it to work. Without the debug mode turned on, the byte array is being passed through and, for some reason that isn't clear, it is treated differently.

Workaround

To work around this issue it is recommended to modify the Extract attachments script on the message which is being used to process attachments. By adding a new saveAttachments() function that makes use of ServiceNow's writeBase64() method, the data does not need to be decoded like it was in the past and attachments are processed properly regardless of Unifi settings.

function saveAttachment(record, filename, content_type, data) {
  var id;

  if (typeof filename === 'undefined') {
    x_snd_eb.ws_console.warn('Ignoring attachment with missing filename attribute.');
    id = 'x-attachment-error-missing-filename';
  } else if (!filename) {
    x_snd_eb.ws_console.warn('Ignoring attachment with empty filename attribute.');
    id = 'x-attachment-error-filename-is-empty';
  } else {
    // use x_snd_eb scope to force using the scoped version of GSA
    id = new x_snd_eb.GlideSysAttachment().writeBase64(record, filename, content_type, data);
    x_snd_eb.ws_console.trace('Extracted attachment [' + filename + ':' + content_type + ']' +
      ' to record [' + id + ']' +
      ' for [' + record.getTableName() + ':' + record.sys_id + ']');
  }

  // return the original tag with the sys_id of the new attachment record
  // so we can pick it up in processing later on
  return '<x-attachment-data sys_id="' + id + '" />';
}

Binary Base64

Decoding a base64 string should be done with binary aware methods to ensure that binary files are decoded correctly. We have seen problems using methods like GlideStringUtil.base64Decode() with binary files where the attachment is created but it is not usable. This is likely the problem if plain text files are working but images are not.

Full example for Extract attachments script

This example is for a JSON payload, but the same concept applies to SOAP.

payload = (function extractAttachments(payload, request) {

  function saveAttachment(record, filename, content_type, data) {
    var id;

    if (typeof filename === 'undefined') {
      x_snd_eb.ws_console.warn('Ignoring attachment with missing filename attribute.');
      id = 'x-attachment-error-missing-filename';
    } else if (!filename) {
      x_snd_eb.ws_console.warn('Ignoring attachment with empty filename attribute.');
      id = 'x-attachment-error-filename-is-empty';
    } else {  
      // use x_snd_eb scope to force using the scoped version of GSA
      id = new x_snd_eb.GlideSysAttachment().writeBase64(record, filename, content_type, data);
      x_snd_eb.ws_console.trace('Extracted attachment [' + filename + ':' + content_type + ']' +
        ' to record [' + id + ']' +
        ' for [' + record.getTableName() + ':' + record.sys_id + ']');
    }

    // return the original tag with the sys_id of the new attachment record
    // so Unifi can pick it up in processing later on
    return '[x-attachment-data sys_id="' + id + '" /]';
  }

  var obj = JSON.parse(payload);

  obj.attachment.data = saveAttachment(
    request, 
    obj.attachment.filename, 
    obj.attachment.mime_code, 
    obj.attachment.data
  );

  return JSON.stringify(obj);
})(payload, request);