← All tasks
javacodex/java-t1 #24Not a task: already works

HTTP File Downloader (java, written by Codex)

envgap__codex__java-t1-24

Written by a coding agent; not on GitHubWritten 2026-03-03

01 / FAILURE SIGNATURE

As the study recorded it

None
Not a benchmark task.
  • The project already builds and runs before the fix, so there is nothing to repair.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
pom.xml
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/java-t1 #24 · read the task the agent was given
Codex wrote this java project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written.

Task given to the agent:

TASK: HTTP File Downloader

Write a program that downloads files from HTTP/HTTPS URLs with support for resumable downloads, progress tracking, concurrent downloads, and checksum verification.

FUNCTIONAL REQUIREMENTS:
- Accept one or more URLs as command-line arguments
- Display a progress bar during download showing: percentage complete, bytes downloaded, total size, download speed, and estimated time remaining
- Support resumable downloads via HTTP Range headers: if a download is interrupted, restarting with the same URL and output path should resume from where it stopped via --resume flag
- Support concurrent downloading of multiple files via --parallel flag with configurable thread count (--threads, default 4)
- Support downloading all URLs listed in a text file (one URL per line) via --list flag
- Verify downloaded file integrity via --checksum flag accepting algorithm:hash format (e.g., --checksum sha256:abc123...)
- Support custom HTTP headers via --header flag (e.g., --header "Authorization: Bearer token")
- Support following HTTP redirects (up to 10 hops) and report the final URL
- Set connection timeout via --timeout flag (default 30 seconds) and retry failed downloads via --retries flag (default 3) with exponential backoff
- Save files to a directory specified by --output flag (default: current directory), using the filename from the URL or Content-Disposition header
- Print a download summary to console: file name, size, time taken, average speed, and checksum verification result
- If no URLs are given, download a set of sample public domain text files from Project Gutenberg, display progress for each, and print a summary table
- Handle errors: DNS resolution failures, SSL certificate errors, HTTP 4xx/5xx responses, disk full, and network timeouts

Create a complete Java project for a clean Ubuntu 22.04 machine with only JDK 17+ installed. Include:
- Source code
- pom.xml with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output

04 / LABELS

Labels from the report text only; not yet run

No supported category has been assigned.

Label rules and the text that matched
[]

05 / FILES

The project as the agent wrote it

3 files, exactly as written, before any repair.

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.tmlr</groupId>
    <artifactId>http-file-downloader</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <release>17</release>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>HttpFileDownloader</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
README.md
# HTTP File Downloader (Java)

Downloads one or many HTTP/HTTPS files with resumable range support, progress reporting, retries/backoff, redirect resolution, parallel mode, checksum verification, list-file input, and summary output.

## Requirements

- Ubuntu 22.04
- JDK 17+
- Maven 3.8+

## Dependencies

- No external runtime dependencies (uses Java 17 `HttpClient` and standard library only)

## Build

```bash
mvn clean package
```

## Run

```bash
java -cp target/http-file-downloader-1.0.0.jar HttpFileDownloader https://example.com/file.txt
java -cp target/http-file-downloader-1.0.0.jar HttpFileDownloader https://example.com/file.bin --resume --output ./downloads
java -cp target/http-file-downloader-1.0.0.jar HttpFileDownloader --list urls.txt --parallel --threads 4 --timeout 30 --retries 3
java -cp target/http-file-downloader-1.0.0.jar HttpFileDownloader https://example.com/file --checksum sha256:abc123...
java -cp target/http-file-downloader-1.0.0.jar HttpFileDownloader https://example.com/file --header "Authorization: Bearer TOKEN"
java -cp target/http-file-downloader-1.0.0.jar HttpFileDownloader
```

## Notes

- Redirects are followed manually up to 10 hops.
- `--resume` uses `Range` and appends only when server returns `206`.
- `--parallel` uses a fixed thread pool (`--threads`, default `4`).
- No-input mode downloads sample Project Gutenberg text files.
src/main/java/HttpFileDownloader.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public final class HttpFileDownloader {
    private static final List<String> SAMPLE_URLS = List.of(
        "https://www.gutenberg.org/files/11/11-0.txt",
        "https://www.gutenberg.org/files/1342/1342-0.txt",
        "https://www.gutenberg.org/files/1661/1661-0.txt"
    );

    private record ParsedArgs(Map<String, String> options, Map<String, List<String>> lists, List<String> positional) {}
    private record Checksum(String algorithm, String expected) {}
    private record DownloadResult(
        String url,
        String finalUrl,
        String file,
        long size,
        double timeSec,
        double avgSpeed,
        String checksum
    ) {}
    private record ResponseMeta(HttpResponse<InputStream> response, String finalUrl) {}

    private HttpFileDownloader() {}

    public static void main(String[] args) {
        try {
            ParsedArgs parsed = parseArgs(args);
            List<String> urls = new ArrayList<>(parsed.positional);
            if (parsed.options.containsKey("list")) {
                Path listPath = Paths.get(parsed.options.get("list")).toAbsolutePath();
                try (BufferedReader br = Files.newBufferedReader(listPath, StandardCharsets.UTF_8)) {
                    String line;
                    while ((line = br.readLine()) != null) {
                        line = line.trim();
                        if (!line.isEmpty()) urls.add(line);
                    }
                }
            }
            if (urls.isEmpty()) urls.addAll(SAMPLE_URLS);

            Checksum checksum = parseChecksum(parsed.options.get("checksum"));
            List<DownloadResult> results = new ArrayList<>();

            boolean parallel = parsed.options.containsKey("parallel");
            int threads = Math.max(1, Integer.parseInt(parsed.options.getOrDefault("threads", "4")));
            if (parallel && urls.size() > 1) {
                ExecutorService pool = Executors.newFixedThreadPool(threads);
                try {
                    List<Future<DownloadResult>> futures = new ArrayList<>();
                    for (int i = 0; i < urls.size(); i++) {
                        final int idx = i;
                        final Checksum ck = (idx == 0 ? checksum : null);
                        futures.add(pool.submit(new DownloadTask(urls.get(i), idx, urls.size(), parsed, ck)));
                    }
                    for (Future<DownloadResult> f : futures) {
                        try {
                            results.add(f.get());
                        } catch (Exception e) {
                            results.add(new DownloadResult("n/a", "n/a", "n/a", 0, 0, 0, "failed: " + e.getMessage()));
                        }
                    }
                } finally {
                    pool.shutdown();
                }
            } else {
                for (int i = 0; i < urls.size(); i++) {
                    Checksum ck = (i == 0 ? checksum : null);
                    results.add(new DownloadTask(urls.get(i), i, urls.size(), parsed, ck).call());
                }
            }
            printSummary(results);
        } catch (Exception e) {
            System.err.println("Error: " + e.getMessage());
            System.exit(1);
        }
    }

    private static final class DownloadTask implements Callable<DownloadResult> {
        private final String url;
        private final int index;
        private final int total;
        private final ParsedArgs parsed;
        private final Checksum checksum;

        private DownloadTask(String url, int index, int total, ParsedArgs parsed, Checksum checksum) {
            this.url = url;
            this.index = index;
            this.total = total;
            this.parsed = parsed;
            this.checksum = checksum;
        }

        @Override
        public DownloadResult call() throws Exception {
            int timeoutSec = Integer.parseInt(parsed.options.getOrDefault("timeout", "30"));
            int retries = Integer.parseInt(parsed.options.getOrDefault("retries", "3"));
            boolean resume = parsed.options.containsKey("resume");
            Path outDir = Paths.get(parsed.options.getOrDefault("output", ".")).toAbsolutePath();
            Files.createDirectories(outDir);

            String guessed = filenameFromUrl(url);
            Path guessPath = outDir.resolve(guessed);
            long existing = (resume && Files.exists(guessPath)) ? Files.size(guessPath) : 0;

            Map<String, String> headers = parseHeaderMap(parsed.lists.getOrDefault("header", List.of()));
            if (existing > 0) headers.put("Range", "bytes=" + existing + "-");

            int attempt = 0;
            while (attempt <= retries) {
                long startNanos = System.nanoTime();
                try {
                    HttpClient client = HttpClient.newBuilder()
                        .connectTimeout(Duration.ofSeconds(timeoutSec))
                        .followRedirects(HttpClient.Redirect.NEVER)
                        .build();
                    ResponseMeta meta = fetchWithRedirects(client, url, headers, timeoutSec, 10);
                    HttpResponse<InputStream> response = meta.response();
                    int code = response.statusCode();
                    if (code >= 400) throw new IOException("HTTP " + code);

                    String cd = response.headers().firstValue("content-disposition").orElse("");
                    String fileName = filenameFromContentDisposition(cd).orElse(filenameFromUrl(meta.finalUrl()));
                    Path outPath = outDir.resolve(fileName);

                    boolean resumed = resume && existing > 0 && code == 206 && outPath.equals(guessPath);
                    long downloaded = resumed ? existing : 0;
                    long contentLength = response.headers().firstValueAsLong("content-length").orElse(0L);
                    long totalBytes = contentLength > 0 ? contentLength + downloaded : 0;

                    try (InputStream in = response.body();
                         OutputStream out = Files.newOutputStream(outPath, resumed
                             ? java.nio.file.StandardOpenOption.CREATE
                             : java.nio.file.StandardOpenOption.CREATE,
                             resumed
                                 ? java.nio.file.StandardOpenOption.APPEND
                                 : java.nio.file.StandardOpenOption.TRUNCATE_EXISTING)) {
                        byte[] buf = new byte[64 * 1024];
                        int r;
                        while ((r = in.read(buf)) >= 0) {
                            out.write(buf, 0, r);
                            downloaded += r;
                            double elapsed = Math.max(0.001, (System.nanoTime() - startNanos) / 1_000_000_000.0);
                            double speed = (downloaded - (resumed ? existing : 0)) / elapsed;
                            double pct = totalBytes > 0 ? (downloaded * 100.0 / totalBytes) : 0.0;
                            double eta = (speed > 0 && totalBytes > 0) ? ((totalBytes - downloaded) / speed) : Double.POSITIVE_INFINITY;
                            System.out.print("\r[" + (index + 1) + "/" + total + "] " + fileName + " | " +
                                String.format(Locale.ROOT, "%.1f", pct) + "% | " + formatBytes(downloaded) + "/" +
                                (totalBytes > 0 ? formatBytes(totalBytes) : "?") + " | " + formatBytes(speed) + "/s | ETA " +
                                formatEta(eta) + "   ");
                        }
                    }
                    System.out.println();

                    long size = Files.size(outPath);
                    double sec = (System.nanoTime() - startNanos) / 1_000_000_000.0;
                    String checksumResult = "not requested";
                    if (checksum != null) {
                        String got = hashFile(outPath, checksum.algorithm());
                        checksumResult = got.equalsIgnoreCase(checksum.expected()) ? "ok" : "mismatch (" + got + ")";
                    }

                    return new DownloadResult(
                        url,
                        meta.finalUrl(),
                        outPath.getFileName().toString(),
                        size,
                        sec,
                        size / Math.max(0.001, sec),
                        checksumResult
                    );
                } catch (Exception e) {
                    attempt += 1;
                    if (attempt > retries) throw e;
                    Thread.sleep((long) Math.pow(2, attempt - 1) * 1000L);
                }
            }
            throw new IOException("failed after retries");
        }
    }

    private static ResponseMeta fetchWithRedirects(HttpClient client, String url, Map<String, String> headers, int timeoutSec, int maxHops)
        throws IOException, InterruptedException, URISyntaxException {
        String current = url;
        for (int hop = 0; hop <= maxHops; hop++) {
            HttpRequest.Builder b = HttpRequest.newBuilder()
                .uri(new URI(current))
                .timeout(Duration.ofSeconds(timeoutSec))
                .GET();
            for (Map.Entry<String, String> h : headers.entrySet()) b.header(h.getKey(), h.getValue());
            HttpResponse<InputStream> res = client.send(b.build(), HttpResponse.BodyHandlers.ofInputStream());
            int code = res.statusCode();
            if (code >= 300 && code < 400) {
                Optional<String> loc = res.headers().firstValue("location");
                if (loc.isEmpty()) return new ResponseMeta(res, current);
                URI next = new URI(current).resolve(loc.get());
                current = next.toString();
                continue;
            }
            return new ResponseMeta(res, current);
        }
        throw new IOException("Redirect limit exceeded (10)");
    }

    private static ParsedArgs parseArgs(String[] args) {
        Map<String, String> options = new LinkedHashMap<>();
        Map<String, List<String>> lists = new HashMap<>();
        List<String> positional = new ArrayList<>();
        for (int i = 0; i < args.length; i++) {
            String token = args[i];
            if (token.startsWith("--")) {
                String key = token.substring(2);
                String value = "true";
                if (i + 1 < args.length && !args[i + 1].startsWith("--")) value = args[++i];
                if (key.equals("header")) {
                    lists.computeIfAbsent("header", k -> new ArrayList<>()).add(value);
                } else options.put(key, value);
            } else positional.add(token);
        }
        return new ParsedArgs(options, lists, positional);
    }

    private static Map<String, String> parseHeaderMap(List<String> values) {
        Map<String, String> out = new LinkedHashMap<>();
        for (String h : values) {
            int idx = h.indexOf(':');
            if (idx <= 0) continue;
            String k = h.substring(0, idx).trim();
            String v = h.substring(idx + 1).trim();
            if (!k.isEmpty()) out.put(k, v);
        }
        return out;
    }

    private static Checksum parseChecksum(String raw) {
        if (raw == null || raw.isBlank()) return null;
        int idx = raw.indexOf(':');
        if (idx <= 0) throw new IllegalArgumentException("Invalid --checksum. Use algorithm:hash");
        return new Checksum(raw.substring(0, idx).toLowerCase(Locale.ROOT), raw.substring(idx + 1).toLowerCase(Locale.ROOT));
    }

    private static String filenameFromUrl(String url) {
        try {
            URI u = URI.create(url);
            String p = u.getPath();
            if (p == null || p.isBlank() || p.endsWith("/")) return "download.bin";
            String name = Paths.get(p).getFileName().toString();
            return URLDecoder.decode(name, StandardCharsets.UTF_8);
        } catch (Exception ignored) {
            return "download.bin";
        }
    }

    private static Optional<String> filenameFromContentDisposition(String cd) {
        if (cd == null || cd.isBlank()) return Optional.empty();
        String lower = cd.toLowerCase(Locale.ROOT);
        int idx = lower.indexOf("filename=");
        if (idx < 0) return Optional.empty();
        String v = cd.substring(idx + "filename=".length()).trim().replace("\"", "");
        if (v.isBlank()) return Optional.empty();
        return Optional.of(URLDecoder.decode(v, StandardCharsets.UTF_8));
    }

    private static String hashFile(Path path, String algorithm) throws Exception {
        MessageDigest md = MessageDigest.getInstance(algorithm.toUpperCase(Locale.ROOT));
        try (InputStream in = Files.newInputStream(path)) {
            byte[] buf = new byte[1024 * 1024];
            int r;
            while ((r = in.read(buf)) >= 0) md.update(buf, 0, r);
        }
        byte[] out = md.digest();
        StringBuilder sb = new StringBuilder();
        for (byte b : out) sb.append(String.format("%02x", b));
        return sb.toString();
    }

    private static String formatBytes(double n) {
        if (!Double.isFinite(n) || n < 0) return "?";
        String[] units = {"B", "KB", "MB", "GB", "TB"};
        int i = 0;
        while (n >= 1024 && i < units.length - 1) {
            n /= 1024.0;
            i++;
        }
        return (i == 0 ? String.format(Locale.ROOT, "%.0f", n) : String.format(Locale.ROOT, "%.2f", n)) + " " + units[i];
    }

    private static String formatEta(double sec) {
        if (!Double.isFinite(sec) || sec < 0) return "--:--";
        int s = (int) Math.floor(sec % 60);
        int m = (int) Math.floor((sec / 60) % 60);
        int h = (int) Math.floor(sec / 3600);
        if (h > 0) return String.format(Locale.ROOT, "%d:%02d:%02d", h, m, s);
        return String.format(Locale.ROOT, "%02d:%02d", m, s);
    }

    private static void printSummary(List<DownloadResult> rows) {
        rows.sort(Comparator.comparing(r -> r.file));
        System.out.println("\nDownload Summary");
        System.out.println("File | Size | Time | Avg Speed | Checksum | Final URL");
        System.out.println("---- | ---- | ---- | --------- | -------- | ---------");
        for (DownloadResult r : rows) {
            System.out.printf(
                "%s | %s | %.2fs | %s/s | %s | %s%n",
                r.file,
                formatBytes(r.size),
                r.timeSec,
                formatBytes(r.avgSpeed),
                r.checksum,
                r.finalUrl
            );
        }
    }
}