HTTP REST Client with Retry Logic (cpp, written by Claude Code)
envgap__claude-code__cpp-t1-27
Written by a coding agent; not on GitHubWritten 2026-02-27
01 / FAILURE SIGNATURE
As the study recorded it
No identifying execution failure has been captured.
Not a benchmark task.
- The project already builds and runs before the fix, so there is nothing to repair.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
CMakeLists.txt- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
claude-code/cpp-t1 #27 · read the task the agent was given
Claude Code wrote this cpp project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written. Task given to the agent: TASK: HTTP 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)
# cpp-httplib
FetchContent_Declare(
httplib
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git
GIT_TAG v0.15.3
)
FetchContent_MakeAvailable(httplib)
# nlohmann/json
FetchContent_Declare(
json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.3
)
FetchContent_MakeAvailable(json)
add_executable(rest_client main.cpp)
target_link_libraries(rest_client PRIVATE
httplib::httplib
nlohmann_json::nlohmann_json
)
# OpenSSL for HTTPS support
find_package(OpenSSL QUIET)
if(OpenSSL_FOUND)
target_compile_definitions(rest_client PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
target_link_libraries(rest_client PRIVATE OpenSSL::SSL OpenSSL::Crypto)
message(STATUS "OpenSSL found - HTTPS support enabled")
else()
message(STATUS "OpenSSL not found - HTTPS support disabled")
endif()
if(WIN32)
target_link_libraries(rest_client PRIVATE ws2_32)
endif()
main.cpp
/**
* HTTP REST Client with Retry Logic using cpp-httplib and nlohmann/json.
*
* Features:
* - GET, POST, PUT, PATCH, DELETE with configurable retry/backoff
* - Request chaining with context interpolation
* - Response validation with custom rules
* - Structured logging
*/
#include <httplib.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <functional>
#include <iomanip>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
using json = nlohmann::json;
// ---------------------------------------------------------------------------
// 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;
json json_body;
bool ok = false;
std::vector<std::string> validation_errors;
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.json_body.is_object() && r.json_body.contains(key);
},
"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;
json json_body;
std::string label;
std::vector<ValidationRule> validators;
int timeout_seconds = 30;
};
// ---------------------------------------------------------------------------
// 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()) {
// Strip trailing slashes
while (!base_url_.empty() && base_url_.back() == '/') {
base_url_.pop_back();
}
}
// 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));
// Parse host and path from URL
std::string scheme, host, path;
parse_url(url, scheme, host, path);
// Add query params to path
if (!cfg.params.empty()) {
path += "?";
bool first = true;
for (const auto& [k, v] : cfg.params) {
if (!first) path += "&";
path += k + "=" + v;
first = false;
}
}
// Merge headers
auto merged_headers = default_headers_;
for (const auto& [k, v] : cfg.headers) {
merged_headers[k] = v;
}
// Prepare body
std::string body_str;
if (!cfg.json_body.is_null()) {
body_str = cfg.json_body.dump();
merged_headers["Content-Type"] = "application/json";
}
// Execute with retry
for (int attempt = 0; attempt < max_retries_; ++attempt) {
auto start = std::chrono::steady_clock::now();
try {
httplib::Client cli(scheme + "://" + host);
cli.set_connection_timeout(cfg.timeout_seconds);
cli.set_read_timeout(cfg.timeout_seconds);
httplib::Headers hdrs;
for (const auto& [k, v] : merged_headers) {
hdrs.emplace(k, v);
}
httplib::Result result = execute_method(cli, cfg.method, path, hdrs, body_str);
auto end = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(end - start).count();
if (!result) {
Logger::warn("Connection error on attempt " + std::to_string(attempt + 1));
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");
}
auto& resp = result.value();
// Check if we should retry based on status code
if (retry_status_codes_.count(resp.status) && attempt < max_retries_ - 1) {
Logger::warn("Retryable status " + std::to_string(resp.status) +
" 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 response record
ResponseRecord record;
record.label = cfg.label;
record.method = cfg.method;
record.url = url;
record.status_code = resp.status;
record.elapsed_seconds = elapsed;
record.raw_body = resp.body;
record.ok = (resp.status >= 200 && resp.status < 300);
for (const auto& [k, v] : resp.headers) {
record.headers[k] = v;
}
// Parse JSON
try {
record.json_body = json::parse(resp.body);
} catch (...) {
record.json_body = nullptr;
}
// Validate
record.validation_errors = validate(record, cfg.validators);
log_response(record);
history_.push_back(record);
if (!record.validation_errors.empty()) {
throw ValidationError(record.validation_errors);
}
return record;
} catch (const ValidationError&) {
throw;
} catch (const std::exception& e) {
if (attempt >= max_retries_ - 1) {
Logger::error("All retries exhausted for " + cfg.method + " " + url +
": " + e.what());
throw;
}
}
}
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 json& body,
const std::string& label = "",
const std::vector<ValidationRule>& validators = {}) {
RequestConfig cfg;
cfg.method = "POST";
cfg.url = url;
cfg.json_body = body;
cfg.label = label;
cfg.validators = validators;
return request(cfg);
}
ResponseRecord put(const std::string& url, const json& body,
const std::string& label = "",
const std::vector<ValidationRule>& validators = {}) {
RequestConfig cfg;
cfg.method = "PUT";
cfg.url = url;
cfg.json_body = body;
cfg.label = label;
cfg.validators = validators;
return request(cfg);
}
ResponseRecord patch(const std::string& url, const json& body,
const std::string& label = "",
const std::vector<ValidationRule>& validators = {}) {
RequestConfig cfg;
cfg.method = "PATCH";
cfg.url = url;
cfg.json_body = 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];
// Interpolate context into URL
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);
}
}
// Interpolate context into headers
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(record);
if (extractor) {
context = extractor(record, context);
}
}
return results;
}
// History
const std::vector<ResponseRecord>& get_history() const { return 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;
}
void parse_url(const std::string& url, std::string& scheme,
std::string& host, std::string& path) const {
size_t scheme_end = url.find("://");
if (scheme_end == std::string::npos) {
scheme = "http";
scheme_end = 0;
} else {
scheme = url.substr(0, scheme_end);
scheme_end += 3;
}
size_t path_start = url.find('/', scheme_end);
if (path_start == std::string::npos) {
host = url.substr(scheme_end);
path = "/";
} else {
host = url.substr(scheme_end, path_start - scheme_end);
path = url.substr(path_start);
}
}
httplib::Result execute_method(httplib::Client& cli, const std::string& method,
const std::string& path, const httplib::Headers& headers,
const std::string& body) {
if (method == "GET") return cli.Get(path, headers);
if (method == "POST") return cli.Post(path, headers, body, "application/json");
if (method == "PUT") return cli.Put(path, headers, body, "application/json");
if (method == "PATCH") return cli.Patch(path, headers, body, "application/json");
if (method == "DELETE") return cli.Delete(path, headers);
throw std::invalid_argument("Unsupported HTTP method: " + method);
}
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.json_body.is_object()) {
std::cout << "Post title: " << record.json_body["title"].get<std::string>()
<< 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 {
json post_body = {{"title", "Hello"}, {"body", "World"}, {"userId", 1}};
auto record = client.post("/posts", post_body, "create_post",
{status_equals(201)});
if (record.json_body.is_object()) {
std::cout << "Created post ID: " << record.json_body["id"] << 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", {}, {}, nullptr, "get_user", {status_ok()}, 30},
{"GET", "/users/{user_id}/posts", {}, {}, nullptr, "get_user_posts", {status_ok()}, 30},
};
auto results = client.chain(chain_configs, {},
[](const ResponseRecord& record, std::map<std::string, std::string> ctx) {
if (record.json_body.is_object() && record.json_body.contains("id")) {
ctx["user_id"] = std::to_string(record.json_body["id"].get<int>());
}
return ctx;
});
if (results.size() >= 2 && results[1].json_body.is_array()) {
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 {
json put_body = {{"id", 1}, {"title", "Updated"}, {"body", "Content"}, {"userId", 1}};
auto record = client.put("/posts/1", put_body, "update_post", {status_ok()});
if (record.json_body.is_object()) {
std::cout << "Updated title: " << record.json_body["title"].get<std::string>()
<< 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;
}