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;
}