[SalesForce] Salesforce isn’t handling Base64 Encoded attachments uploaded via Rest API

I'm uploading an image as an attachment to an object in Salesforce via Rest API v32. My HTTP request body looks like so:

--boundary_string_1416597802788
Content-Disposition: form-data; name="entity_attachment";
Content-Type: application/json

{"ContentType":"image/png;base64","Name":"Signature","Description":"Signature signed on November 21st 2014 1:23 PM","ParentId":"a02J000000DsXDQIA3","IsPrivate":false}

--boundary_string_1416597802788
Content-Type: image/png;base64
Content-Disposition: form-data; name="Body"; filename="filler"

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AAECBCoBpbcSMidAgAABAgQIEIgXUHrjI7QAAQIECBAgQIBAJaD0VkLmBAgQIECAAAEC8QJKb3yEFiBAgAABAgQIEKgElN5KyJwAAQIECBAgQCBeQOmNj9ACBAgQIECAAAEClYDSWwmZEyBAgAABAgQIxAsovfERWoAAAQIECBAgQKASUHorIXMCBAgQIECAAIF4AaU3PkILECBAgAABAgQIVAJKbyVkToAAAQIECBAgEC+g9MZHaAECBAgQIECAAIFKQOmthMwJECBAgAABAgTiBZTe+AgtQIAAAQIECBAgUAkovZWQOQECBAgQIECAQLyA0hsfoQUIECBAgAABAgQqAaW3EjInQIAAAQIECBCIF1B64yO0AAECBAgQIECAQCWg9FZC5gQIECBAgAABAvECSm98hBYgQIAAAQIECBCoBJTeSsicAAECBAgQIEAgXkDpjY/QAgQIECBAgAABApWA0lsJmRMgQIAAAQIECMQLKL3xEVqAAAECBAgQIECgElB6KyFzAgQIECBAgACBeAGlNz5CCxAgQIAAAQIECFQCSm8lZE6AAAECBAgQIBAvoPTGR2gBAgQIECBAgACBSkDprYTMCRAgQIAAAQIE4gWU3vgILUCAAAECBAgQIFAJKL2VkDkBAgQIECBAgEC8gNIbH6EFCBAgQIAAAQIEKgGltxIyJ0CAAAECBAgQiBdQeuMjtAABAgQIECBAgEAloPRWQuYECBAgQIAAAQLxAkpvfIQWIECAAAECBAgQqASU3krInAABAgQIECBAIF5A6Y2P0AIECBAgQIAAAQKVgNJbCZkTIECAAAECBAjECyi98RFagAABAgQIECBAoBJQeishcwIECBAgQIAAgXgBpTc+QgsQIECAAAECBAhUAkpvJWROgAABAgQIECAQL6D0xkdoAQIECBAgQIAAgUpA6a2EzAkQIECAAAECBOIFlN74CC1AgAABAgQIECBQCSi9lZA5AQIECBAgQIBAvIDSGx+hBQgQIECAAAECBCoBpbcSMidAgAABAgQIEIgXUHrjI7QAAQIECBAgQIBAJaD0VkLmBAgQIECAAAEC8QJKb3yEFiBAgAABAgQIEKgElN5KyJwAAQIECBAgQCBeoFXpvXPnzlfr9freSGU/PhkLECBAgAABAgR2J/D3arX65u7du/d3d4TzfbLSe76e7kaAAAECBAgQ6CCg9HZI0Q4ECBAgQIAAAQLvl0CrT3rfr+hsS4AAAQIECBAgMCug9M5KuY4AAQIECBAgQCBWQOmNjc7BCRAgQIAAAQIEZgWU3lkp1xEgQIAAAQIECMQKKL2x0Tk4AQIECBAgQIDArIDSOyvlOgIECBAgQIAAgVgBpTc2OgcnQIAAAQIECBCYFVB6Z6VcR4AAAQIECBAgECug9MZG5+AECBAgQIAAAQKzAkrvrJTrCBAgQIAAAQIEYgWU3tjoHJwAAQIECBAgQGBWQOmdlXIdAQIECBAgQIBArIDSGxudgxMgQIAAAQIECMwKKL2zUq4jQIAAAQIECBCIFVB6Y6NzcAIECBAgQIAAgVkBpXdWynUECBAgQIAAAQKxAkpvbHQOToAAAQIECBAgMCug9M5KuY4AAQIECBAgQCBWQOmNjc7BCRAgQIAAAQIEZgWU3lkp1xEgQIAAAQIECMQKKL2x0Tk4AQIECBAgQIDArIDSOyvlOgIECBAgQIAAgVgBpTc2OgcnQIAAAQIECBCYFVB6Z6VcR4AAAQIECBAgECug9MZG5+AECBAgQIAAAQKzAkrvrJTrCBAgQIAAAQIEYgWU3tjoHJwAAQIECBAgQGBWQOmdlXIdAQIECBAgQIBArIDSGxudgxMgQIAAAQIECMwKKL2zUq4jQIAAAQIECBCIFVB6Y6NzcAIECBAgQIAAgVkBpXdWynUECBAgQIAAAQKxAkpvbHQOToAAAQIECBAgMCvwD2zg269ST+CHAAAAAElFTkSuQmCC

--boundary_string_1416597802788--

Please note that the encoded base64 image is not sensitive

The upload completes successfully, and I can verify that the base64 encoded data is now saved in Salesforce. However, I cannot view the attachment on Salesforce. When trying to "view" the image, I am taken to a url like …./servlet/servlet.FileDownload?file=00PJ0000001tmb3 and the image displays a broken image icon (in Firefox, it tells me that the image cannot be displayed because it contains errors).

After some investigation, I realized that the image is not displaying because it is still in the base64 format, even after it has been uploaded. I assumed that I would need to upload the file in base64 format, but this assumption appears to be wrong, OR Salesforce is not properly decoding it on their side.

What I've tried:

  • I tried playing around with the content types, and removed the ;base64 part and uploaded the plain binary. This didn't work, as in the image is not dsiplayable via Salesforce
  • I added a .png extension to the filename="".

Update
Based on Ketich C's answer, I tried adding the Content-Transfer-Encoding: base64 header, but that didn't work, and causes Salesforce to throw an exception upon upload.

Best Answer

Based on my research, I have concluded that at the time of this writing (using REST API v32), Salesforce does not decode uploaded base64 encoded data for Attachments. This is a bit confusing, since the Attachments documentation specifies the following:

The API sends and receives the binary file attachment data encoded as a base64Binary data type. Prior to creating a record, client applications must encode the binary attachment data as base64. Upon receiving a response, client applications must decode the base64 data to binary (this conversion is usually handled for you by the SOAP client).

The difficult part is uploading an attachment to match Salesforce's specific multipart/form-data template. Without going into the exhausing details, the only I found to do this was to manually build the HTTP request body, and put the entire body in an ArrayBuffer, which the browser will not UTF-8 encode. This answer on StackOverflow really paved the way for this fix.

// {
//   Body: base64EncodedData,
//   ContentType: '',
//   Additional attachment info (i.e. ParentId, Name, Description, etc.)
// }

function uploadAttachment (objectData, onSuccess, onError) {
    // Define a boundary
    var boundary = 'boundary_string_' + Date.now().toString();
    var attachmentContentType = !app.isNullOrUndefined(objectData.ContentType) ? objectData.ContentType : 'application/octet-stream';

    // Serialize the object, excluding the body, which will be placed in the second partition of the multipart/form-data request
    var serializedObject = JSON.stringify(objectData, function (key, value) { return key !== 'Body' ? value : undefined; });

    var requestBodyBeginning = '--' + boundary
        + '\r\n'
        + 'Content-Disposition: form-data; name="entity_attachment";'
        + '\r\n'
        + 'Content-Type: application/json'
        + '\r\n\r\n'
        + serializedObject
        + '\r\n\r\n' +
        '--' + boundary
        + '\r\n'
        + 'Content-Type: ' + attachmentContentType
        + '\r\n'
        + 'Content-Disposition: form-data; name="Body"; filename="filler"'
        + '\r\n\r\n';

    var requestBodyEnd =
        '\r\n\r\n'
        + '--' + boundary + '--';

    // The atob function will decode a base64-encoded string into a new string with a character for each byte of the binary data.
    var byteCharacters = window.atob(objectData.Body);

    // Each character's code point (charCode) will be the value of the byte.
    // We can create an array of byte values by applying .charCodeAt for each character in the string.
    var byteNumbers = new Array(byteCharacters.length);

    for (var i = 0; i < byteCharacters.length; i++) {
        byteNumbers[i] = byteCharacters.charCodeAt(i);
    }

    // Convert into a real typed byte array. (Represents an array of 8-bit unsigned integers)
    var byteArray = new Uint8Array(byteNumbers);

    var totalRequestSize = requestBodyBeginning.length + byteArray.byteLength + requestBodyEnd.length;

    var uint8array = new Uint8Array(totalRequestSize);
    var i;

    // Append the beginning of the request
    for (i = 0; i < requestBodyBeginning.length; i++) {
        uint8array[i] = requestBodyBeginning.charCodeAt(i) & 0xff;
    }

    // Append the binary attachment
    for (var j = 0; j < byteArray.byteLength; i++, j++) {
        uint8array[i] = byteArray[j];
    }

    // Append the end of the request
    for (var j = 0; j < requestBodyEnd.length; i++, j++) {
        uint8array[i] = requestBodyEnd.charCodeAt(j) & 0xff;
    }

    return $j.ajax({
        type: "POST",
        url: salesforceUrl,
        contentType: 'multipart/form-data' + "; boundary=\"" + boundary + "\"",
        cache: false,
        processData: false,
        data: uint8array.buffer,
        success: onSuccess,
        error: onError,
        beforeSend: function (xhr) {
            // Setup OAuth headers...
        }
    });
};
Related Topic