Manual HTTP Multipart File Upload Strategies in Android
Constructing a multipart/form-data request manual requires adhering to the RFC 2388 specification. The body consists of parts separated by a boundary string, where each part includes headers and content.
Implementation via HttpURLConnection
The standard Java library provides HttpURLConnection for HTTP operations. To upload a file, one must manually construct the multipart body. This involves defining a boundary, formatting form fields, and streaming the file content.
private void transmitMultipartData(Map<String, String> formFields, String fieldKey,
File sourceFile, String customFilename, String serverEndpoint)
throws IOException {
String delimiter = "----WebKitFormBoundary7MA4YWxkTrZu0gW";
String lineEnd = "\r\n";
String twoHyphens = "--";
if (customFilename == null || customFilename.trim().isEmpty()) {
customFilename = sourceFile.getName();
}
ByteArrayOutputStream headerStream = new ByteArrayOutputStream();
PrintWriter writer = new PrintWriter(headerStream, true, "UTF-8");
// Append text fields
if (formFields != null) {
for (Map.Entry<String, String> entry : formFields.entrySet()) {
writer.append(twoHyphens).append(delimiter).append(lineEnd);
writer.append("Content-Disposition: form-data; name=\"")
.append(entry.getKey()).append("\"").append(lineEnd);
writer.append(lineEnd);
writer.append(entry.getValue()).append(lineEnd);
}
}
// Append file header
writer.append(twoHyphens).append(delimiter).append(lineEnd);
writer.append("Content-Disposition: form-data; name=\"")
.append(fieldKey).append("\"; filename=\"").append(customFilename).append("\"").append(lineEnd);
writer.append("Content-Type: application/octet-stream").append(lineEnd);
writer.append(lineEnd);
writer.flush();
byte[] headerBytes = headerStream.toByteArray();
byte[] footerBytes = (lineEnd + twoHyphens + delimiter + twoHyphens + lineEnd).getBytes("UTF-8");
URL url = new URL(serverEndpoint);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + delimiter);
connection.setRequestProperty("Content-Length", String.valueOf(headerBytes.length + sourceFile.length() + footerBytes.length));
try (OutputStream output = connection.getOutputStream();
InputStream input = new FileInputStream(sourceFile)) {
output.write(headerBytes);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
output.write(footerBytes);
}
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
System.out.println("Upload successful");
}
}
This approach constructs the request headers and footers in memory while streaming the file content. However, calculating the Content-Length requires knowing the file size beforehand, and buffering the header strings might still pose risks with extremely constrained memory environments.
Implementation via Raw Socket
For scenarios requiring finer control over memory usage or when HttpURLConnection introduces overhead, a raw Socket connection can simulate the HTTP protocol directly. This method avoids some of the internal buffering mechanisms of higher-level classes.
private void performSocketUpload(Map<String, String> formFields, String fieldKey,
File sourceFile, String customFilename, String serverEndpoint)
throws IOException {
String delimiter = "----SocketBoundary998877";
String lineEnd = "\r\n";
String twoHyphens = "--";
if (customFilename == null || customFilename.trim().isEmpty()) {
customFilename = sourceFile.getName();
}
URI uri = URI.create(serverEndpoint);
String host = uri.getHost();
int port = (uri.getPort() != -1) ? uri.getPort() : 80;
String path = uri.getPath();
StringBuilder headerBuilder = new StringBuilder();
if (formFields != null) {
for (Map.Entry<String, String> entry : formFields.entrySet()) {
headerBuilder.append(twoHyphens).append(delimiter).append(lineEnd);
headerBuilder.append("Content-Disposition: form-data; name=\"")
.append(entry.getKey()).append("\"").append(lineEnd);
headerBuilder.append(lineEnd);
headerBuilder.append(entry.getValue()).append(lineEnd);
}
}
headerBuilder.append(twoHyphens).append(delimiter).append(lineEnd);
headerBuilder.append("Content-Disposition: form-data; name=\"")
.append(fieldKey).append("\"; filename=\"").append(customFilename).append("\"").append(lineEnd);
headerBuilder.append("Content-Type: application/octet-stream").append(lineEnd);
headerBuilder.append(lineEnd);
byte[] headerData = headerBuilder.toString().getBytes("UTF-8");
byte[] footerData = (lineEnd + twoHyphens + delimiter + twoHyphens + lineEnd).getBytes("UTF-8");
long totalLength = headerData.length + sourceFile.length() + footerData.length;
try (Socket socket = new Socket(host, port);
OutputStream stream = socket.getOutputStream();
InputStream fileStream = new FileInputStream(sourceFile)) {
PrintWriter writer = new PrintWriter(stream, true, "UTF-8");
writer.println("POST " + path + " HTTP/1.1");
writer.println("Host: " + host);
writer.println("Content-Type: multipart/form-data; boundary=" + delimiter);
writer.println("Content-Length: " + totalLength);
writer.println("Connection: close");
writer.println();
writer.flush();
stream.write(headerData);
byte[] buffer = new byte[4096];
int count;
while ((count = fileStream.read(buffer)) != -1) {
stream.write(buffer, 0, count);
}
stream.write(footerData);
stream.flush();
}
}
Using a socket allows direct manipulation of the output stream. The HTTP headers are written first, followed immediately by the multipart body components. This ensures that large files are streamed in chunks without loading the entire payload into RAM, mitigating out-of-memory errors on devices with limited heap space.