Skip to content

Salesforce Apex/Flow: POST salesforce file as Asana attachments via Asana API encoded as multipart/form-data

Recently I was working on Salesforce – Asana integration project using Asana API in apex. The API requests work fine for creating asana tasks, sub-tasks, projects, etc. using normal API requests, which authorize the request using the Asana API Key. But when it comes to uploading Salesforce files as attachments to Asana consuming API, it becomes tricky. Because it requires the file data to be encoded in some special format with the multipart/form-data.

The Asana Upload an attachment endpoint expects a multipart/form-data encoded request containing the full contents of the file to be uploaded. I was using Salesforce flows to create an asana task and then using the file upload component. I also wanted to upload the Salesforce files to be posted to Asana as an attachment related to the new task. So I came up with the following apex code.

public class AsanaUploadFile {
    
    public class Requests {
        @InvocableVariable(label='Asana Task Id' required=true)
        public String AsanaTaskId;
        
        @InvocableVariable(label='Content Document Ids' required=true)
        public list<String> ContentDocumentIds;
    }
    
    @InvocableMethod(label='Asana - Upload Attachment to a Task' iconName='slds:standard:attach')
    public static list<Result> uploadFiles(List<Requests> requestList) {
        
        String api_key = 'PUT_YOUR_API_KEY_HERE';
        
        list<Result> ResultToReturn = new list<Result>();
        
        for(Requests reqItem : requestList){
            Result ResultItem = new Result();
            ResultItem.HasError = false;
            
            String taskId = reqItem.AsanaTaskId;
            list<String> ContentDocumentIds = reqItem.ContentDocumentIds;
            
            String reqEndpoint = 'https://app.asana.com/api/1.0/tasks/'+taskId+'/attachments';
            
            list<ContentVersion> Files = new list<ContentVersion>([SELECT Id, ContentDocumentId, ContentUrl, FileExtension, FileType, IsLatest, Title, VersionData FROM ContentVersion WHERE ContentDocumentId IN :ContentDocumentIds AND IsLatest = TRUE]);
            for(ContentVersion file : Files){
                
                Blob file_content = file.VersionData;
                String file_name = file.Title+'.'+file.FileExtension;
                
                
                string boundary = '1ff13444ed8140c7a32fc4e6451aa76d';
                
                String header = '--' + boundary + '\r\n' +
                    + 'Content-Type: application/octet-stream\r\n'+
                    + 'Content-Disposition: form-data; name="file";filename="' + file_name +'"';
                
                String headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header + '\r\n\r\n'));
                
                String footer = '--' + boundary + '--';
                
                while(headerEncoded.endsWith('='))
                {
                    header += ' ';
                    headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header+'\r\n\r\n'));
                }
                
                String bodyEncoded = EncodingUtil.base64Encode(file_content);
                
                Blob bodyBlob = null;
                
                String last4Bytes = bodyEncoded.substring(bodyEncoded.length()-4,bodyEncoded.length());
                System.debug('----->last4Bytes: '  + last4Bytes );
                
                if(last4Bytes.endsWith('==')) {
                    last4Bytes = last4Bytes.substring(0,2) + '0K';
                    bodyEncoded = bodyEncoded.substring(0,bodyEncoded.length()-4) + last4Bytes;
                    // We have appended the \r\n to the Blob, so leave footer as it is.
                    String footerEncoded = EncodingUtil.base64Encode(Blob.valueOf(footer));
                    bodyBlob = EncodingUtil.base64Decode(headerEncoded + bodyEncoded + footerEncoded);
                } else if(last4Bytes.endsWith('=')) {
                    last4Bytes = last4Bytes.substring(0,3) + 'N';
                    bodyEncoded = bodyEncoded.substring(0,bodyEncoded.length()-4) + last4Bytes;
                    // We have appended the CR e.g. \r, still need to prepend the line feed to the footer
                    footer = '\n' + footer;
                    String footerEncoded = EncodingUtil.base64Encode(Blob.valueOf(footer));
                    bodyBlob = EncodingUtil.base64Decode(headerEncoded+bodyEncoded+footerEncoded);            
                    
                } else {
                    // Prepend the CR LF to the footer
                    footer = '\r\n' + footer;
                    String footerEncoded = EncodingUtil.base64Encode(Blob.valueOf(footer));
                    bodyBlob = EncodingUtil.base64Decode(headerEncoded + bodyEncoded + footerEncoded);
                }
                
                System.debug('---> Body: ' + bodyBlob);
                HTTPRequest req= new HttpRequest();
                req.setendpoint(reqEndpoint);
                req.setmethod('POST');
                req.setHeader('Authorization', 'Bearer '+api_key);
                req.setHeader('Content-Type','multipart/form-data; boundary='+boundary);
                req.setBodyAsBlob(bodyBlob);
                System.debug(' res -----> :' + req.getBody());
                Http p = new Http();
                HttpResponse res= new HttpResponse();
                res = p.send(req);
                
                system.debug('status Code::'+res.getStatusCode());
                system.debug('Response Body::'+res.getBody());
                
                if(res.getStatusCode() == 200){
                    AsanaAttachmentSuccess AttachmentSuccessData = AsanaAttachmentSuccess.parse(res.getBody());
                    if(AttachmentSuccessData != NULL && AttachmentSuccessData.data != NULL){
                        system.debug('gid::'+AttachmentSuccessData.data.gid);
                        system.debug('name::'+AttachmentSuccessData.data.name);
                        ResultItem.AttachmentGid = AttachmentSuccessData.data.gid;
                        ResultItem.AttachmentName = AttachmentSuccessData.data.name;
                    }
                }else{
                    ResultItem.HasError = TRUE;
                    String ErrMessage = '';
                    AsanaAttachmentError AttachmentErrData = AsanaAttachmentError.parse(res.getBody());
                    if(AttachmentErrData != NULL && AttachmentErrData.errors != NULL){
                        for(AsanaAttachmentError.Errors err : AttachmentErrData.errors){
                            system.debug('message::'+err.message);
                            system.debug('help::'+err.help);
                            ErrMessage += 'message: '+err.message+'/n';
                            ErrMessage += 'help: '+err.help+'/n';
                        }
                    }
                    ResultItem.ErrorMessage = ErrMessage;
                }
            }
            ResultToReturn.add(ResultItem);
        }
        
        return ResultToReturn;
    }
    
    
    public class Result {
        @InvocableVariable(label='Attachment Id')
        public String AttachmentGid;
        
        @InvocableVariable(label='Attachment Name')
        public String AttachmentName;
        
        @InvocableVariable(label='Has Error?')
        public boolean HasError;
        
        @InvocableVariable(label='Error Message')
        public String ErrorMessage;
    }
}

Apex class code to parse the API success response when the status code is 200.

public class AsanaAttachmentSuccess {

    public Data data;

    public class Data {
        public String gid;
        public String resource_type;
        public String name;
        public String resource_subtype;
    }

    
    public static AsanaAttachmentSuccess parse(String json) {
        return (AsanaAttachmentSuccess) System.JSON.deserialize(json, AsanaAttachmentSuccess.class);
    }
}

Apex class code to parse the API failure response when the status code is NOT 200.

public class AsanaAttachmentError {

    public class Errors {
        public String message;
        public String help;
    }

    public List<Errors> errors;

    
    public static AsanaAttachmentError parse(String json) {
        return (AsanaAttachmentError) System.JSON.deserialize(json, AsanaAttachmentError.class);
    }
}

All the credits to: https://salesforce-season.blogspot.com/2017/05/http-request-with-multipartform-data.html