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.