18

I am trying to POST a PDF attachment to a Restful web service (not under my control). My issue is that I need to get put the raw bytes of the PDF into the Body of the request and whenever I call Blob.toString(), I get stopped by a UTF-8 String error:

BLOB is not a valid UTF-8 String

I've tried to put the base64 encoded bytes into the body instead, but the web service doesn't seem to support it.

public Dom.Document uploadFile(Attachment attachment){
    // Body
    String boundary = '------------' + String.valueOf(DateTime.now().getTime());
    string body = '';
    body+= boundary;
    body+= '\r\n';
    body+= 'Content-Disposition: form-data; name=\"file\";';
    body+= 'filename=\"' + attachment.name + '\"';
    body+= '\r\n';
    //body+= 'Content-Transfer-Encoding: base64\r\n';
    body+= 'Content-Type: application/octet-stream';
    body+= '\r\n\r\n';
    body+= attachment.body.toString();
    body+= '\r\n' + boundary + '\r\n';

    HttpRequest req = new HttpRequest();

    req.setEndpoint('http://connect.sandbox.mimeo.com/2012/02/storageservice/test');

    // Set headers
    req.setHeader('Authorization', authorizationData);
    req.setHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);
    req.setHeader('Content-Length',String.valueof(body.length()));
    req.setMethod('POST');
    req.setTimeOut(120000);

    req.setBody(body);

    Http http = new Http();

    HttpResponse res = http.send(req);
    Dom.Document retDocument = res.getBodyDocument();
    return retDocument;
}

Is there anyway to get the blob as a non utf8 string? I thinking about trying to write my own base64 decode that returns a string but I don't even know if this is even possible in apex.

Update

Just to give some more info here is the request I'm trying to replicate in Apex. This was made using Mimeo's C# library and was successful. Mimeo provides no documentation for this service that I can find:

POST http://connect.sandbox.mimeo.com/2012/02/storageservice/test HTTP/1.1
Authorization: Basic [removed]
Content-Type: multipart/form-data; boundary=----------8d0de7b15063540
Host: connect.sandbox.mimeo.com
Content-Length: 493827
Expect: 100-continue
Connection: Keep-Alive

------------8d0de7b15063540
Content-Disposition: form-data; name="file"; filename="printme.pdf"
Content-Type: application/octet-stream

[BINARY PDF]

------------8d0de7b15063540
2

3 Answers 3

26

This is how I send attachments. All credits(& explanation) http://enreeco.blogspot.in/2013/01/salesforce-apex-post-mutipartform-data.html

@Future(callout=true)
public static void uploadFile(Blob file_body, String file_name, String reqEndPoint){

          String boundary = '----------------------------741e90d31eff';
          String header = '--'+boundary+'\r\nContent-Disposition: form-data; name="file"; filename="'+file_name+'"\r\nContent-Type: application/octet-stream'; // added '\r's removed ';' see Tim Smith's comment
          String footer = '\r\n--'+boundary+'--';              
          String headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header+'\r\n\r\n'));
          while(headerEncoded.endsWith('='))
          {
           header+=' ';
           headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header+'\r\n\r\n'));
          }
          String bodyEncoded = EncodingUtil.base64Encode(file_body);
          String footerEncoded = EncodingUtil.base64Encode(Blob.valueOf(footer));

          Blob bodyBlob = null;
          String last4Bytes = bodyEncoded.substring(bodyEncoded.length()-4,bodyEncoded.length());
          if(last4Bytes.endsWith('='))
          {
               Blob decoded4Bytes = EncodingUtil.base64Decode(last4Bytes);
               HttpRequest tmp = new HttpRequest();
               tmp.setBodyAsBlob(decoded4Bytes);
               String last4BytesFooter = tmp.getBody()+footer;   
               bodyBlob = EncodingUtil.base64Decode(headerEncoded+bodyEncoded.substring(0,bodyEncoded.length()-4)+EncodingUtil.base64Encode(Blob.valueOf(last4BytesFooter)));
          }
          else
          {
                bodyBlob = EncodingUtil.base64Decode(headerEncoded+bodyEncoded+footerEncoded);
          }

          HttpRequest req = new HttpRequest();
          req.setHeader('Content-Type','multipart/form-data; boundary='+boundary);
          req.setMethod('POST');
          req.setEndpoint(reqEndPoint);
          req.setBodyAsBlob(bodyBlob);
          req.setTimeout(120000);

          Http http = new Http();
          HTTPResponse res = http.send(req);


}
4
  • 1
    Just as I was about to give up hope... this worked first time! I had tried something similar but couldn't get it to work. Thanks so much, this just saved me hours
    – NSjonas
    Jan 14, 2014 at 6:44
  • 2
    I mentioned above as well, but I ran into an issue where form parsers were failing after the initial boundary because of the \ns in the header string as well as the extra ; after the filename. This doesn't meet the RFC (w3.org/TR/html401/interact/forms.html#h-17.13.4.2). Changing to \r\n and removing the extra ; did the trick.
    – Tim Smith
    Aug 10, 2014 at 6:20
  • 1
    What is value 741e90d31eff here for boundary ? is it constant value & will never change ? Jul 23, 2020 at 14:02
  • Will this solution also work with Files (ContentDocument/ContentVersion)? Nov 13, 2020 at 18:24
16

Fix for occasional corruption when uploading a file from Salesforce using multipart mime.

The Salesforce solutions for multipart mime so far have left one problem outstanding, in that very occasionally you will get corruption in the last few bytes of your data.

The fix below resolves this issue, I'm posting it here as I'm extremely grateful to have been able to take advantage of the excellent code from previous postings, hopefully the below will be of some use to others.

I've tested this fix with previous files that corrupted in transit, and with 100's of doc, pdf, zip & .exe files, using both windiff and SHA1 hashes to confirm the files aren't getting corrupted in transit.

public static void uploadFile(Blob file_body, String file_name, String reqEndPoint){
      // Repost of code  with fix for file corruption issue
      // Orignal code postings and explanations
      // http://enreeco.blogspot.in/2013/01/salesforce-apex-post-mutipartform-data.html
      // http://salesforce.stackexchange.com/questions/24108/post-multipart-without-base64-encoding-the-body
      // Additional changes commented GW: that fix issue with occasional corruption of files
      String boundary = '----------------------------741e90d31eff';
      String header = '--'+boundary+'\nContent-Disposition: form-data; name="file"; filename="'+file_name+'";\nContent-Type: application/octet-stream';
      // GW: Do not prepend footer with \r\n, you'll see why in a moment
      // String footer = '\r\n--'+boundary+'--'; 
      String footer = '--'+boundary+'--';             
      String headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header+'\r\n\r\n'));
      while(headerEncoded.endsWith('='))
      {
       header+=' ';
       headerEncoded = EncodingUtil.base64Encode(Blob.valueOf(header+'\r\n\r\n'));
      }
      String bodyEncoded = EncodingUtil.base64Encode(file_body);
      // GW: Do not encode footer yet
      // String footerEncoded = EncodingUtil.base64Encode(Blob.valueOf(footer));

      Blob bodyBlob = null;
      String last4Bytes = bodyEncoded.substring(bodyEncoded.length()-4,bodyEncoded.length());

      // GW: Replacing this entire section
      /*
      if(last4Bytes.endsWith('='))
      {
           Blob decoded4Bytes = EncodingUtil.base64Decode(last4Bytes);
           HttpRequest tmp = new HttpRequest();
           tmp.setBodyAsBlob(decoded4Bytes);
           String last4BytesFooter = tmp.getBody()+footer;   
           bodyBlob = EncodingUtil.base64Decode(headerEncoded+bodyEncoded.substring(0,bodyEncoded.length()-4)+EncodingUtil.base64Encode(Blob.valueOf(last4BytesFooter)));
      }
      else
      {
            bodyBlob = EncodingUtil.base64Decode(headerEncoded+bodyEncoded+footerEncoded);
      }
      */
     // GW: replacement section to get rid of padding without corrupting data
     if(last4Bytes.endsWith('==')) {
        // The '==' sequence indicates that the last group contained only one 8 bit byte
        // 8 digit binary representation of CR is 00001101
        // 8 digit binary representation of LF is 00001010
        // Stitch them together and then from the right split them into 6 bit chunks
        // 0000110100001010 becomes 0000 110100 001010
        // Note the first 4 bits 0000 are identical to the padding used to encode the
        // second original 6 bit chunk, this is handy it means we can hard code the response in
        // The decimal values of 110100 001010 are 52 10
        // The base64 mapping values of 52 10 are 0 K
        // See http://en.wikipedia.org/wiki/Base64 for base64 mapping table
        // Therefore, we replace == with 0K
        // Note: if using \n\n instead of \r\n replace == with 'oK'
        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('=')) {
        // '=' indicates that encoded data already contained two out of 3x 8 bit bytes
        // We replace final 8 bit byte with a CR e.g. \r
        // 8 digit binary representation of CR is 00001101
        // Ignore the first 2 bits of 00 001101 they have already been used up as padding
        // for the existing data.
        // The Decimal value of 001101 is 13
        // The base64 value of 13 is N
        // Therefore, we replace = with N
        // Note: if using \n instead of \r replace = with 'K'
        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);  
      }

      HttpRequest req = new HttpRequest();
      req.setHeader('Content-Type','multipart/form-data; boundary='+boundary);
      req.setMethod('POST');
      req.setEndpoint(reqEndPoint);
      req.setBodyAsBlob(bodyBlob);
      req.setTimeout(120000);

      Http http = new Http();
      HTTPResponse res = http.send(req);
}   
4
  • I mentioned above as well, but I ran into an issue where form parsers were failing after the initial boundary because of the \ns in the header string as well as the extra ; after the filename. This doesn't meet the RFC (w3.org/TR/html401/interact/forms.html#h-17.13.4.2). Changing to \r\n and removing the extra ; did the trick.
    – Tim Smith
    Aug 10, 2014 at 6:21
  • This is an important improvement to my solution enreeco.blogspot.it/2013/01/… I'll add this link to my blog post Oct 17, 2014 at 8:18
  • I'm curious why the Content-Length parameter isn't being set in this code. Aug 3, 2016 at 22:31
  • Hi, Can anybody please comment how to add one more form parameter along with file? Mar 14, 2018 at 12:59
3

Does the Restful webservice require a multi-part request? If not, your could use HttpRequest.setBodyAsBlob() to POST the raw Blob.

See also: Sending a blob from Salesforce in a HTTP request?

Rather than using attachment.body.toString() to convert the Blob to a string. Try: base64Encode(Blob)

 body+= EncodingUtil.base64Encode(attachment.body); 
5
  • I already tried converting the blob to base64 as I mentioned in the question. This API seems to be picky and if I use setBlobAsBody() then how do I add the Boundary, Content-Disposition, and Content-Type to the top of the body?
    – NSjonas
    Jan 13, 2014 at 22:43
  • From the question in your answer, it seems there is no way to add the multipart headers... Once again salesforce has made a seemly simple task exponentially more complicated
    – NSjonas
    Jan 13, 2014 at 22:51
  • @NSjonas The Boundary, Content-Disposition, etc... in the body are only required for a multi-part posts. Try setting the Content-Type and Content-Disposition in the HttpRequest headers instead and use setBodyAsBlob() to just add the Blob body. Jan 13, 2014 at 22:54
  • Yup I'm actually saving code to try that as I try this. I ported this code from a C# library that I can successfully make the call on. I'm not sure why they are doing it as a multipart request. Crossing my fingers that this will work
    – NSjonas
    Jan 13, 2014 at 23:01
  • @DanielBallinger (As far as i know) Only multipart posts can contain the attachments. Jan 14, 2014 at 5:42

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .