DEV Community

paohaijiao
paohaijiao

Posted on Originally published at paohaijiao.hashnode.dev

Multipart Uploads Made Simple: Sending Files with curl's `-F` Flag in Java

Every admin console ends up with a file upload — Excel imports, avatar pictures, PDF attachments. Implementing multipart requests by hand in OkHttp or RestTemplate means building MultipartBody pieces, computing Content-Type boundaries, and leaking HTTP plumbing into service code. In JQuickCurl you simply keep curl's -F file=@/path/to/file syntax: the upload command is the curl command.

We'll cover:

  • Single-file upload.
  • Multiple files in one request.
  • Mixing files with plain form fields.
  • Dynamic file paths through variables.
  • A runnable example against httpbin's echo endpoint.

The curl Syntax Refresher

# one file
curl -X POST https://api.example.com/upload -F 'file=@./photo.png'

# two files under the SAME field name
curl -X POST https://api.example.com/upload \
  -F 'files=@./a.pdf' -F 'files=@./b.pdf'

# a file plus regular form fields
curl -X POST https://api.example.com/upload \
  -F 'userId=123' \
  -F 'note=hello' \
  -F 'file=@./photo.png'
Enter fullscreen mode Exit fullscreen mode

The magic marker is @: key=@path means this value is a file on disk; key=value without @ is a plain text field. JQuickCurl's parser understands exactly this convention and builds the corresponding multipart body over OkHttp.

Example 1: Single-File Upload

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;

public interface UploadApi {

    @JCurlCommand("curl -X POST 'https://httpbin.org/post' "
            + "-F 'avatar=@./profile.png'")
    String uploadAvatar(JQuickCurlReq request);
}
Enter fullscreen mode Exit fullscreen mode
JQuickCurlReq req = new JQuickCurlReq();
String result = JCurlInvoker.createProxy(UploadApi.class).uploadAvatar(req);
System.out.println(result);   // httpbin echoes back the "files" section
Enter fullscreen mode Exit fullscreen mode

httpbin's /post returns "files": {"avatar": "<binary content>"}, which proves the multipart body really was transmitted.

Example 2: Multiple Files Under One Field

Uploading several files at once simply repeats -F with the same key:

public interface UploadApi {

    @JCurlCommand("curl -X POST 'https://httpbin.org/post' "
            + "-F 'documents=@./report-q1.pdf' "
            + "-F 'documents=@./report-q2.pdf'")
    String uploadQuarterlyReports(JQuickCurlReq request);
}
Enter fullscreen mode Exit fullscreen mode

The same pattern works for a set of related files such as images or logs. For truly arbitrary numbers of files, pair the -F command with a small loop that builds several commands or pick a fixed upper bound per call.

Example 3: File + Ordinary Form Fields (The Importer)

Real import endpoints rarely want a bare file — they want metadata alongside it. Combine fields and files freely:

public interface UploadApi {

    @JCurlCommand("curl -X POST 'https://httpbin.org/post' "
            + "-F 'uploader=${uploader}' "
            + "-F 'description=${description}' "
            + "-F 'spreadsheet=@./products.xlsx'")
    String importSpreadsheet(JQuickCurlReq request);
}
Enter fullscreen mode Exit fullscreen mode
JQuickCurlReq req = new JQuickCurlReq();
req.put("uploader", "jane.doe");
req.put("description", "monthly product refresh");
req.put("spreadsheet", "./products.xlsx");
String out = api.importSpreadsheet(req);
Enter fullscreen mode Exit fullscreen mode

The server receives form fields uploader and description plus file spreadsheet — everything curl's multipart encoding supports.

Example 4: Dynamic File Paths

Hard-coding a path in the annotation works, but real applications pick the file at runtime. Because -F values participate in variable substitution, the path can come from JQuickCurlReq:

@JCurlCommand("curl -X POST 'https://httpbin.org/post' "
        + "-F 'file=@${filePath}'")
String uploadFromPath(JQuickCurlReq request);
Enter fullscreen mode Exit fullscreen mode
JQuickCurlReq req = new JQuickCurlReq();
req.put("filePath", "downloads/today/report.xlsx");
api.uploadFromPath(req);
Enter fullscreen mode Exit fullscreen mode

Path tips: use forward slashes for portability; on Windows, backslashes inside a Java annotation string must be escaped (\\\\) or prefer /. Relative paths resolve against the process's working directory.

Complete Runnable Demo

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;

public interface MultiUploadApi {

    @JCurlCommand("curl -X POST 'https://httpbin.org/post' "
            + "-F 'owner=${owner}' "
            + "-F 'primary=@./pom.xml' "
            + "-F 'secondary=@./README.md'")
    String uploadPair(JQuickCurlReq request);
}

class UploadDemo {
    public static void main(String[] args) {
        MultiUploadApi api = JCurlInvoker.createProxy(MultiUploadApi.class);

        JQuickCurlReq req = new JQuickCurlReq();
        req.put("owner", "team-core");

        String result = api.uploadPair(req);
        System.out.println(result);
        // Look for "form": { "owner": "team-core" } and
        // "files": { "primary": ..., "secondary": ... } in the echo.
    }
}
Enter fullscreen mode Exit fullscreen mode

Run it from the project root so pom.xml and README.md exist — the demo then needs no extra setup.

When Multipart Is the Wrong Tool

  • Large files / resumable uploads: multipart isn't streaming-friendly for gigabytes; consider chunked or presigned-URL uploads.
  • Tiny key/value pairs: plain JSON or --data-urlencode is lighter than multipart.
  • Files already on a server: pass a URL and let the backend fetch it instead.

Summary

JQuickCurl lets your team keep using the file-upload syntax they already know from curl and Postman. -F 'key=@path' for files, -F 'key=value' for fields, repeated -F options for multi-file uploads, and ${path} variables for runtime-selected files. The multipart boundary choreography stays inside the library.

That covers sending bytes up; Post 10 flips the direction and downloads remote files straight to local disk with curl's --output flag.

java #springboot #httpclient #opensource #java-library

Top comments (0)