← All tasks
cppclaude-code/cpp-t2 #27Not a task: not reproduced

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

envgap__claude-code__cpp-t2-27

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

01 / FAILURE SIGNATURE

As the study recorded it

CURL not installed; rapidjson::Document non-copyable in ResponseRecord struct
Not a benchmark task.
  • In a clean container the reported failure did not reproduce, or the known fix did not make the project run.

02 / ENVIRONMENT RECIPE

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

03 / TASK AND FAILURE

claude-code/cpp-t2 #27 · read the task the agent was given
Claude Code wrote this cpp 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 C++ project for a clean Ubuntu 22.04 machine with only G++ 12+ and CMake 3.22+ installed. Include:
- Source code
- CMakeLists.txt 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

2 files, exactly as written, before any repair.

CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(rest-client VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include(FetchContent)

# RapidJSON
FetchContent_Declare(
    rapidjson
    GIT_REPOSITORY https://github.com/Tencent/rapidjson.git
    GIT_TAG v1.1.0
)
FetchContent_MakeAvailable(rapidjson)

# Find system libcurl
find_package(CURL REQUIRED)

add_executable(rest_client main.cpp)

target_include_directories(rest_client PRIVATE
    ${rapidjson_SOURCE_DIR}/include
)

target_link_libraries(rest_client PRIVATE
    CURL::libcurl
)

if(WIN32)
    target_link_libraries(rest_client PRIVATE ws2_32)
endif()
main.cpp
/**
 * HTTP REST Client with Retry Logic using libcurl and RapidJSON.
 *
 * Features:
 * - GET, POST, PUT, PATCH, DELETE with configurable retry/backoff
 * - Request chaining with context interpolation
 * - Response validation with custom rules
 * - Structured logging
 */

#include <curl/curl.h>
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/prettywriter.h>

#include <algorithm>
#include <chrono>
#include <cmath>
#include <functional>
#include <iomanip>
#include <iostream>
#include <map>
#include <memory>
#include <set>
#include <sstream>
#include <string>
#include <thread>
#include <vector>

// ---------------------------------------------------------------------------
// Logging
// ---------------------------------------------------------------------------

enum class LogLevel { DEBUG, INFO, WARNING, ERROR_LEVEL };

class Logger {
public:
    static void log(LogLevel level, const std::string& msg) {
        auto now = std::chrono::system_clock::now();
        auto time_t = std::chrono::system_clock::to_time_t(now);
        std::tm* gmt = std::gmtime(&time_t);
        char buf[64];
        std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", gmt);

        const char* level_str;
        switch (level) {
            case LogLevel::DEBUG:       level_str = "DEBUG"; break;
            case LogLevel::INFO:        level_str = "INFO"; break;
            case LogLevel::WARNING:     level_str = "WARN"; break;
            case LogLevel::ERROR_LEVEL: level_str = "ERROR"; break;
        }
        std::cout << "[" << buf << "] [" << level_str << "] " << msg << std::endl;
    }

    static void info(const std::string& msg)  { log(LogLevel::INFO, msg); }
    static void warn(const std::string& msg)  { log(LogLevel::WARNING, msg); }
    static void error(const std::string& msg) { log(LogLevel::ERROR_LEVEL, msg); }
    static void debug(const std::string& msg) { log(LogLevel::DEBUG, msg); }
};

// ---------------------------------------------------------------------------
// Response Record
// ---------------------------------------------------------------------------

struct ResponseRecord {
    std::string label;
    std::string method;
    std::string url;
    int status_code = 0;
    double elapsed_seconds = 0.0;
    std::map<std::string, std::string> headers;
    std::string raw_body;
    rapidjson::Document json_body;
    bool ok = false;
    std::vector<std::string> validation_errors;
    bool has_json = false;

    bool is_ok() const { return ok && validation_errors.empty(); }
};

// ---------------------------------------------------------------------------
// Validation
// ---------------------------------------------------------------------------

struct ValidationRule {
    std::string name;
    std::function<bool(const ResponseRecord&)> check;
    std::string message;
};

class ValidationError : public std::runtime_error {
public:
    std::vector<std::string> failed_rules;
    ValidationError(const std::vector<std::string>& rules)
        : std::runtime_error("Validation failed"), failed_rules(rules) {}
};

// Built-in validators
ValidationRule status_ok() {
    return {"status_ok",
            [](const ResponseRecord& r) { return r.status_code >= 200 && r.status_code < 300; },
            "Expected 2xx status code"};
}

ValidationRule status_equals(int code) {
    return {"status_" + std::to_string(code),
            [code](const ResponseRecord& r) { return r.status_code == code; },
            "Expected status code " + std::to_string(code)};
}

ValidationRule json_has_key(const std::string& key) {
    return {"has_key_" + key,
            [key](const ResponseRecord& r) {
                return r.has_json && r.json_body.IsObject() && r.json_body.HasMember(key.c_str());
            },
            "Expected JSON key '" + key + "' in response"};
}

ValidationRule max_response_time(double seconds) {
    return {"max_response_time",
            [seconds](const ResponseRecord& r) { return r.elapsed_seconds <= seconds; },
            "Response took longer than " + std::to_string(seconds) + "s"};
}

// ---------------------------------------------------------------------------
// Request Config
// ---------------------------------------------------------------------------

struct RequestConfig {
    std::string method;
    std::string url;
    std::map<std::string, std::string> headers;
    std::map<std::string, std::string> params;
    std::string json_body;
    std::string label;
    std::vector<ValidationRule> validators;
    int timeout_seconds = 30;
};

// ---------------------------------------------------------------------------
// CURL helpers
// ---------------------------------------------------------------------------

static size_t write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
    size_t total = size * nmemb;
    std::string* str = static_cast<std::string*>(userp);
    str->append(static_cast<char*>(contents), total);
    return total;
}

static size_t header_callback(char* buffer, size_t size, size_t nitems, void* userp) {
    size_t total = size * nitems;
    auto* headers = static_cast<std::map<std::string, std::string>*>(userp);
    std::string line(buffer, total);

    size_t colon = line.find(':');
    if (colon != std::string::npos) {
        std::string key = line.substr(0, colon);
        std::string value = line.substr(colon + 1);
        // Trim whitespace
        value.erase(0, value.find_first_not_of(" \t\r\n"));
        value.erase(value.find_last_not_of(" \t\r\n") + 1);
        // Lowercase key for consistency
        std::transform(key.begin(), key.end(), key.begin(), ::tolower);
        (*headers)[key] = value;
    }
    return total;
}

// ---------------------------------------------------------------------------
// REST Client
// ---------------------------------------------------------------------------

class RestClient {
public:
    RestClient(const std::string& base_url,
               const std::map<std::string, std::string>& default_headers = {},
               int max_retries = 3,
               double backoff_multiplier = 1.0,
               double backoff_max = 30.0,
               const std::vector<int>& retry_status_codes = {429, 500, 502, 503, 504})
        : base_url_(base_url),
          default_headers_(default_headers),
          max_retries_(max_retries),
          backoff_multiplier_(backoff_multiplier),
          backoff_max_(backoff_max),
          retry_status_codes_(retry_status_codes.begin(), retry_status_codes.end()) {
        while (!base_url_.empty() && base_url_.back() == '/') {
            base_url_.pop_back();
        }
        curl_global_init(CURL_GLOBAL_ALL);
    }

    ~RestClient() {
        curl_global_cleanup();
    }

    // Core request
    ResponseRecord request(RequestConfig cfg) {
        std::string url = build_url(cfg.url);
        Logger::info(">>> " + cfg.method + " " + url + " | label=" +
                     (cfg.label.empty() ? "(none)" : cfg.label));

        // Add query params
        if (!cfg.params.empty()) {
            url += "?";
            bool first = true;
            for (const auto& [k, v] : cfg.params) {
                if (!first) url += "&";
                url += k + "=" + v;
                first = false;
            }
        }

        for (int attempt = 0; attempt < max_retries_; ++attempt) {
            auto start = std::chrono::steady_clock::now();

            CURL* curl = curl_easy_init();
            if (!curl) {
                throw std::runtime_error("Failed to initialize CURL");
            }

            std::string response_body;
            std::map<std::string, std::string> response_headers;

            // Set URL
            curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
            curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
            curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_body);
            curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback);
            curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_headers);
            curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)cfg.timeout_seconds);
            curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);

            // Set method
            if (cfg.method == "POST") {
                curl_easy_setopt(curl, CURLOPT_POST, 1L);
            } else if (cfg.method == "PUT") {
                curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
            } else if (cfg.method == "PATCH") {
                curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
            } else if (cfg.method == "DELETE") {
                curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
            }

            // Set headers
            struct curl_slist* headers_list = nullptr;
            for (const auto& [k, v] : default_headers_) {
                headers_list = curl_slist_append(headers_list, (k + ": " + v).c_str());
            }
            for (const auto& [k, v] : cfg.headers) {
                headers_list = curl_slist_append(headers_list, (k + ": " + v).c_str());
            }

            // Set body
            if (!cfg.json_body.empty()) {
                headers_list = curl_slist_append(headers_list, "Content-Type: application/json");
                curl_easy_setopt(curl, CURLOPT_POSTFIELDS, cfg.json_body.c_str());
            }

            if (headers_list) {
                curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers_list);
            }

            CURLcode res = curl_easy_perform(curl);
            auto end = std::chrono::steady_clock::now();
            double elapsed = std::chrono::duration<double>(end - start).count();

            if (res != CURLE_OK) {
                curl_slist_free_all(headers_list);
                curl_easy_cleanup(curl);

                Logger::warn("Connection error on attempt " + std::to_string(attempt + 1) +
                             ": " + curl_easy_strerror(res));

                if (attempt < max_retries_ - 1) {
                    double wait = std::min(backoff_multiplier_ * std::pow(2.0, attempt), backoff_max_);
                    Logger::warn("Retrying in " + std::to_string(wait) + "s...");
                    std::this_thread::sleep_for(
                        std::chrono::milliseconds(static_cast<int>(wait * 1000)));
                    continue;
                }
                throw std::runtime_error("All retries exhausted - connection error: " +
                                          std::string(curl_easy_strerror(res)));
            }

            long http_code = 0;
            curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

            curl_slist_free_all(headers_list);
            curl_easy_cleanup(curl);

            // Check retryable status
            if (retry_status_codes_.count(static_cast<int>(http_code)) &&
                attempt < max_retries_ - 1) {
                Logger::warn("Retryable status " + std::to_string(http_code) +
                             " on attempt " + std::to_string(attempt + 1));
                double wait = std::min(backoff_multiplier_ * std::pow(2.0, attempt), backoff_max_);
                std::this_thread::sleep_for(
                    std::chrono::milliseconds(static_cast<int>(wait * 1000)));
                continue;
            }

            // Build record
            ResponseRecord record;
            record.label = cfg.label;
            record.method = cfg.method;
            record.url = url;
            record.status_code = static_cast<int>(http_code);
            record.elapsed_seconds = elapsed;
            record.headers = response_headers;
            record.raw_body = response_body;
            record.ok = (http_code >= 200 && http_code < 300);

            // Parse JSON
            record.json_body.Parse(response_body.c_str());
            record.has_json = !record.json_body.HasParseError();

            // Validate
            record.validation_errors = validate(record, cfg.validators);
            log_response(record);
            history_.push_back(std::move(record));

            auto& last = history_.back();
            if (!last.validation_errors.empty()) {
                throw ValidationError(last.validation_errors);
            }

            return last;
        }

        throw std::runtime_error("Request failed after all retries");
    }

    // Convenience methods
    ResponseRecord get(const std::string& url, const std::string& label = "",
                       const std::vector<ValidationRule>& validators = {}) {
        RequestConfig cfg;
        cfg.method = "GET";
        cfg.url = url;
        cfg.label = label;
        cfg.validators = validators;
        return request(cfg);
    }

    ResponseRecord post(const std::string& url, const std::string& json_body,
                        const std::string& label = "",
                        const std::vector<ValidationRule>& validators = {}) {
        RequestConfig cfg;
        cfg.method = "POST";
        cfg.url = url;
        cfg.json_body = json_body;
        cfg.label = label;
        cfg.validators = validators;
        return request(cfg);
    }

    ResponseRecord put(const std::string& url, const std::string& json_body,
                       const std::string& label = "",
                       const std::vector<ValidationRule>& validators = {}) {
        RequestConfig cfg;
        cfg.method = "PUT";
        cfg.url = url;
        cfg.json_body = json_body;
        cfg.label = label;
        cfg.validators = validators;
        return request(cfg);
    }

    ResponseRecord patch(const std::string& url, const std::string& json_body,
                         const std::string& label = "",
                         const std::vector<ValidationRule>& validators = {}) {
        RequestConfig cfg;
        cfg.method = "PATCH";
        cfg.url = url;
        cfg.json_body = json_body;
        cfg.label = label;
        cfg.validators = validators;
        return request(cfg);
    }

    ResponseRecord del(const std::string& url, const std::string& label = "",
                       const std::vector<ValidationRule>& validators = {}) {
        RequestConfig cfg;
        cfg.method = "DELETE";
        cfg.url = url;
        cfg.label = label;
        cfg.validators = validators;
        return request(cfg);
    }

    // Request chaining
    using Extractor = std::function<std::map<std::string, std::string>(
        const ResponseRecord&, std::map<std::string, std::string>)>;

    std::vector<ResponseRecord> chain(std::vector<RequestConfig>& configs,
                                       std::map<std::string, std::string> context = {},
                                       Extractor extractor = nullptr) {
        std::vector<ResponseRecord> results;

        for (size_t i = 0; i < configs.size(); ++i) {
            auto& cfg = configs[i];

            for (const auto& [key, val] : context) {
                std::string placeholder = "{" + key + "}";
                size_t pos;
                while ((pos = cfg.url.find(placeholder)) != std::string::npos) {
                    cfg.url.replace(pos, placeholder.size(), val);
                }
            }

            for (auto& [hk, hv] : cfg.headers) {
                for (const auto& [key, val] : context) {
                    std::string placeholder = "{" + key + "}";
                    size_t pos;
                    while ((pos = hv.find(placeholder)) != std::string::npos) {
                        hv.replace(pos, placeholder.size(), val);
                    }
                }
            }

            Logger::info("Chain step " + std::to_string(i + 1) + "/" +
                         std::to_string(configs.size()) + ": " +
                         (cfg.label.empty() ? cfg.url : cfg.label));

            auto record = request(cfg);
            results.push_back(std::move(record));

            if (extractor) {
                context = extractor(results.back(), context);
            }
        }

        return results;
    }

    // History
    void print_history() const {
        std::cout << "\n" << std::string(70, '=') << std::endl;
        std::cout << " Request History (" << history_.size() << " requests)" << std::endl;
        std::cout << std::string(70, '=') << std::endl;

        for (size_t i = 0; i < history_.size(); ++i) {
            const auto& r = history_[i];
            std::string status = r.is_ok() ? "OK" : "FAIL";
            std::cout << "  [" << (i + 1) << "] " << status
                      << " | " << r.method << " " << r.url
                      << " -> " << r.status_code
                      << " (" << std::fixed << std::setprecision(3) << r.elapsed_seconds << "s)"
                      << " | " << r.label << std::endl;
            for (const auto& err : r.validation_errors) {
                std::cout << "       ! " << err << std::endl;
            }
        }

        std::cout << std::string(70, '=') << "\n" << std::endl;
    }

private:
    std::string base_url_;
    std::map<std::string, std::string> default_headers_;
    int max_retries_;
    double backoff_multiplier_;
    double backoff_max_;
    std::set<int> retry_status_codes_;
    std::vector<ResponseRecord> history_;

    std::string build_url(const std::string& path) const {
        if (path.substr(0, 7) == "http://" || path.substr(0, 8) == "https://") {
            return path;
        }
        std::string clean_path = path;
        while (!clean_path.empty() && clean_path[0] == '/') {
            clean_path = clean_path.substr(1);
        }
        return base_url_ + "/" + clean_path;
    }

    std::vector<std::string> validate(const ResponseRecord& record,
                                       const std::vector<ValidationRule>& rules) {
        std::vector<std::string> errors;
        for (const auto& rule : rules) {
            try {
                if (!rule.check(record)) {
                    errors.push_back(rule.message.empty() ? rule.name : rule.message);
                    Logger::warn("Validation failed [" + rule.name + "]: " + rule.message);
                }
            } catch (const std::exception& e) {
                errors.push_back(rule.name + ": " + e.what());
            }
        }
        return errors;
    }

    void log_response(const ResponseRecord& record) const {
        std::ostringstream oss;
        oss << "<<< " << record.status_code << " " << record.url
            << " (" << std::fixed << std::setprecision(3) << record.elapsed_seconds << "s)"
            << " | label=" << (record.label.empty() ? "(none)" : record.label)
            << " | errors=";
        if (record.validation_errors.empty()) {
            oss << "none";
        } else {
            for (size_t i = 0; i < record.validation_errors.size(); ++i) {
                if (i > 0) oss << ", ";
                oss << record.validation_errors[i];
            }
        }
        if (record.is_ok()) {
            Logger::info(oss.str());
        } else {
            Logger::warn(oss.str());
        }
    }
};

// ---------------------------------------------------------------------------
// Demo / main
// ---------------------------------------------------------------------------

int main() {
    RestClient client(
        "https://jsonplaceholder.typicode.com",
        {{"Accept", "application/json"}},
        3,   // max retries
        0.5  // backoff multiplier
    );

    // 1. Simple GET with validation
    std::cout << "\n--- Simple GET with validation ---" << std::endl;
    try {
        auto record = client.get("/posts/1", "get_post_1",
                                  {status_ok(), json_has_key("title")});
        if (record.has_json && record.json_body.HasMember("title")) {
            std::cout << "Post title: " << record.json_body["title"].GetString() << std::endl;
        }
    } catch (const std::exception& e) {
        std::cout << "Request failed: " << e.what() << std::endl;
    }

    // 2. POST with body
    std::cout << "\n--- POST with body ---" << std::endl;
    try {
        std::string post_body = R"({"title":"Hello","body":"World","userId":1})";
        auto record = client.post("/posts", post_body, "create_post",
                                   {status_equals(201)});
        if (record.has_json && record.json_body.HasMember("id")) {
            std::cout << "Created post ID: " << record.json_body["id"].GetInt() << std::endl;
        }
    } catch (const std::exception& e) {
        std::cout << "Request failed: " << e.what() << std::endl;
    }

    // 3. Request chaining
    std::cout << "\n--- Request chaining ---" << std::endl;
    try {
        std::vector<RequestConfig> chain_configs = {
            {"GET", "/users/1", {}, {}, "", "get_user", {status_ok()}, 30},
            {"GET", "/users/{user_id}/posts", {}, {}, "", "get_user_posts", {status_ok()}, 30},
        };

        auto results = client.chain(chain_configs, {},
            [](const ResponseRecord& record, std::map<std::string, std::string> ctx) {
                if (record.has_json && record.json_body.IsObject() &&
                    record.json_body.HasMember("id")) {
                    ctx["user_id"] = std::to_string(record.json_body["id"].GetInt());
                }
                return ctx;
            });

        if (results.size() >= 2 && results[1].has_json && results[1].json_body.IsArray()) {
            std::cout << "User has " << results[1].json_body.Size() << " posts" << std::endl;
        }
    } catch (const std::exception& e) {
        std::cout << "Chain failed: " << e.what() << std::endl;
    }

    // 4. PUT request
    std::cout << "\n--- PUT request ---" << std::endl;
    try {
        std::string put_body = R"({"id":1,"title":"Updated","body":"Content","userId":1})";
        auto record = client.put("/posts/1", put_body, "update_post", {status_ok()});
        if (record.has_json && record.json_body.HasMember("title")) {
            std::cout << "Updated title: " << record.json_body["title"].GetString() << std::endl;
        }
    } catch (const std::exception& e) {
        std::cout << "Request failed: " << e.what() << std::endl;
    }

    // 5. DELETE request
    std::cout << "\n--- DELETE request ---" << std::endl;
    try {
        auto record = client.del("/posts/1", "delete_post", {status_ok()});
        std::cout << "Delete status: " << record.status_code << std::endl;
    } catch (const std::exception& e) {
        std::cout << "Request failed: " << e.what() << std::endl;
    }

    client.print_history();
    return 0;
}