DEV Community

paohaijiao
paohaijiao

Posted on Originally published at paohaijiao.hashnode.dev

Downloads Without Boilerplate: Saving HTTP Responses to Local Disk with `--output`

Downloading a file in OkHttp means streaming ResponseBody, choosing a sink, and closing resources correctly. In JQuickCurl, a download is just another curl command: add --output './download/report.pdf' (or -o) and the executor writes the response bytes to that path for you. No InputStream juggling, no try-with-resources plumbing in your business code.

This post covers:

  • The --output one-liner that saves straight to disk.

  • Returning byte[] and saving in Java for full control.

  • Combined approaches and directory-handling notes.

  • A runnable demo against a real image endpoint.

Approach 1: Let the Command Write the File

The cleanest option. Add --output <path> to the curl command; the file appears at the path when the call returns.

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

public interface DownloadApi {

    @JCurlCommand("curl -X GET 'https://httpbin.org/image/png' "
            + "--output './download/diagram.png'")
    byte[] downloadPng(JQuickCurlReq request);
}
Enter fullscreen mode Exit fullscreen mode
import com.github.paohaijiao.executor.JCurlInvoker;

public class DownloadDemo {
    public static void main(String[] args) throws Exception {
        DownloadApi api = JCurlInvoker.createProxy(DownloadApi.class);
        byte[] bytes = api.downloadPng(new JQuickCurlReq());

        java.nio.file.Path file = java.nio.file.Paths.get("./download/diagram.png");
        System.out.println("Saved " + bytes.length + " bytes to "
                + file.toAbsolutePath());
        // -> Saved NNNN bytes to .../download/diagram.png
    }
}
Enter fullscreen mode Exit fullscreen mode

Two things happen: the executor writes ./download/diagram.png, and (because the method still declares byte[]) you also receive the bytes — handy for checksumming or counting without an extra read. The directory must exist; JQuickCurl writes the file but does not create parent folders for you.

Approach 2: Return the Bytes and Save in Java

If the storage location is business logic (a database column, an S3 bucket, a temp dir chosen per tenant), skip --output and save the byte[] yourself:

public interface DownloadApi {

    @JCurlCommand("curl -X GET 'https://httpbin.org/bytes/4096'")
    byte[] downloadBytes(JQuickCurlReq request);
}
Enter fullscreen mode Exit fullscreen mode
byte[] bytes = api.downloadBytes(new JQuickCurlReq());

java.nio.file.Files.write(
        java.nio.file.Paths.get("./download/random-4k.bin"), bytes);
Enter fullscreen mode Exit fullscreen mode

Return types handled by the converter include byte[], and JResult for raw access. Choose by payload size:

Payload Return type
Small / needs random access byte[]
Medium, file target known in command --output in command (any return)
Need metadata too JResult

Directory Setup and Path Conventions

  • Pre-create the target directory (e.g., ./download) before the first call.

  • Prefer forward slashes — --output './download/report.xlsx' is portable.

  • On Windows, absolute paths like 'd://test//piett.xlsx' are the spelling used in the project's own XML samples; Java strings will treat \ as escapes, so / is the low-friction choice.

  • For versioned downloads, inject the file name via a variable:

@JCurlCommand("curl -X GET 'https://api.example.com/files/${fileId}' "
        + "-H 'Accept: application/octet-stream' "
        + "--output './download/${fileId}'")
byte[] downloadById(JQuickCurlReq request);
Enter fullscreen mode Exit fullscreen mode
JQuickCurlReq req = new JQuickCurlReq();
req.put("fileId", "invoice-2026-03.pdf");
byte[] bytes = api.downloadById(req);
// file lands at ./download/invoice-2026-03.pdf
Enter fullscreen mode Exit fullscreen mode

Complete Runnable Demo

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

public interface MediaApi {

    // Image with a header check to prove headers are respected
    @JCurlCommand("curl -X GET 'https://httpbin.org/image/jpeg' "
            + "-H 'Accept: image/jpeg' "
            + "--output './download/photo.jpg'")
    byte[] grabPhoto(JQuickCurlReq request);
}

class SaveToDiskDemo {
    public static void main(String[] args) throws Exception {
        java.nio.file.Files.createDirectories(java.nio.file.Paths.get("./download"));

        MediaApi api = JCurlInvoker.createProxy(MediaApi.class);
        byte[] bytes = api.grabPhoto(new JQuickCurlReq());

        System.out.println("Received " + bytes.length + " bytes");
        // Then verify the file on disk:
        long onDisk = java.nio.file.Files.size(
                java.nio.file.Paths.get("./download/photo.jpg"));
        System.out.println("On disk      : " + onDisk + " bytes");
    }
}
Enter fullscreen mode Exit fullscreen mode

Summary

Downloads collapse into one declarative flag: --output './path' writes the response to disk, while return types byte[] keep the memory footprint in your control. Combined with ${...} variables for file names, JQuickCurl handles the whole download lifecycle with the syntax every engineer already knows.

Code and docs live at dromara/jquick-curl. Post 11 moves from per-request concerns to the process level: global tuning of connection pools, timeouts, retries, and redirects.

java #springboot #httpclient #opensource #java-library

Top comments (0)