← All tasks
javaclaude-code/java-t2 #27Lite task

HTTP REST Client with Retry Logic (java, written by Claude Code)

envgap__claude-code__java-t2-27

Written by a coding agent; not on GitHubWritten 2026-02-27

01 / FAILURE SIGNATURE

Captured in a clean container

error: no classes were compiled

02 / ENVIRONMENT RECIPE

Base commit
825184fe868b3f67334ab935e93cab22b6fb85ab
Manifest
pom.xml
Reproduce
mvn -B -q dependency:copy-dependencies -DoutputDirectory=target/dependency -DincludeScope=runtime && cp=$(ls target/dependency/*.jar 2>/dev/null | tr '\n' ':'); test -d target/classes || { echo 'error: no classes were compiled'; exit 1; }; python3 -c 'import hashlib, os, subprocess, sys tracked = [p for p in subprocess.run(["git", "ls-files", "-z", "--", "*.java"], capture_output=True).stdout.decode().split("\0") if p] digest = lambda p: hashlib.sha256(open(p, "rb").read()).hexdigest() own = {digest(p) for p in tracked if os.path.isfile(p)} names = {os.path.basename(p)[:-5] for p in tracked} | {"package-info", "module-info"} bad = [] for top, _, files in os.walk("target"): for name in files: path = os.path.join(top, name) if name.endswith(".java") and digest(path) not in own: bad.append(path) elif top.startswith(os.path.join("target", "classes")) and name.endswith(".class") and name[:-6].split("$")[0] not in names: bad.append(path) if bad: print("\n".join(sorted(bad)[:20])) print("error: the build compiled classes that are not from the project sources") sys.exit(1)' || exit 1; jd=$(jdeps --multi-release 17 -verbose:class -cp "${cp}target/classes" target/classes 2>&1) && st=0 || st=$?; missing=$(printf '%s\n' "$jd" | grep 'not found' || true); if [ $st -ne 0 ]; then printf '%s\n' "$jd" | tail -n 20; echo 'error: jdeps could not read the classes'; exit 1; fi; if [ -n "$missing" ]; then printf '%s\n' "$missing"; echo 'error: classes the program uses are missing from the class path it runs with'; exit 1; fi
Run under trace
rc=0; out=$(timeout 60 java -cp 'target/dependency/*:target/classes' RestClient < /dev/null 2>&1 | { head -c 1000000; cat > /dev/null; }; exit ${PIPESTATUS[0]}) || rc=$?; printf '%s\n' "$out"; env_error='(ModuleNotFoundError|ImportError|No module named|cannot open shared object file|DLL load failed|shared library|cannot load library|Library not loaded|Cannot find module|ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|ERR_REQUIRE_ESM|compiled against a different Node|Could not find or load main class|ClassNotFoundException|NoClassDefFoundError|UnsupportedClassVersionError|UnsatisfiedLinkError|NoSuchMethodError|NoSuchFieldError|AbstractMethodError|IncompatibleClassChangeError|IllegalAccessError|ServiceConfigurationError|error while loading shared libraries|symbol lookup error|version `[^'"'"']*'"'"' not found|command not found)'; asked='(^| )[[:blank:]]*usage:|the following arguments are required|missing (required )?(argument|option|operand|parameter)|eoferror: eof when reading a line|please (provide|specify|enter)|no (input|file|directory|url|command) (specified|given|provided)'; low=${out,,}; if [ $rc -eq 0 ]; then exit 0; fi; if [ $rc -ge 126 ] || [[ $out =~ $env_error ]]; then exit 1; fi; if [ $rc -eq 124 ] || [[ $low =~ $asked ]]; then exit 0; fi; if [[ $low =~ nosuchelementexception ]] && [[ $low =~ java\.util\.scanner ]]; then exit 0; fi; exit 1
Reference environment fix used for admission
--- /dev/null
+++ b/src/main/java/RestClient.java
@@ -0,0 +1,582 @@
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+import org.apache.hc.client5.http.classic.methods.*;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.util.Timeout;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiFunction;
+import java.util.function.Predicate;
+import java.util.logging.*;
+
+/**
+ * HTTP REST Client with Retry Logic using Apache HttpClient5 and Jackson.
+ *
+ * Features:
+ * - GET, POST, PUT, PATCH, DELETE with configurable retry/backoff
+ * - Request chaining with context interpolation
+ * - Response validation with custom rules
+ * - Structured logging
+ */
+public class RestClient {
+
+    // -----------------------------------------------------------------------
+    // Logging
+    // -----------------------------------------------------------------------
+    private static final Logger logger = Logger.getLogger(RestClient.class.getName());
+
+    static {
+        ConsoleHandler handler = new ConsoleHandler();
+        handler.setLevel(Level.ALL);
+        handler.setFormatter(new SimpleFormatter());
+        logger.addHandler(handler);
+        logger.setLevel(Level.INFO);
+        logger.setUseParentHandlers(false);
+    }
+
+    // -----------------------------------------------------------------------
+    // Inner classes
+    // -----------------------------------------------------------------------
+
+    public static class ValidationRule {
+        private final String name;
+        private final Predicate<ResponseRecord> check;
+        private final String message;
+
+        public ValidationRule(String name, Predicate<ResponseRecord> check, String message) {
+            this.name = name;
+            this.check = check;
+            this.message = message;
+        }
+
+        public String getName() { return name; }
+        public String getMessage() { return message; }
+        public boolean test(ResponseRecord record) { return check.test(record); }
+    }
+
+    public static class ValidationException extends RuntimeException {
+        private final List<String> failedRules;
+
+        public ValidationException(List<String> failedRules) {
+            super("Validation failed: " + String.join(", ", failedRules));
+            this.failedRules = failedRules;
+        }
+
+        public List<String> getFailedRules() { return failedRules; }
+    }
+
+    public static class HttpRequestConfig {
+        private String method;
+        private String url;
+        private Map<String, String> headers;
+        private Map<String, String> queryParams;
+        private Object jsonBody;
+        private String label;
+        private List<ValidationRule> validators;
+        private long timeoutSeconds;
+
+        public HttpRequestConfig(String method, String url) {
+            this.method = method;
+            this.url = url;
+            this.headers = new HashMap<>();
+            this.queryParams = new HashMap<>();
+            this.validators = new ArrayList<>();
+            this.label = "";
+            this.timeoutSeconds = 30;
+        }
+
+        public HttpRequestConfig header(String key, String value) { headers.put(key, value); return this; }
+        public HttpRequestConfig param(String key, String value) { queryParams.put(key, value); return this; }
+        public HttpRequestConfig body(Object body) { this.jsonBody = body; return this; }
+        public HttpRequestConfig label(String label) { this.label = label; return this; }
+        public HttpRequestConfig validate(ValidationRule rule) { validators.add(rule); return this; }
+        public HttpRequestConfig timeout(long seconds) { this.timeoutSeconds = seconds; return this; }
+
+        public String getMethod() { return method; }
+        public String getUrl() { return url; }
+        public void setUrl(String url) { this.url = url; }
+        public Map<String, String> getHeaders() { return headers; }
+        public void setHeaders(Map<String, String> headers) { this.headers = headers; }
+        public Map<String, String> getQueryParams() { return queryParams; }
+        public Object getJsonBody() { return jsonBody; }
+        public void setJsonBody(Object body) { this.jsonBody = body; }
+        public String getLabel() { return label; }
+        public List<ValidationRule> getValidators() { return validators; }
+        public long getTimeoutSeconds() { return timeoutSeconds; }
+    }
+
+    public static class ResponseRecord {
+        private final String label;
+        private final String method;
+        private final String url;
+        private final int statusCode;
+        private final double elapsedSeconds;
+        private final Map<String, String> headers;
+        private final String rawBody;
+        private final JsonNode jsonBody;
+        private final boolean ok;
+        private List<String> validationErrors;
+
+        public ResponseRecord(String label, String method, String url, int statusCode,
+                              double elapsedSeconds, Map<String, String> headers,
+                              String rawBody, JsonNode jsonBody, boolean ok) {
+            this.label = label;
+            this.method = method;
+            this.url = url;
+            this.statusCode = statusCode;
+            this.elapsedSeconds = elapsedSeconds;
+            this.headers = headers;
+            this.rawBody = rawBody;
+            this.jsonBody = jsonBody;
+            this.ok = ok;
+            this.validationErrors = new ArrayList<>();
+        }
+
+        public String getLabel() { return label; }
+        public String getMethod() { return method; }
+        public String getUrl() { return url; }
+        public int getStatusCode() { return statusCode; }
+        public double getElapsedSeconds() { return elapsedSeconds; }
+        public Map<String, String> getHeaders() { return headers; }
+        public String getRawBody() { return rawBody; }
+        public JsonNode getJsonBody() { return jsonBody; }
+        public boolean isOk() { return ok && validationErrors.isEmpty(); }
+        public List<String> getValidationErrors() { return validationErrors; }
+        public void setValidationErrors(List<String> errors) { this.validationErrors = errors; }
+    }
+
+    // -----------------------------------------------------------------------
+    // Built-in validators
+    // -----------------------------------------------------------------------
+
+    public static ValidationRule statusOk() {
+        return new ValidationRule("status_ok",
+                r -> r.getStatusCode() >= 200 && r.getStatusCode() < 300,
+                "Expected 2xx status code");
+    }
+
+    public static ValidationRule statusEquals(int code) {
+        return new ValidationRule("status_" + code,
+                r -> r.getStatusCode() == code,
+                "Expected status code " + code);
+    }
+
+    public static ValidationRule jsonHasKey(String key) {
+        return new ValidationRule("has_key_" + key,
+                r -> r.getJsonBody() != null && r.getJsonBody().isObject() && r.getJsonBody().has(key),
+                "Expected JSON key '" + key + "' in response");
+    }
+
+    public static ValidationRule maxResponseTime(double seconds) {
+        return new ValidationRule("max_response_time",
+                r -> r.getElapsedSeconds() <= seconds,
+                "Response took longer than " + seconds + "s");
+    }
+
+    // -----------------------------------------------------------------------
+    // Client fields
+    // -----------------------------------------------------------------------
+
+    private final String baseUrl;
+    private final CloseableHttpClient httpClient;
+    private final ObjectMapper objectMapper;
+    private final Map<String, String> defaultHeaders;
+    private final Set<Integer> retryStatusCodes;
+    private final int maxRetries;
+    private final long backoffMillis;
+    private final List<ResponseRecord> history;
+
+    // -----------------------------------------------------------------------
+    // Constructor
+    // -----------------------------------------------------------------------
+
+    public RestClient(String baseUrl, Map<String, String> defaultHeaders,
+                      int maxRetries, long backoffMillis, Set<Integer> retryStatusCodes) {
+        this.baseUrl = baseUrl.replaceAll("/+$", "");
+        this.defaultHeaders = defaultHeaders != null ? defaultHeaders : new HashMap<>();
+        this.maxRetries = maxRetries;
+        this.backoffMillis = backoffMillis;
+        this.retryStatusCodes = retryStatusCodes != null ? retryStatusCodes :
+                Set.of(429, 500, 502, 503, 504);
+        this.history = new ArrayList<>();
+        this.objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
+
+        RequestConfig requestConfig = RequestConfig.custom()
+                .setConnectionRequestTimeout(Timeout.of(30, TimeUnit.SECONDS))
+                .setResponseTimeout(Timeout.of(30, TimeUnit.SECONDS))
+                .build();
+
+        this.httpClient = HttpClients.custom()
+                .setDefaultRequestConfig(requestConfig)
+                .build();
+    }
+
+    // -----------------------------------------------------------------------
+    // URL building
+    // -----------------------------------------------------------------------
+
+    private String buildUrl(String path) {
+        if (path.startsWith("http://") || path.startsWith("https://")) {
+            return path;
+        }
+        return baseUrl + "/" + path.replaceAll("^/+", "");
+    }
+
+    private String buildUrlWithParams(String url, Map<String, String> params) {
+        if (params == null || params.isEmpty()) return url;
+        StringBuilder sb = new StringBuilder(url);
+        sb.append("?");
+        boolean first = true;
+        for (Map.Entry<String, String> entry : params.entrySet()) {
+            if (!first) sb.append("&");
+            sb.append(entry.getKey()).append("=").append(entry.getValue());
+            first = false;
+        }
+        return sb.toString();
+    }
+
+    // -----------------------------------------------------------------------
+    // Core request
+    // -----------------------------------------------------------------------
+
+    public ResponseRecord request(HttpRequestConfig cfg) {
+        String url = buildUrl(cfg.getUrl());
+        logger.info(String.format(">>> %s %s | label=%s",
+                cfg.getMethod().toUpperCase(), url,
+                cfg.getLabel().isEmpty() ? "(none)" : cfg.getLabel()));
+
+        String fullUrl = buildUrlWithParams(url, cfg.getQueryParams());
+
+        for (int attempt = 0; attempt < maxRetries; attempt++) {
+            long start = System.nanoTime();
+
+            try {
+                // Build request
+                HttpUriRequestBase request = createRequest(cfg.getMethod(), fullUrl, cfg);
+
+                // Add headers
+                defaultHeaders.forEach(request::addHeader);
+                cfg.getHeaders().forEach(request::addHeader);
+
+                // Execute
+                ClassicHttpResponse response = (ClassicHttpResponse) httpClient.execute(request);
+                double elapsed = (System.nanoTime() - start) / 1_000_000_000.0;
+
+                int statusCode = response.getCode();
+                String rawBody = response.getEntity() != null ?
+                        EntityUtils.toString(response.getEntity()) : "";
+
+                // Check if retryable
+                if (retryStatusCodes.contains(statusCode) && attempt < maxRetries - 1) {
+                    logger.warning(String.format("Retryable status %d on attempt %d",
+                            statusCode, attempt + 1));
+                    long wait = backoffMillis * (long) Math.pow(2, attempt);
+                    Thread.sleep(wait);
+                    continue;
+                }
+
+                // Parse JSON
+                JsonNode jsonBody = null;
+                try {
+                    jsonBody = objectMapper.readTree(rawBody);
+                } catch (Exception ignored) {}
+
+                // Response headers
+                Map<String, String> respHeaders = new HashMap<>();
+                for (Header header : response.getHeaders()) {
+                    respHeaders.put(header.getName(), header.getValue());
+                }
+
+                boolean isOk = statusCode >= 200 && statusCode < 300;
+                ResponseRecord record = new ResponseRecord(
+                        cfg.getLabel(), cfg.getMethod().toUpperCase(), url,
+                        statusCode, elapsed, respHeaders, rawBody, jsonBody, isOk);
+
+                // Validate
+                List<String> errors = validate(record, cfg.getValidators());
+                record.setValidationErrors(errors);
+
+                logResponse(record);
+                history.add(record);
+
+                if (!errors.isEmpty()) {
+                    throw new ValidationException(errors);
+                }
+
+                return record;
+
+            } catch (ValidationException ve) {
+                throw ve;
+            } catch (Exception e) {
+                if (attempt >= maxRetries - 1) {
+                    logger.severe(String.format("All retries exhausted for %s %s: %s",
+                            cfg.getMethod().toUpperCase(), url, e.getMessage()));
+                    throw new RuntimeException("Request failed after retries", e);
+                }
+                logger.warning(String.format("Attempt %d failed: %s", attempt + 1, e.getMessage()));
+                try {
+                    long wait = backoffMillis * (long) Math.pow(2, attempt);
+                    Thread.sleep(wait);
+                } catch (InterruptedException ie) {
+                    Thread.currentThread().interrupt();
+                }
+            }
+        }
+
+        throw new RuntimeException("Request failed after all retries");
+    }
+
+    private HttpUriRequestBase createRequest(String method, String url, HttpRequestConfig cfg) {
+        HttpUriRequestBase request;
+        switch (method.toUpperCase()) {
+            case "GET":    request = new HttpGet(url); break;
+            case "POST":   request = new HttpPost(url); break;
+            case "PUT":    request = new HttpPut(url); break;
+            case "PATCH":  request = new HttpPatch(url); break;
+            case "DELETE": request = new HttpDelete(url); break;
+            default: throw new IllegalArgumentException("Unsupported method: " + method);
+        }
+
+        if (cfg.getJsonBody() != null && request instanceof HttpPost) {
+            try {
+                String json = objectMapper.writeValueAsString(cfg.getJsonBody());
+                ((HttpPost) request).setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));
+            } catch (Exception e) {
+                throw new RuntimeException("Failed to serialize JSON body", e);
+            }
+        } else if (cfg.getJsonBody() != null && request instanceof HttpPut) {
+            try {
+                String json = objectMapper.writeValueAsString(cfg.getJsonBody());
+                ((HttpPut) request).setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));
+            } catch (Exception e) {
+                throw new RuntimeException("Failed to serialize JSON body", e);
+            }
+        } else if (cfg.getJsonBody() != null && request instanceof HttpPatch) {
+            try {
+                String json = objectMapper.writeValueAsString(cfg.getJsonBody());
+                ((HttpPatch) request).setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));
+            } catch (Exception e) {
+                throw new RuntimeException("Failed to serialize JSON body", e);
+            }
+        }
+
+        return request;
+    }
+
+    private List<String> validate(ResponseRecord record, List<ValidationRule> rules) {
+        List<String> errors = new ArrayList<>();
+        for (ValidationRule rule : rules) {
+            try {
+                if (!rule.test(record)) {
+                    errors.add(rule.getMessage().isEmpty() ? rule.getName() : rule.getMessage());
+                    logger.warning(String.format("Validation failed [%s]: %s",
+                            rule.getName(), rule.getMessage()));
+                }
+            } catch (Exception e) {
+                errors.add(rule.getName() + ": " + e.getMessage());
+            }
+        }
+        return errors;
+    }
+
+    private void logResponse(ResponseRecord record) {
+        logger.log(record.isOk() ? Level.INFO : Level.WARNING,
+                String.format("<<< %d %s (%.3fs) | label=%s | errors=%s",
+                        record.getStatusCode(), record.getUrl(),
+                        record.getElapsedSeconds(),
+                        record.getLabel().isEmpty() ? "(none)" : record.getLabel(),
+                        record.getValidationErrors().isEmpty() ? "none" : record.getValidationErrors()));
+    }
+
+    // -----------------------------------------------------------------------
+    // Convenience methods
+    // -----------------------------------------------------------------------
+
+    public ResponseRecord get(String url, String label, ValidationRule... validators) {
+        HttpRequestConfig cfg = new HttpRequestConfig("GET", url).label(label);
+        for (ValidationRule v : validators) cfg.validate(v);
+        return request(cfg);
+    }
+
+    public ResponseRecord post(String url, Object body, String label, ValidationRule... validators) {
+        HttpRequestConfig cfg = new HttpRequestConfig("POST", url).body(body).label(label);
+        for (ValidationRule v : validators) cfg.validate(v);
+        return request(cfg);
+    }
+
+    public ResponseRecord put(String url, Object body, String label, ValidationRule... validators) {
+        HttpRequestConfig cfg = new HttpRequestConfig("PUT", url).body(body).label(label);
+        for (ValidationRule v : validators) cfg.validate(v);
+        return request(cfg);
+    }
+
+    public ResponseRecord patch(String url, Object body, String label, ValidationRule... validators) {
+        HttpRequestConfig cfg = new HttpRequestConfig("PATCH", url).body(body).label(label);
+        for (ValidationRule v : validators) cfg.validate(v);
+        return request(cfg);
+    }
+
+    public ResponseRecord delete(String url, String label, ValidationRule... validators) {
+        HttpRequestConfig cfg = new HttpRequestConfig("DELETE", url).label(label);
+        for (ValidationRule v : validators) cfg.validate(v);
+        return request(cfg);
+    }
+
+    // -----------------------------------------------------------------------
+    // Request chaining
+    // -----------------------------------------------------------------------
+
+    public List<ResponseRecord> chain(List<HttpRequestConfig> configs,
+                                       Map<String, String> context,
+                                       BiFunction<ResponseRecord, Map<String, String>, Map<String, String>> extractor) {
+        Map<String, String> ctx = context != null ? new HashMap<>(context) : new HashMap<>();
+        List<ResponseRecord> results = new ArrayList<>();
+
+        for (int i = 0; i < configs.size(); i++) {
+            HttpRequestConfig cfg = configs.get(i);
+
+            String url = cfg.getUrl();
+            for (Map.Entry<String, String> entry : ctx.entrySet()) {
+                url = url.replace("{" + entry.getKey() + "}", entry.getValue());
+            }
+            cfg.setUrl(url);
+
+            Map<String, String> headers = new HashMap<>();
+            for (Map.Entry<String, String> entry : cfg.getHeaders().entrySet()) {
+                String val = entry.getValue();
+                for (Map.Entry<String, String> ctxEntry : ctx.entrySet()) {
+                    val = val.replace("{" + ctxEntry.getKey() + "}", ctxEntry.getValue());
+                }
+                headers.put(entry.getKey(), val);
+            }
+            cfg.setHeaders(headers);
+
+            logger.info(String.format("Chain step %d/%d: %s", i + 1, configs.size(),
+                    cfg.getLabel().isEmpty() ? cfg.getUrl() : cfg.getLabel()));
+
+            ResponseRecord record = request(cfg);
+            results.add(record);
+
+            if (extractor != null) {
+                ctx = extractor.apply(record, ctx);
+            }
+        }
+        return results;
+    }
+
+    // -----------------------------------------------------------------------
+    // History
+    // -----------------------------------------------------------------------
+
+    public List<ResponseRecord> getHistory() { return Collections.unmodifiableList(history); }
+
+    public void printHistory() {
+        System.out.println("\n" + "=".repeat(70));
+        System.out.printf(" Request History (%d requests)%n", history.size());
+        System.out.println("=".repeat(70));
+        for (int i = 0; i < history.size(); i++) {
+            ResponseRecord r = history.get(i);
+            String status = r.isOk() ? "OK" : "FAIL";
+            System.out.printf("  [%d] %s | %s %s -> %d (%.3fs) | %s%n",
+                    i + 1, status, r.getMethod(), r.getUrl(),
+                    r.getStatusCode(), r.getElapsedSeconds(), r.getLabel());
+            for (String err : r.getValidationErrors()) {
+                System.out.printf("       ! %s%n", err);
+            }
+        }
+        System.out.println("=".repeat(70) + "\n");
+    }
+
+    // -----------------------------------------------------------------------
+    // Main
+    // -----------------------------------------------------------------------
+
+    public static void main(String[] args) {
+        Map<String, String> defaultHeaders = Map.of("Accept", "application/json");
+        RestClient client = new RestClient(
+                "https://jsonplaceholder.typicode.com",
+                defaultHeaders, 3, 1000,
+                Set.of(429, 500, 502, 503, 504));
+
+        // 1. Simple GET
+        System.out.println("\n--- Simple GET with validation ---");
+        try {
+            ResponseRecord record = client.get("/posts/1", "get_post_1",
+                    statusOk(), jsonHasKey("title"));
+            if (record.getJsonBody() != null) {
+                System.out.println("Post title: " + record.getJsonBody().get("title").asText());
+            }
+        } catch (Exception e) {
+            System.out.println("Request failed: " + e.getMessage());
+        }
+
+        // 2. POST
+        System.out.println("\n--- POST with body ---");
+        try {
+            Map<String, Object> postBody = Map.of("title", "Hello", "body", "World", "userId", 1);
+            ResponseRecord record = client.post("/posts", postBody, "create_post", statusEquals(201));
+            if (record.getJsonBody() != null) {
+                System.out.println("Created post ID: " + record.getJsonBody().get("id").asInt());
+            }
+        } catch (Exception e) {
+            System.out.println("Request failed: " + e.getMessage());
+        }
+
+        // 3. Request chaining
+        System.out.println("\n--- Request chaining ---");
+        try {
+            List<HttpRequestConfig> chainConfigs = List.of(
+                    new HttpRequestConfig("GET", "/users/1").label("get_user").validate(statusOk()),
+                    new HttpRequestConfig("GET", "/users/{user_id}/posts").label("get_user_posts").validate(statusOk())
+            );
+
+            BiFunction<ResponseRecord, Map<String, String>, Map<String, String>> extractor = (record, ctx) -> {
+                if (record.getJsonBody() != null && record.getJsonBody().has("id")) {
+                    ctx.put("user_id", String.valueOf(record.getJsonBody().get("id").asInt()));
+                }
+                return ctx;
+            };
+
+            List<ResponseRecord> results = client.chain(chainConfigs, null, extractor);
+            if (results.size() >= 2 && results.get(1).getJsonBody() != null) {
+                System.out.println("User has " + results.get(1).getJsonBody().size() + " posts");
+            }
+        } catch (Exception e) {
+            System.out.println("Chain failed: " + e.getMessage());
+        }
+
+        // 4. PUT
+        System.out.println("\n--- PUT request ---");
+        try {
+            Map<String, Object> putBody = Map.of("id", 1, "title", "Updated", "body", "Content", "userId", 1);
+            ResponseRecord record = client.put("/posts/1", putBody, "update_post", statusOk());
+            if (record.getJsonBody() != null) {
+                System.out.println("Updated title: " + record.getJsonBody().get("title").asText());
+            }
+        } catch (Exception e) {
+            System.out.println("Request failed: " + e.getMessage());
+        }
+
+        // 5. DELETE
+        System.out.println("\n--- DELETE request ---");
+        try {
+            ResponseRecord record = client.delete("/posts/1", "delete_post", statusOk());
+            System.out.println("Delete status: " + record.getStatusCode());
+        } catch (Exception e) {
+            System.out.println("Request failed: " + e.getMessage());
+        }
+
+        client.printHistory();
+    }
+}

03 / TASK AND FAILURE

claude-code/java-t2 #27 · read the task the agent was given
Claude Code wrote this java project from the task below. It does not run on a clean Ubuntu 22.04 machine as written.

Task given to the agent:

TASK: HTTP REST Client with Retry Logic

Write a program that sends HTTP requests to REST APIs with configurable retry logic, response validation, request chaining, and comprehensive logging of request/response cycles.

FUNCTIONAL REQUIREMENTS:
- Accept a URL as a command-line argument with --method flag (GET, POST, PUT, PATCH, DELETE; default: GET)
- Support sending request body via --data flag (JSON string) or --file flag (read body from a file)
- Support custom headers via --header flag (repeatable, e.g., --header "Content-Type: application/json" --header "Authorization: Bearer token")
- Implement configurable retry logic: --retries flag (default 3), --backoff flag for backoff strategy (linear, exponential; default: exponential), --retry-delay flag for initial delay in seconds (default 1)
- Retry only on configurable HTTP status codes via --retry-on flag (default: 429, 500, 502, 503, 504)
- Support request timeout via --timeout flag (default 30 seconds)
- Support request chaining from a JSON recipe file via --chain flag: execute a sequence of requests where later requests can reference values from earlier responses using JSONPath expressions
- Log complete request/response cycles: method, URL, headers, body, status code, response headers, response body, timing, and retry attempts
- Support response validation via --expect-status flag (e.g., --expect-status 200) and --expect-json flag (JSONPath expression that must exist in the response)
- Print formatted response to console: status code, headers, and pretty-printed body (auto-detect JSON and format it)
- Save the full request/response log as JSON with --output flag (default: request_log.json)
- If no URL is given, demonstrate the client by sending requests to a public test API (httpbin.org or similar), showing GET, POST with JSON body, retry on simulated failure, and a simple request chain
- Handle errors: connection refused, DNS failure, SSL errors, malformed JSON responses, and redirect loops

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 checked by running the task · needs human review

misspecification
Label rules and the text that matched
[
  {
    "category": "misspecification",
    "rule": "signature.build_layout_mismatch",
    "source": "failure_signature",
    "excerpt": "error: no classes were compiled"
  }
]

Written by Claude Code (study run M1T2P27L2). It failed as written and was repaired by changing only its environment.

Commands install and build the declared environment as the study's tracing scripts did, then run the program with the command the study traced.

Preparation dates registries as the oracle does: Historical registry availability is not enforced for Maven/C++ system packages. Maven updatePolicy controls refresh frequency, not publication date.

05 / FILES

The project as the agent wrote it

2 files, exactly as written, before any repair.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>rest-client</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>HTTP REST Client with Retry Logic</name>
    <description>REST client using Apache HttpClient5 and Jackson with retry/backoff, request chaining, and validation</description>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <httpclient5.version>5.3</httpclient5.version>
        <jackson.version>2.16.1</jackson.version>
    </properties>

    <dependencies>
        <!-- Apache HttpClient5 -->
        <dependency>
            <groupId>org.apache.httpcomponents.client5</groupId>
            <artifactId>httpclient5</artifactId>
            <version>${httpclient5.version}</version>
        </dependency>

        <!-- Jackson Databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>RestClient</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
RestClient.java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

import org.apache.hc.client5.http.classic.methods.*;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.util.Timeout;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.logging.*;

/**
 * HTTP REST Client with Retry Logic using Apache HttpClient5 and Jackson.
 *
 * Features:
 * - GET, POST, PUT, PATCH, DELETE with configurable retry/backoff
 * - Request chaining with context interpolation
 * - Response validation with custom rules
 * - Structured logging
 */
public class RestClient {

    // -----------------------------------------------------------------------
    // Logging
    // -----------------------------------------------------------------------
    private static final Logger logger = Logger.getLogger(RestClient.class.getName());

    static {
        ConsoleHandler handler = new ConsoleHandler();
        handler.setLevel(Level.ALL);
        handler.setFormatter(new SimpleFormatter());
        logger.addHandler(handler);
        logger.setLevel(Level.INFO);
        logger.setUseParentHandlers(false);
    }

    // -----------------------------------------------------------------------
    // Inner classes
    // -----------------------------------------------------------------------

    public static class ValidationRule {
        private final String name;
        private final Predicate<ResponseRecord> check;
        private final String message;

        public ValidationRule(String name, Predicate<ResponseRecord> check, String message) {
            this.name = name;
            this.check = check;
            this.message = message;
        }

        public String getName() { return name; }
        public String getMessage() { return message; }
        public boolean test(ResponseRecord record) { return check.test(record); }
    }

    public static class ValidationException extends RuntimeException {
        private final List<String> failedRules;

        public ValidationException(List<String> failedRules) {
            super("Validation failed: " + String.join(", ", failedRules));
            this.failedRules = failedRules;
        }

        public List<String> getFailedRules() { return failedRules; }
    }

    public static class HttpRequestConfig {
        private String method;
        private String url;
        private Map<String, String> headers;
        private Map<String, String> queryParams;
        private Object jsonBody;
        private String label;
        private List<ValidationRule> validators;
        private long timeoutSeconds;

        public HttpRequestConfig(String method, String url) {
            this.method = method;
            this.url = url;
            this.headers = new HashMap<>();
            this.queryParams = new HashMap<>();
            this.validators = new ArrayList<>();
            this.label = "";
            this.timeoutSeconds = 30;
        }

        public HttpRequestConfig header(String key, String value) { headers.put(key, value); return this; }
        public HttpRequestConfig param(String key, String value) { queryParams.put(key, value); return this; }
        public HttpRequestConfig body(Object body) { this.jsonBody = body; return this; }
        public HttpRequestConfig label(String label) { this.label = label; return this; }
        public HttpRequestConfig validate(ValidationRule rule) { validators.add(rule); return this; }
        public HttpRequestConfig timeout(long seconds) { this.timeoutSeconds = seconds; return this; }

        public String getMethod() { return method; }
        public String getUrl() { return url; }
        public void setUrl(String url) { this.url = url; }
        public Map<String, String> getHeaders() { return headers; }
        public void setHeaders(Map<String, String> headers) { this.headers = headers; }
        public Map<String, String> getQueryParams() { return queryParams; }
        public Object getJsonBody() { return jsonBody; }
        public void setJsonBody(Object body) { this.jsonBody = body; }
        public String getLabel() { return label; }
        public List<ValidationRule> getValidators() { return validators; }
        public long getTimeoutSeconds() { return timeoutSeconds; }
    }

    public static class ResponseRecord {
        private final String label;
        private final String method;
        private final String url;
        private final int statusCode;
        private final double elapsedSeconds;
        private final Map<String, String> headers;
        private final String rawBody;
        private final JsonNode jsonBody;
        private final boolean ok;
        private List<String> validationErrors;

        public ResponseRecord(String label, String method, String url, int statusCode,
                              double elapsedSeconds, Map<String, String> headers,
                              String rawBody, JsonNode jsonBody, boolean ok) {
            this.label = label;
            this.method = method;
            this.url = url;
            this.statusCode = statusCode;
            this.elapsedSeconds = elapsedSeconds;
            this.headers = headers;
            this.rawBody = rawBody;
            this.jsonBody = jsonBody;
            this.ok = ok;
            this.validationErrors = new ArrayList<>();
        }

        public String getLabel() { return label; }
        public String getMethod() { return method; }
        public String getUrl() { return url; }
        public int getStatusCode() { return statusCode; }
        public double getElapsedSeconds() { return elapsedSeconds; }
        public Map<String, String> getHeaders() { return headers; }
        public String getRawBody() { return rawBody; }
        public JsonNode getJsonBody() { return jsonBody; }
        public boolean isOk() { return ok && validationErrors.isEmpty(); }
        public List<String> getValidationErrors() { return validationErrors; }
        public void setValidationErrors(List<String> errors) { this.validationErrors = errors; }
    }

    // -----------------------------------------------------------------------
    // Built-in validators
    // -----------------------------------------------------------------------

    public static ValidationRule statusOk() {
        return new ValidationRule("status_ok",
                r -> r.getStatusCode() >= 200 && r.getStatusCode() < 300,
                "Expected 2xx status code");
    }

    public static ValidationRule statusEquals(int code) {
        return new ValidationRule("status_" + code,
                r -> r.getStatusCode() == code,
                "Expected status code " + code);
    }

    public static ValidationRule jsonHasKey(String key) {
        return new ValidationRule("has_key_" + key,
                r -> r.getJsonBody() != null && r.getJsonBody().isObject() && r.getJsonBody().has(key),
                "Expected JSON key '" + key + "' in response");
    }

    public static ValidationRule maxResponseTime(double seconds) {
        return new ValidationRule("max_response_time",
                r -> r.getElapsedSeconds() <= seconds,
                "Response took longer than " + seconds + "s");
    }

    // -----------------------------------------------------------------------
    // Client fields
    // -----------------------------------------------------------------------

    private final String baseUrl;
    private final CloseableHttpClient httpClient;
    private final ObjectMapper objectMapper;
    private final Map<String, String> defaultHeaders;
    private final Set<Integer> retryStatusCodes;
    private final int maxRetries;
    private final long backoffMillis;
    private final List<ResponseRecord> history;

    // -----------------------------------------------------------------------
    // Constructor
    // -----------------------------------------------------------------------

    public RestClient(String baseUrl, Map<String, String> defaultHeaders,
                      int maxRetries, long backoffMillis, Set<Integer> retryStatusCodes) {
        this.baseUrl = baseUrl.replaceAll("/+$", "");
        this.defaultHeaders = defaultHeaders != null ? defaultHeaders : new HashMap<>();
        this.maxRetries = maxRetries;
        this.backoffMillis = backoffMillis;
        this.retryStatusCodes = retryStatusCodes != null ? retryStatusCodes :
                Set.of(429, 500, 502, 503, 504);
        this.history = new ArrayList<>();
        this.objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);

        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(Timeout.of(30, TimeUnit.SECONDS))
                .setResponseTimeout(Timeout.of(30, TimeUnit.SECONDS))
                .build();

        this.httpClient = HttpClients.custom()
                .setDefaultRequestConfig(requestConfig)
                .build();
    }

    // -----------------------------------------------------------------------
    // URL building
    // -----------------------------------------------------------------------

    private String buildUrl(String path) {
        if (path.startsWith("http://") || path.startsWith("https://")) {
            return path;
        }
        return baseUrl + "/" + path.replaceAll("^/+", "");
    }

    private String buildUrlWithParams(String url, Map<String, String> params) {
        if (params == null || params.isEmpty()) return url;
        StringBuilder sb = new StringBuilder(url);
        sb.append("?");
        boolean first = true;
        for (Map.Entry<String, String> entry : params.entrySet()) {
            if (!first) sb.append("&");
            sb.append(entry.getKey()).append("=").append(entry.getValue());
            first = false;
        }
        return sb.toString();
    }

    // -----------------------------------------------------------------------
    // Core request
    // -----------------------------------------------------------------------

    public ResponseRecord request(HttpRequestConfig cfg) {
        String url = buildUrl(cfg.getUrl());
        logger.info(String.format(">>> %s %s | label=%s",
                cfg.getMethod().toUpperCase(), url,
                cfg.getLabel().isEmpty() ? "(none)" : cfg.getLabel()));

        String fullUrl = buildUrlWithParams(url, cfg.getQueryParams());

        for (int attempt = 0; attempt < maxRetries; attempt++) {
            long start = System.nanoTime();

            try {
                // Build request
                HttpUriRequestBase request = createRequest(cfg.getMethod(), fullUrl, cfg);

                // Add headers
                defaultHeaders.forEach(request::addHeader);
                cfg.getHeaders().forEach(request::addHeader);

                // Execute
                ClassicHttpResponse response = (ClassicHttpResponse) httpClient.execute(request);
                double elapsed = (System.nanoTime() - start) / 1_000_000_000.0;

                int statusCode = response.getCode();
                String rawBody = response.getEntity() != null ?
                        EntityUtils.toString(response.getEntity()) : "";

                // Check if retryable
                if (retryStatusCodes.contains(statusCode) && attempt < maxRetries - 1) {
                    logger.warning(String.format("Retryable status %d on attempt %d",
                            statusCode, attempt + 1));
                    long wait = backoffMillis * (long) Math.pow(2, attempt);
                    Thread.sleep(wait);
                    continue;
                }

                // Parse JSON
                JsonNode jsonBody = null;
                try {
                    jsonBody = objectMapper.readTree(rawBody);
                } catch (Exception ignored) {}

                // Response headers
                Map<String, String> respHeaders = new HashMap<>();
                for (Header header : response.getHeaders()) {
                    respHeaders.put(header.getName(), header.getValue());
                }

                boolean isOk = statusCode >= 200 && statusCode < 300;
                ResponseRecord record = new ResponseRecord(
                        cfg.getLabel(), cfg.getMethod().toUpperCase(), url,
                        statusCode, elapsed, respHeaders, rawBody, jsonBody, isOk);

                // Validate
                List<String> errors = validate(record, cfg.getValidators());
                record.setValidationErrors(errors);

                logResponse(record);
                history.add(record);

                if (!errors.isEmpty()) {
                    throw new ValidationException(errors);
                }

                return record;

            } catch (ValidationException ve) {
                throw ve;
            } catch (Exception e) {
                if (attempt >= maxRetries - 1) {
                    logger.severe(String.format("All retries exhausted for %s %s: %s",
                            cfg.getMethod().toUpperCase(), url, e.getMessage()));
                    throw new RuntimeException("Request failed after retries", e);
                }
                logger.warning(String.format("Attempt %d failed: %s", attempt + 1, e.getMessage()));
                try {
                    long wait = backoffMillis * (long) Math.pow(2, attempt);
                    Thread.sleep(wait);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
            }
        }

        throw new RuntimeException("Request failed after all retries");
    }

    private HttpUriRequestBase createRequest(String method, String url, HttpRequestConfig cfg) {
        HttpUriRequestBase request;
        switch (method.toUpperCase()) {
            case "GET":    request = new HttpGet(url); break;
            case "POST":   request = new HttpPost(url); break;
            case "PUT":    request = new HttpPut(url); break;
            case "PATCH":  request = new HttpPatch(url); break;
            case "DELETE": request = new HttpDelete(url); break;
            default: throw new IllegalArgumentException("Unsupported method: " + method);
        }

        if (cfg.getJsonBody() != null && request instanceof HttpPost) {
            try {
                String json = objectMapper.writeValueAsString(cfg.getJsonBody());
                ((HttpPost) request).setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));
            } catch (Exception e) {
                throw new RuntimeException("Failed to serialize JSON body", e);
            }
        } else if (cfg.getJsonBody() != null && request instanceof HttpPut) {
            try {
                String json = objectMapper.writeValueAsString(cfg.getJsonBody());
                ((HttpPut) request).setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));
            } catch (Exception e) {
                throw new RuntimeException("Failed to serialize JSON body", e);
            }
        } else if (cfg.getJsonBody() != null && request instanceof HttpPatch) {
            try {
                String json = objectMapper.writeValueAsString(cfg.getJsonBody());
                ((HttpPatch) request).setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));
            } catch (Exception e) {
                throw new RuntimeException("Failed to serialize JSON body", e);
            }
        }

        return request;
    }

    private List<String> validate(ResponseRecord record, List<ValidationRule> rules) {
        List<String> errors = new ArrayList<>();
        for (ValidationRule rule : rules) {
            try {
                if (!rule.test(record)) {
                    errors.add(rule.getMessage().isEmpty() ? rule.getName() : rule.getMessage());
                    logger.warning(String.format("Validation failed [%s]: %s",
                            rule.getName(), rule.getMessage()));
                }
            } catch (Exception e) {
                errors.add(rule.getName() + ": " + e.getMessage());
            }
        }
        return errors;
    }

    private void logResponse(ResponseRecord record) {
        logger.log(record.isOk() ? Level.INFO : Level.WARNING,
                String.format("<<< %d %s (%.3fs) | label=%s | errors=%s",
                        record.getStatusCode(), record.getUrl(),
                        record.getElapsedSeconds(),
                        record.getLabel().isEmpty() ? "(none)" : record.getLabel(),
                        record.getValidationErrors().isEmpty() ? "none" : record.getValidationErrors()));
    }

    // -----------------------------------------------------------------------
    // Convenience methods
    // -----------------------------------------------------------------------

    public ResponseRecord get(String url, String label, ValidationRule... validators) {
        HttpRequestConfig cfg = new HttpRequestConfig("GET", url).label(label);
        for (ValidationRule v : validators) cfg.validate(v);
        return request(cfg);
    }

    public ResponseRecord post(String url, Object body, String label, ValidationRule... validators) {
        HttpRequestConfig cfg = new HttpRequestConfig("POST", url).body(body).label(label);
        for (ValidationRule v : validators) cfg.validate(v);
        return request(cfg);
    }

    public ResponseRecord put(String url, Object body, String label, ValidationRule... validators) {
        HttpRequestConfig cfg = new HttpRequestConfig("PUT", url).body(body).label(label);
        for (ValidationRule v : validators) cfg.validate(v);
        return request(cfg);
    }

    public ResponseRecord patch(String url, Object body, String label, ValidationRule... validators) {
        HttpRequestConfig cfg = new HttpRequestConfig("PATCH", url).body(body).label(label);
        for (ValidationRule v : validators) cfg.validate(v);
        return request(cfg);
    }

    public ResponseRecord delete(String url, String label, ValidationRule... validators) {
        HttpRequestConfig cfg = new HttpRequestConfig("DELETE", url).label(label);
        for (ValidationRule v : validators) cfg.validate(v);
        return request(cfg);
    }

    // -----------------------------------------------------------------------
    // Request chaining
    // -----------------------------------------------------------------------

    public List<ResponseRecord> chain(List<HttpRequestConfig> configs,
                                       Map<String, String> context,
                                       BiFunction<ResponseRecord, Map<String, String>, Map<String, String>> extractor) {
        Map<String, String> ctx = context != null ? new HashMap<>(context) : new HashMap<>();
        List<ResponseRecord> results = new ArrayList<>();

        for (int i = 0; i < configs.size(); i++) {
            HttpRequestConfig cfg = configs.get(i);

            String url = cfg.getUrl();
            for (Map.Entry<String, String> entry : ctx.entrySet()) {
                url = url.replace("{" + entry.getKey() + "}", entry.getValue());
            }
            cfg.setUrl(url);

            Map<String, String> headers = new HashMap<>();
            for (Map.Entry<String, String> entry : cfg.getHeaders().entrySet()) {
                String val = entry.getValue();
                for (Map.Entry<String, String> ctxEntry : ctx.entrySet()) {
                    val = val.replace("{" + ctxEntry.getKey() + "}", ctxEntry.getValue());
                }
                headers.put(entry.getKey(), val);
            }
            cfg.setHeaders(headers);

            logger.info(String.format("Chain step %d/%d: %s", i + 1, configs.size(),
                    cfg.getLabel().isEmpty() ? cfg.getUrl() : cfg.getLabel()));

            ResponseRecord record = request(cfg);
            results.add(record);

            if (extractor != null) {
                ctx = extractor.apply(record, ctx);
            }
        }
        return results;
    }

    // -----------------------------------------------------------------------
    // History
    // -----------------------------------------------------------------------

    public List<ResponseRecord> getHistory() { return Collections.unmodifiableList(history); }

    public void printHistory() {
        System.out.println("\n" + "=".repeat(70));
        System.out.printf(" Request History (%d requests)%n", history.size());
        System.out.println("=".repeat(70));
        for (int i = 0; i < history.size(); i++) {
            ResponseRecord r = history.get(i);
            String status = r.isOk() ? "OK" : "FAIL";
            System.out.printf("  [%d] %s | %s %s -> %d (%.3fs) | %s%n",
                    i + 1, status, r.getMethod(), r.getUrl(),
                    r.getStatusCode(), r.getElapsedSeconds(), r.getLabel());
            for (String err : r.getValidationErrors()) {
                System.out.printf("       ! %s%n", err);
            }
        }
        System.out.println("=".repeat(70) + "\n");
    }

    // -----------------------------------------------------------------------
    // Main
    // -----------------------------------------------------------------------

    public static void main(String[] args) {
        Map<String, String> defaultHeaders = Map.of("Accept", "application/json");
        RestClient client = new RestClient(
                "https://jsonplaceholder.typicode.com",
                defaultHeaders, 3, 1000,
                Set.of(429, 500, 502, 503, 504));

        // 1. Simple GET
        System.out.println("\n--- Simple GET with validation ---");
        try {
            ResponseRecord record = client.get("/posts/1", "get_post_1",
                    statusOk(), jsonHasKey("title"));
            if (record.getJsonBody() != null) {
                System.out.println("Post title: " + record.getJsonBody().get("title").asText());
            }
        } catch (Exception e) {
            System.out.println("Request failed: " + e.getMessage());
        }

        // 2. POST
        System.out.println("\n--- POST with body ---");
        try {
            Map<String, Object> postBody = Map.of("title", "Hello", "body", "World", "userId", 1);
            ResponseRecord record = client.post("/posts", postBody, "create_post", statusEquals(201));
            if (record.getJsonBody() != null) {
                System.out.println("Created post ID: " + record.getJsonBody().get("id").asInt());
            }
        } catch (Exception e) {
            System.out.println("Request failed: " + e.getMessage());
        }

        // 3. Request chaining
        System.out.println("\n--- Request chaining ---");
        try {
            List<HttpRequestConfig> chainConfigs = List.of(
                    new HttpRequestConfig("GET", "/users/1").label("get_user").validate(statusOk()),
                    new HttpRequestConfig("GET", "/users/{user_id}/posts").label("get_user_posts").validate(statusOk())
            );

            BiFunction<ResponseRecord, Map<String, String>, Map<String, String>> extractor = (record, ctx) -> {
                if (record.getJsonBody() != null && record.getJsonBody().has("id")) {
                    ctx.put("user_id", String.valueOf(record.getJsonBody().get("id").asInt()));
                }
                return ctx;
            };

            List<ResponseRecord> results = client.chain(chainConfigs, null, extractor);
            if (results.size() >= 2 && results.get(1).getJsonBody() != null) {
                System.out.println("User has " + results.get(1).getJsonBody().size() + " posts");
            }
        } catch (Exception e) {
            System.out.println("Chain failed: " + e.getMessage());
        }

        // 4. PUT
        System.out.println("\n--- PUT request ---");
        try {
            Map<String, Object> putBody = Map.of("id", 1, "title", "Updated", "body", "Content", "userId", 1);
            ResponseRecord record = client.put("/posts/1", putBody, "update_post", statusOk());
            if (record.getJsonBody() != null) {
                System.out.println("Updated title: " + record.getJsonBody().get("title").asText());
            }
        } catch (Exception e) {
            System.out.println("Request failed: " + e.getMessage());
        }

        // 5. DELETE
        System.out.println("\n--- DELETE request ---");
        try {
            ResponseRecord record = client.delete("/posts/1", "delete_post", statusOk());
            System.out.println("Delete status: " + record.getStatusCode());
        } catch (Exception e) {
            System.out.println("Request failed: " + e.getMessage());
        }

        client.printHistory();
    }
}