Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Manual HTTP Multipart File Upload Strategies in Android

Tech 2

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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.