In my personal experience, composing HTTP requests with multipart form data to send to a remote endpoint has not been a straightforward story in Salesforce Apex code. I didn’t find any built-in helper classes to do this. After many attempts and going down various avenues that led to dead-ends, I was finally able to cobble up a working solution. But before we get to the solution, let’s take a look at the problem:
Background Story (and Use Case)
Recently, I had to write some custom Apex code to make some requests against a legacy API. In this particular case, I’m being a bit generous with the term “API”; it is simply a remote endpoint that breaks a lot of the rules and ergonomics of a well-built API:
- There was only one endpoint, to service all requests. There is a JSON blob that you pass to this one endpoint which has an “action” attribute that specifies the action you’re trying to take against the remote system.
- HTTP verbs were not utilized properly. Everything is a POST. And when the system responds, it always gives you a 200 OK, even for errors – you have to parse the return JSON to see if you got an actual response or an error message.
- In certain cases, I needed to pass this endpoint binary data. PDF files. In those instances, it didn’t take the binary data as a JSON property, perhaps as a Base64 encoded string, like it did with everything else but required you to send it as part of a multipart/form-data submission. That’s what lead me to figuring out how to do that in Apex and hence, this blog post.
If I had to draw a picture of what my request payload needed to look like, it is something akin to this:

Your need may look like this:

In essence, if you need to make an HTTP post in Apex mixing in some key-value pairs of textual data along with a binary file, I have a solution, below:
Show me the Code
The biggest piece of the puzzle is a class that I found on GitHub, here:
To avoid the risk of that code changing on me or becoming inaccessible, here’s my raw version, with class names changed to make it a bit more generic as it is certainly a multipurpose helper class:
public virtual class HttpMultipartFormDataManager {
// The boundary is alligned so it doesn't produce padding characters when base64 encoded.
private final static string Boundary = '1ff13444ed8140c7a32fc4e6451aa76d';
public static String getBoundary() {
return Boundary;
}
/**
* Returns the request's content type for multipart/form-data requests.
*/
public static string GetContentType() {
return 'multipart/form-data; charset="UTF-8"; boundary="' + Boundary + '"';
}
/**
* Pad the value with spaces until the base64 encoding is no longer padded.
*/
public static string SafelyPad(
string value,
string valueCrLf64,
string lineBreaks) {
string valueCrLf = '';
blob valueCrLfBlob = null;
while (valueCrLf64.endsWith('=')) {
value += ' ';
valueCrLf = value + lineBreaks;
valueCrLfBlob = blob.valueOf(valueCrLf);
valueCrLf64 = EncodingUtil.base64Encode(valueCrLfBlob);
}
return valueCrLf64;
}
/**
* Write a boundary between parameters to the form's body.
*/
public static string WriteBoundary() {
string value = '--' + Boundary + '\r\n';
blob valueBlob = blob.valueOf(value);
return EncodingUtil.base64Encode(valueBlob);
}
/**
* Write a boundary at the end of the form's body.
*/
public static string WriteBoundary(
EndingType ending) {
string value = '';
if (ending == EndingType.Cr) {
// The file's base64 was padded with a single '=',
// so it was replaced with '\r'. Now we have to
// prepend the boundary with '\n' to complete
// the line break.
value += '\n';
} else if (ending == EndingType.None) {
// The file's base64 was not padded at all,
// so we have to prepend the boundary with
// '\r\n' to create the line break.
value += '\r\n';
}
// Else:
// The file's base64 was padded with a double '=',
// so they were replaced with '\r\n'. We don't have to
// do anything to the boundary because there's a complete
// line break before it.
value += '--' + Boundary + '--';
blob valueBlob = blob.valueOf(value);
return EncodingUtil.base64Encode(valueBlob);
}
/**
* Write a key-value pair to the form's body.
*/
public static string WriteBodyParameter(
string key,
string value) {
string contentDisposition = 'Content-Disposition: form-data; name="' + key + '"';
string contentDispositionCrLf = contentDisposition + '\r\n\r\n';
blob contentDispositionCrLfBlob = blob.valueOf(contentDispositionCrLf);
string contentDispositionCrLf64 = EncodingUtil.base64Encode(contentDispositionCrLfBlob);
string content = SafelyPad(contentDisposition, contentDispositionCrLf64, '\r\n\r\n');
string valueCrLf = value + '\r\n';
blob valueCrLfBlob = blob.valueOf(valueCrLf);
string valueCrLf64 = EncodingUtil.base64Encode(valueCrLfBlob);
content += SafelyPad(value, valueCrLf64, '\r\n');
return content;
}
/**
* Write a key-value pair to the form's body for a blob.
*/
public static string WriteBlobBodyParameter(string key, string file64, string fileName) {
String mimeType = resolveMimeType(fileName);
string contentDisposition = 'Content-Disposition: form-data; name="' + key + '"; filename="'+fileName+'"';
string contentDispositionCrLf = contentDisposition + '\r\n';
blob contentDispositionCrLfBlob = blob.valueOf(contentDispositionCrLf);
string contentDispositionCrLf64 = EncodingUtil.base64Encode(contentDispositionCrLfBlob);
string content = SafelyPad(contentDisposition, contentDispositionCrLf64, '\r\n');
string contentTypeHeader = 'Content-Type: ' + mimeType;
string contentTypeCrLf = contentTypeHeader + '\r\n\r\n';
blob contentTypeCrLfBlob = blob.valueOf(contentTypeCrLf);
string contentTypeCrLf64 = EncodingUtil.base64Encode(contentTypeCrLfBlob);
content += SafelyPad(contentTypeHeader, contentTypeCrLf64, '\r\n\r\n');
integer file64Length = file64.length();
String last4Bytes = file64.substring(file64.length()-4,file64.length());
// Avoid padding the file data with spaces, which SafelyPad does
// http://salesforce.stackexchange.com/a/33326/102
EndingType ending = EndingType.None;
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';
file64 = file64.substring(0,file64.length()-4) + last4Bytes;
// We have appended the \r\n to the Blob, so leave footer as it is.
ending = EndingType.CrLf;
} 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
last4Bytes = last4Bytes.substring(0,3) + 'N';
file64 = file64.substring(0,file64.length()-4) + last4Bytes;
// We have appended the CR e.g. \r, still need to prepend the line feed to the footer
ending = EndingType.Cr;
}
content += file64;
content += WriteBoundary(ending);
return content;
}
private static String resolveMimeType(String fileName) {
String fileType = fileName.substringAfterLast('.');
String mimeType = 'image/png'; // fallback value
if (fileType.equalsIgnoreCase('png')) {
mimeType = 'image/png';
} else if (fileType.equalsIgnoreCase('jpeg') || fileType.equalsIgnoreCase('jpg')) {
mimeType = 'image/jpg';
} else if (fileType.equalsIgnoreCase('pgm')) {
mimeType = 'image/x-portable-graymap';
} else if (fileType.equalsIgnoreCase('ppm')) {
mimeType = 'image/x-portable-pixmap';
} else if (fileType.equalsIgnoreCase('pdf')) {
mimeType = 'application/pdf';
}
return mimeType;
}
/**
* Helper enum indicating how a file's base64 padding was replaced.
*/
public enum EndingType {
Cr,
CrLf,
None
}
}
With the aid of the above helper class, you can construct a multipart/form-data request like so:
Http http = new Http();
HttpRequest multipartRequest = new HttpRequest();
multipartRequest.setMethod('POST');
multipartRequest.setEndpoint("some-url");
multipartRequest.setHeader('Content-Type', HttpMultipartFormDataManager.GetContentType());
// TODO: Get the binary data that you want to send as part of the multipart/form-data
// and put that data into a Blob variable.
Blob profilePicBlob = (get blob data from somewhere, here);
// Compose form
string form64 = '';
form64 += HttpMultipartFormDataManager.WriteBoundary();
form64 += HttpMultipartFormDataManager.WriteBodyParameter('name', 'John Doe');
form64 += HttpMultipartFormDataManager.WriteBoundary();
form64 += HttpMultipartFormDataManager.WriteBodyParameter('email', 'john-doe@test.com');
form64 += HttpMultipartFormDataManager.WriteBoundary();
form64 += HttpMultipartFormDataManager.WriteBodyParameter('dob', 'Jan 1, 1981');
form64 += HttpMultipartFormDataManager.WriteBoundary();
form64 += HttpMultipartFormDataManager.WriteBodyParameter('favorite-color', 'blue');
form64 += HttpMultipartFormDataManager.WriteBoundary();
form64 += HttpMultipartFormDataManager.WriteBlobBodyParameter('profile-pic', EncodingUtil.base64Encode(profilePicBlob), 'my-profile-pic.jpg');
Blob formBlob = EncodingUtil.base64Decode(form64);
string contentLength = string.valueOf(formBlob.size());
multipartRequest.setBodyAsBlob(formBlob);
HttpResponse multipartResponse = new HttpResponse();
multipartResponse = http.send(multipartRequest);
if(multipartResponse.getStatusCode() == 200) {
System.debug('Yaaaay!');
System.debug(multipartResponse.getBody());
}else{
System.debug('Blah! ' + String.valueOf(multipartResponse.getStatusCode()) + multipartResponse.getBody());
}
Some things to highlight, in the above code:
- Each item in the form is separated by a boundary, a string tag of sorts.
- The binary data, enclosed in a blob is first Base64 encoded and then written as a body parameter.
- The entire request body is then Base64 decoded and set as a blob body in the HttpRequest object.
Parting Thoughts
If you are so inclined, you can dig a bit deeper into some of the challenges associated with composing and sending a request that mixes in some binary data. I’ll link a couple of posts that I came across in my explorations that shed a bit more light into what those challenges actually are:
Post File From Salesforce Apex to External HTTP Webservices – Docparser
Nerd @ Work: [Salesforce / Apex] POST Mutipart/form-data with HttpRequest (enreeco.blogspot.com)
