Join the #BuildWithBuildAgent Challenge! Get recognized, earn exclusive swag, and inspire the ServiceNow Community with what you can build using Build Agent.  Join the Challenge.

Not able to store the attachment in the sys_attachment table

Praju_123
Tera Contributor

I have created a custom widget in ServiceNow and added it to a Record Producer using a custom type variable field. The purpose of this widget is to upload attachments to the sys_attachment table. Additionally, I have created Category (choice) field to associate a category with each attachment.

To achieve this, I opted for a custom widget instead of using the out-of-the-box (OOB) attachment widget. I also created a Script Include, which I’m calling from the widget’s client controller script.

However, when I click the attachment upload button, I encounter the error:
"Unhandled exception in GlideAjax".
I have already wrapped the logic in a try-catch block, but the error message remains unclear and doesn’t provide specific details.

Could you please advise on how to troubleshoot or resolve this issue?

Praju_123_0-1755525206473.png

Praju_123_1-1755525451872.png

 

 


widget code
html

<!-- Add Attachments Button -->
<button class="btn btn-link" ng-click="c.openModal()">
<i class="fa fa-paperclip"></i> Add attachments
</button>

 

<!-- Uploaded Attachments Display -->
<div ng-if="c.uploadedAttachments.length > 0" class="uploaded-attachments mt-3">
<div class="attachment-card" ng-repeat="attachment in c.uploadedAttachments track by $index">
   <div class="attachment-info">
     <div class="attachment-preview">
       <!-- Show image preview if it's an image file -->
       <img ng-if="attachment.isImage && attachment.previewUrl" 
            ng-src="{{attachment.previewUrl}}" 
            alt="{{attachment.name}}" 
            class="attachment-image">
       <!-- Show file icon for non-image files -->
       <div ng-if="!attachment.isImage" class="attachment-icon">
         <i class="fa fa-file-o fa-2x"></i>
       </div>
       <!-- Loading state while reading image -->
       <div ng-if="attachment.isImage && !attachment.previewUrl" class="attachment-loading">
         <i class="fa fa-spinner fa-spin fa-2x"></i>
       </div>
     </div>
     <div class="attachment-details">
       <!--div class="attachment-name">{{attachment.name}}</div-->
       <div class="attachment-meta">
         <span class="file-name">{{attachment.displayFileName}}</span>
         <span class="file-size">({{attachment.fileSize}})</span>
         <span class="upload-time">{{attachment.uploadTime}}</span>
       </div>
       <div class="attachment-category" ng-if="attachment.category">
         Category: {{attachment.category}}
       </div>
     </div>
   </div>
   <div class="attachment-actions">
     <button type="button" class="btn btn-sm btn-link" ng-click="c.editAttachment($index)" title="Edit">
       <i class="fa fa-pencil"></i>
     </button>
     <button type="button" class="btn btn-sm btn-link text-danger" ng-click="c.deleteUploadedAttachment($index)" title="Delete">
       <i class="fa fa-times"></i>
     </button>
   </div>
</div>
</div>

 

<!-- Modal Window -->
<div class="modal" tabindex="-1" role="dialog" ng-show="c.modalOpen" style="display:block; background:rgba(0,0,0,0.3);">
<div class="modal-dialog" role="document">
   <div class="modal-content" style="margin-top:80px;">
     <div class="modal-header">
       <h4>Attach file(s)</h4>
       <button type="button" class="close" ng-click="c.closeModal()">&times;</button>
     </div>
     <div class="modal-body">
       <!-- Drag and Drop Area -->
       <div class="upload-zone text-center p-4 mb-3 border-dashed" 
            ng-drop="true" 
            ng-drop-success="c.handleDrop($event)">
         <i class="fa fa-paperclip fa-2x"></i>
         <div>Drag and drop files here</div>
       </div>
       <div class="text-center my-2">OR</div>
       <!-- Select Files Button -->
       <button class="btn btn-secondary" type="button" ng-click="c.selectFiles()">
         Select file(s)
       </button>
       <input type="file" id="fileInput" multiple style="display:none" 
              onchange="angular.element(this).scope().c.handleFileSelect(this.files)">
       <!-- Attachments Table -->
       <div ng-if="c.attachments.length > 0" class="mt-4">
         <table class="table table-sm">
           <thead>
             <tr>
               <th>Preview</th>
               <th>Name</th>
               <th>File</th>
               <th>Category</th>
               <th></th>
             </tr>
           </thead>
           <tbody>
             <tr ng-repeat="a in c.attachments">
               <td>
                 <img ng-if="a.isImage && a.previewUrl" ng-src="{{a.previewUrl}}" class="table-preview-image">
                 <i ng-if="!a.isImage" class="fa fa-file-o"></i>
                 <i ng-if="a.isImage && !a.previewUrl" class="fa fa-spinner fa-spin"></i>
               </td>
               <td>
                 <input class="form-control" ng-model="a.name" ng-change="c.updateFileName(a)" required>
               </td>
               <td>{{a.displayFileName}}</td>
               <td>
                 <select class="form-control" ng-model="a.category" ng-options="cat for cat in c.categories"></select>
               </td>
               <td>
                 <button type="button" class="btn btn-danger btn-sm" ng-click="c.removeAttachment($index)">
                   <i class="fa fa-trash"></i>
                 </button>
               </td>
             </tr>
           </tbody>
         </table>
       </div>
     </div>
     <div class="modal-footer">
       <button class="btn btn-secondary" ng-click="c.closeModal()">Cancel</button>
       <button class="btn btn-primary" ng-disabled="!c.attachments.length" ng-click="c.uploadAttachments()">Attach</button>
     </div>
   </div>
</div>
</div>

client controller

api.controller = function($scope, $element, spUtil) {



    var c = this;
    // Modal visibility control
    c.modalOpen = false;
    // Arrays for attachments
    c.attachments = []; // Temporary attachments in modal
    c.uploadedAttachments = []; // Final uploaded attachments displayed on form
    c.editingIndex = -1; // Track if we're editing an existing attachment

    // Categories -- update per your requirement

    c.categories = [

        'Photo of affected shipment', 'CCTV Screenshots / Images', 'Photo - Others',
        'Document - POD (Delivery note)', 'Document - AWB & invoice',
        'Document - ULD breakdown manifest', 'Document - ULD build-up manifest',
        'Document - Survey Report / GHA Investigation Report',
        'Document - Claims letter', 'Document - Others'

    ];
    // Image file types
    c.imageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', 'image/webp'];

    c.openModal = function() {
        c.attachments = [];
        c.editingIndex = -1;
        c.modalOpen = true;
    };



    c.closeModal = function() {
        c.modalOpen = false;
        c.editingIndex = -1;
        $element.find('#fileInput').val(""); // Clear file input

    };

    c.selectFiles = function() {
        $element.find('#fileInput')[0].click();
    };

    // Helper function to check if file is an image

    c.isImageFile = function(file) {
        return c.imageTypes.indexOf(file.type.toLowerCase()) !== -1;
    };

    // Helper function to get file extension

    c.getFileExtension = function(filename) {
        return filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2);
    };



    // Helper function to get filename without extension

    c.getFileNameWithoutExtension = function(filename) {
        return filename.slice(0, filename.lastIndexOf('.')) || filename;
    };

    // Function to update display filename when name changes

    c.updateFileName = function(attachment) {
        if (attachment.name && attachment.originalExtension) {
            attachment.displayFileName = attachment.name + '.' + attachment.originalExtension;
        }
    };

    // Helper function to read image file and create preview

    c.createImagePreview = function(file, attachment) {
        if (!c.isImageFile(file)) {
            attachment.isImage = false;
            return;

        }

        attachment.isImage = true;
        attachment.previewUrl = null; // Loading state  

        var reader = new FileReader();
        reader.onload = function(e) {
            attachment.previewUrl = e.target.result;
            $scope.$apply(); // Trigger digest cycle to update view

        };
        reader.readAsDataURL(file);
    };


    c.handleFileSelect = function(files) {
        for (var i = 0; i < files.length; i++) {
            var originalFileName = files[i].name;
            var nameWithoutExt = c.getFileNameWithoutExtension(originalFileName);
            var extension = c.getFileExtension(originalFileName);

            var attachment = {
                name: nameWithoutExt,
                originalFileName: originalFileName,
                originalExtension: extension,
                displayFileName: originalFileName, // Initially same as original
                file: files[i],
                category: c.categories[0],
                isImage: false,
                previewUrl: null

            };

            c.attachments.push(attachment);
            c.createImagePreview(files[i], attachment);
        }

        $scope.$apply();

    };



    c.handleDrop = function(event) {
        var dt = event.dataTransfer || (event.originalEvent && event.originalEvent.dataTransfer);
        if (dt && dt.files && dt.files.length) {
            c.handleFileSelect(dt.files);

        }

    };


    c.removeAttachment = function(idx) {
        c.attachments.splice(idx, 1);
    };


    // Helper function to format file size

    c.formatFileSize = function(bytes) {
        if (bytes === 0) return '0 Bytes';
        var k = 1024;
        var sizes = ['Bytes', 'KB', 'MB', 'GB'];
        var i = Math.floor(Math.log(bytes) / Math.log(k));
        return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];

    };


    c.uploadAttachments = function() {

        // Move attachments from modal to uploaded list

        /*for (var i = 0; i < c.attachments.length; i++) {

            var attachment = c.attachments[i];
            var uploadedAttachment = {
                name: attachment.name,
                originalFileName: attachment.originalFileName,
                displayFileName: attachment.displayFileName,
                originalExtension: attachment.originalExtension,
                fileSize: c.formatFileSize(attachment.file.size),
                category: attachment.category,
                uploadTime: 'just now',
                file: attachment.file, // Keep reference for actual upload later
                isImage: attachment.isImage,
                previewUrl: attachment.previewUrl
            };*/
        /*
            var ga = new GlideAjax('x_icpa_gbs_secur_0.storeAttachmentToTable');
            ga.addParam('table_value', 'x_icpa_gbs_ltr_l_0_letter_request]');
            ga.addParam('cat_value', "name");
            //ga.addParam('file_value', attachment.file);

            ga.getXMLAnswer(function(response) {
                // Optionally, use response for feedback or logging
                // alert(response);
            });

            alert(uploadedAttachment.category + " " + uploadedAttachment.file);
            */
        /*if (c.editingIndex >= 0) {
                // Replace existing attachment if editing

                c.uploadedAttachments[c.editingIndex] = uploadedAttachment;
                c.editingIndex = -1;

            } else {

                // Add new attachment
                c.uploadedAttachments.push(uploadedAttachment);

            }
        }*/

        var targetTable = 'x_icpa_gbs_secur_0_gbs_security_incident'; // <-- Your table name
        // var targetSysId = $scope.data.sys_id; // <-- Make sure to set this on the widget (from page)
        var targetSysId = "9e733d76931b22103d4a701efaba1048";
        if (!targetSysId) {
            spUtil.addErrorMessage('Target record sys_id missing.');
            return;
        }

        for (var i = 0; i < c.attachments.length; i++) {
            (function(attachment) {
                var reader = new FileReader();
                reader.onload = function(e) {
                    // Remove data&colon;image/...;base64, prefix if present
                    var base64Content = e.target.result.split(',')[1];

                    var ga = new GlideAjax('storeAttachmentToTable');
                    ga.addParam('sysparm_name', 'storeAttachmentRecord');
                    ga.addParam('sysparm_table_value', targetTable);
                    ga.addParam('sysparm_sys_id', targetSysId);
                    ga.addParam('sysparm_cat_value', attachment.category);
                    ga.addParam('sysparm_file_name', attachment.displayFileName);
                    ga.addParam('sysparm_file_content', base64Content);

                    ga.getXMLAnswer(function(response) {
                        alert(response);
                    });

                    // Add to uploadedAttachments list
                    c.uploadedAttachments.push({
                        name: attachment.name,
                        originalFileName: attachment.originalFileName,
                        displayFileName: attachment.displayFileName,
                        originalExtension: attachment.originalExtension,
                        fileSize: c.formatFileSize(attachment.file.size),
                        category: attachment.category,
                        uploadTime: 'just now',
                        isImage: attachment.isImage,
                        previewUrl: attachment.previewUrl
                    });
                };
                reader.readAsDataURL(attachment.file);
            })(c.attachments[i]);
        };

        c.closeModal();

        // Here you would implement the actual file upload to ServiceNow

        // For now, just show success message
        spUtil.addInfoMessage(c.attachments.length + ' attachment(s) added successfully');



        // Clear the modal attachments
        c.attachments = [];
    };

    // Edit an uploaded attachment

    c.editAttachment = function(index) {

        var attachment = c.uploadedAttachments[index];
        c.editingIndex = index;
        // Pre-populate modal with attachment data
        var editAttachment = {
            name: attachment.name,
            originalFileName: attachment.originalFileName,
            displayFileName: attachment.displayFileName,
            originalExtension: attachment.originalExtension,
            file: attachment.file,
            category: attachment.category,
            isImage: attachment.isImage,
            previewUrl: attachment.previewUrl
        };

        c.attachments = [editAttachment];
        c.modalOpen = true;

    };

    // Delete an uploaded attachment
    c.deleteUploadedAttachment = function(index) {

        if (confirm('Are you sure you want to delete this attachment?')) {

            c.uploadedAttachments.splice(index, 1);
            spUtil.addInfoMessage('Attachment deleted successfully');

        }
    };
};

script include code
 
var storeAttachmentToTable = Class.create();
storeAttachmentToTable.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
    storeAttachmentRecord: function() {
        try {
            // Parameter retrieval from GlideAjax
            var tableName   = this.getParameter('sysparm_table_value');
            var recordSysId = this.getParameter('sysparm_sys_id');
            var catoption   = this.getParameter('sysparm_cat_value');
            var fileName    = this.getParameter('sysparm_file_name');
            var base64Data  = this.getParameter('sysparm_file_content');

            // Validate required parameters
            if (!tableName || !recordSysId || !fileName || !base64Data) {
                return "Missing required parameters.";
            }

            // Decode base64 to bytes
            var contentBytes = GlideStringUtil.base64DecodeAsBytes(base64Data);

            // Write attachment record to sys_attachment
            var gsa = new GlideSysAttachment();
            var sysAttachmentId = gsa.write(tableName, recordSysId, fileName, contentBytes);

            // Optional: Set category (custom field) if provided
            if (catoption) {
                var attachmentGR = new GlideRecord('sys_attachment');
                if (attachmentGR.get(sysAttachmentId)) {
                    attachmentGR.u_categoryoption = catoption; // Pass real value, not hardcoded '2'
                    attachmentGR.update();
                }
            }

            // Return new attachment sys_id
            return sysAttachmentId;

        } catch (e) {
            gs.error('storeAttachmentToTable ScriptInclude error: ' + (e.message || e));
            return 'Error: ' + (e.message || e);
        }
    },

    // MUST be client-callable for GlideAjax usage
    type: 'storeAttachmentToTable'
});





1 REPLY 1

sonamsharma8789
Tera Expert

Hi @Praju_123 ,

The code you pasted is very big and hard to understand in this format. But still there are some reason for the "Unhandled exception in GlideAjax" which means that a GlideAjax call from a Client Script/UI Policy/UI Action tried to call a Script Include, but something broke on the server-side response.

Reasons:

  1. Check Script Include if it is not Client Callable-If you want to use a Script Include in GlideAjax, it must be:Active,Client Callable = true,Extend AbstractAjaxProcessor.
  2. Method Name Mismatch-The value of sysparm_name must exactly match a function in the Script Include.
  3. Server-side Error Inside Script Include-Check System Logs > All for a stack trace.
  4. Script Include Not Accessible in Scope-If you’re in a scoped app, but the Script Include is global (or vice versa), you might hit scope access issues.Fix: Check "Accessible from" in the Script Include.
  5. Return Type is Wrong-Sometimes we try return inside the method instead of return "value"; at the right place.For getXMLAnswer(), you must return stringValue; from Script Include.

Check these few things in your script. Please mark helpful.