12

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
20

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);


}
  • 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 '14 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 '14 at 6:20
12

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);
}   
  • 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 '14 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 – Enrico Murru Oct 17 '14 at 8:18
  • I'm curious why the Content-Length parameter isn't being set in this code. – David Cheng Aug 3 '16 at 22:31
  • Hi, Can anybody please comment how to add one more form parameter along with file? – SFIntegrator Mar 14 '18 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); 
  • 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 '14 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 '14 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. – Daniel Ballinger Jan 13 '14 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 '14 at 23:01
  • @DanielBallinger (As far as i know) Only multipart posts can contain the attachments. – Ganesh Bhosle Jan 14 '14 at 5:42

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.