← All tasks
javaclaude-code/java-t1 #27Not a task: repair not recorded

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

envgap__claude-code__java-t1-27

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

01 / FAILURE SIGNATURE

As the study recorded it

cannot find symbol: method apply() on CheckedSupplier + source in root + no shade
Not a benchmark task.
  • It was made to work, but its repair cannot be rebuilt from the saved files (the saved copy shows no change, or not all of the changes the study's notes describe), so there is no fix to score against.

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

claude-code/java-t1 #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 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
<?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 with retry/backoff, request chaining, response validation, and logging</description>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <okhttp.version>4.12.0</okhttp.version>
        <gson.version>2.10.1</gson.version>
        <resilience4j.version>2.2.0</resilience4j.version>
    </properties>

    <dependencies>
        <!-- OkHttp - HTTP client -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>${okhttp.version}</version>
        </dependency>

        <!-- Gson - JSON serialization/deserialization -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>${gson.version}</version>
        </dependency>

        <!-- Resilience4j - Retry with backoff -->
        <dependency>
            <groupId>io.github.resilience4j</groupId>
            <artifactId>resilience4j-retry</artifactId>
            <version>${resilience4j.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>
README.md
# HTTP REST Client with Retry Logic (Java - Trial 1)

An HTTP REST client using OkHttp, Gson, and Resilience4j for retry logic.

## Dependencies

- **OkHttp** (4.12.0) - HTTP client for Java
- **Gson** (2.10.1) - JSON serialization/deserialization
- **Resilience4j** (2.2.0) - Retry with configurable backoff

## Features

- GET, POST, PUT, PATCH, DELETE request methods
- Automatic retry with exponential backoff via Resilience4j
- Configurable retry status codes
- Request chaining with context interpolation
- Response validation with built-in and custom rules
- Full request/response history with logging

## Build and Run

```bash
mvn clean compile exec:java -Dexec.mainClass="RestClient"
```

Or:

```bash
mvn clean package
java -jar target/rest-client-1.0.0.jar
```
RestClient.java
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.retry.RetryRegistry;
import okhttp3.*;

import java.io.IOException;
import java.time.Duration;
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 OkHttp, Gson, and Resilience4j.
 *
 * 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
    // -----------------------------------------------------------------------

    /** A single validation rule for an HTTP response. */
    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); }
    }

    /** Exception thrown when validation fails. */
    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; }
    }

    /** Configuration for a single HTTP request. */
    public static class RequestConfig {
        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 RequestConfig(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 RequestConfig header(String key, String value) { headers.put(key, value); return this; }
        public RequestConfig param(String key, String value) { queryParams.put(key, value); return this; }
        public RequestConfig body(Object body) { this.jsonBody = body; return this; }
        public RequestConfig label(String label) { this.label = label; return this; }
        public RequestConfig validate(ValidationRule rule) { validators.add(rule); return this; }
        public RequestConfig 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; }
    }

    /** Stores details about a completed request/response cycle. */
    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 JsonElement 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, JsonElement 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 JsonElement 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; }

        public JsonObject getJsonObject() {
            return jsonBody != null && jsonBody.isJsonObject() ? jsonBody.getAsJsonObject() : null;
        }
    }

    // -----------------------------------------------------------------------
    // 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.getJsonObject() != null && r.getJsonObject().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 OkHttpClient httpClient;
    private final Gson gson;
    private final Retry retry;
    private final Map<String, String> defaultHeaders;
    private final Set<Integer> retryStatusCodes;
    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.retryStatusCodes = retryStatusCodes != null ? retryStatusCodes :
                Set.of(429, 500, 502, 503, 504);
        this.history = new ArrayList<>();
        this.gson = new GsonBuilder().setPrettyPrinting().create();

        this.httpClient = new OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .writeTimeout(30, TimeUnit.SECONDS)
                .build();

        RetryConfig retryConfig = RetryConfig.custom()
                .maxAttempts(maxRetries)
                .waitDuration(Duration.ofMillis(backoffMillis))
                .retryOnException(e -> e instanceof IOException)
                .retryOnResult(r -> {
                    if (r instanceof Response) {
                        return this.retryStatusCodes.contains(((Response) r).code());
                    }
                    return false;
                })
                .build();

        RetryRegistry registry = RetryRegistry.of(retryConfig);
        this.retry = registry.retry("restClient");
    }

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

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

    private HttpUrl buildHttpUrl(String url, Map<String, String> params) {
        HttpUrl.Builder builder = HttpUrl.parse(url).newBuilder();
        if (params != null) {
            params.forEach(builder::addQueryParameter);
        }
        return builder.build();
    }

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

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

        HttpUrl httpUrl = buildHttpUrl(url, cfg.getQueryParams());

        // Build request body
        RequestBody body = null;
        if (cfg.getJsonBody() != null) {
            String json = gson.toJson(cfg.getJsonBody());
            body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8"));
        }

        // Build request
        Request.Builder reqBuilder = new Request.Builder().url(httpUrl);
        defaultHeaders.forEach(reqBuilder::header);
        cfg.getHeaders().forEach(reqBuilder::header);

        switch (cfg.getMethod().toUpperCase()) {
            case "GET":    reqBuilder.get(); break;
            case "POST":   reqBuilder.post(body != null ? body : RequestBody.create("", null)); break;
            case "PUT":    reqBuilder.put(body != null ? body : RequestBody.create("", null)); break;
            case "PATCH":  reqBuilder.patch(body != null ? body : RequestBody.create("", null)); break;
            case "DELETE": reqBuilder.delete(body); break;
            default: throw new IllegalArgumentException("Unsupported method: " + cfg.getMethod());
        }

        Request request = reqBuilder.build();

        // Execute with retry
        long start = System.nanoTime();
        try {
            Response response = Retry.decorateCheckedSupplier(retry, () -> {
                Response resp = httpClient.newCall(request).execute();
                if (retryStatusCodes.contains(resp.code())) {
                    logger.warning(String.format("Retryable status %d from %s", resp.code(), url));
                    resp.close();
                    throw new IOException("Retryable status " + resp.code());
                }
                return resp;
            }).apply();

            double elapsed = (System.nanoTime() - start) / 1_000_000_000.0;

            // Parse response
            String rawBody = response.body() != null ? response.body().string() : "";
            JsonElement jsonBody = null;
            try {
                jsonBody = JsonParser.parseString(rawBody);
            } catch (Exception ignored) {}

            Map<String, String> respHeaders = new HashMap<>();
            for (String name : response.headers().names()) {
                respHeaders.put(name, response.header(name));
            }

            boolean isOk = response.isSuccessful();
            ResponseRecord record = new ResponseRecord(
                    cfg.getLabel(), cfg.getMethod().toUpperCase(), url,
                    response.code(), elapsed, respHeaders, rawBody, jsonBody, isOk);

            response.close();

            // 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 (Throwable e) {
            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);
        }
    }

    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) {
        String level = record.isOk() ? "INFO" : "WARNING";
        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) {
        RequestConfig cfg = new RequestConfig("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) {
        RequestConfig cfg = new RequestConfig("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) {
        RequestConfig cfg = new RequestConfig("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) {
        RequestConfig cfg = new RequestConfig("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) {
        RequestConfig cfg = new RequestConfig("DELETE", url).label(label);
        for (ValidationRule v : validators) cfg.validate(v);
        return request(cfg);
    }

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

    public List<ResponseRecord> chain(List<RequestConfig> 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++) {
            RequestConfig cfg = configs.get(i);
            // Interpolate context into URL
            String url = cfg.getUrl();
            for (Map.Entry<String, String> entry : ctx.entrySet()) {
                url = url.replace("{" + entry.getKey() + "}", entry.getValue());
            }
            cfg.setUrl(url);

            // Interpolate context into headers
            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.getJsonObject() != null) {
                System.out.println("Post title: " + record.getJsonObject().get("title").getAsString());
            }
        } 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.getJsonObject() != null) {
                System.out.println("Created post ID: " + record.getJsonObject().get("id").getAsInt());
            }
        } catch (Exception e) {
            System.out.println("Request failed: " + e.getMessage());
        }

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

            BiFunction<ResponseRecord, Map<String, String>, Map<String, String>> extractor = (record, ctx) -> {
                if (record.getJsonObject() != null && record.getJsonObject().has("id")) {
                    ctx.put("user_id", String.valueOf(record.getJsonObject().get("id").getAsInt()));
                }
                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().getAsJsonArray().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.getJsonObject() != null) {
                System.out.println("Updated title: " + record.getJsonObject().get("title").getAsString());
            }
        } 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();
    }
}