HTTP REST Client with Retry Logic (javascript, written by Claude Code)
envgap__claude-code__javascript-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
package.json- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
claude-code/javascript-t1 #27 · read the task the agent was given
Claude Code wrote this javascript 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 JavaScript project for a clean Ubuntu 22.04 machine with only Node.js 20+ (LTS) installed. Include: - Source code - package.json 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.
client.js
/**
* HTTP REST Client with Retry Logic
*
* Features:
* - GET, POST, PUT, PATCH, DELETE with retry/backoff via axios-retry
* - Request chaining with context interpolation
* - Response validation with custom rules
* - Structured logging with chalk
*/
const axios = require('axios');
const axiosRetry = require('axios-retry').default;
const chalk = require('chalk');
// ---------------------------------------------------------------------------
// Logging
// ---------------------------------------------------------------------------
class Logger {
static timestamp() {
return new Date().toISOString();
}
static info(msg) {
console.log(chalk.blue(`[${Logger.timestamp()}] [INFO] ${msg}`));
}
static warn(msg) {
console.log(chalk.yellow(`[${Logger.timestamp()}] [WARN] ${msg}`));
}
static error(msg) {
console.log(chalk.red(`[${Logger.timestamp()}] [ERROR] ${msg}`));
}
static success(msg) {
console.log(chalk.green(`[${Logger.timestamp()}] [OK] ${msg}`));
}
static debug(msg) {
console.log(chalk.gray(`[${Logger.timestamp()}] [DEBUG] ${msg}`));
}
}
// ---------------------------------------------------------------------------
// Validation
// ---------------------------------------------------------------------------
class ValidationError extends Error {
constructor(failedRules) {
super(`Validation failed: ${failedRules.join(', ')}`);
this.name = 'ValidationError';
this.failedRules = failedRules;
}
}
/**
* Create a validation rule.
* @param {string} name - Rule name
* @param {function} check - Function(response) => boolean
* @param {string} message - Error message on failure
*/
function createRule(name, check, message = '') {
return { name, check, message: message || name };
}
// Built-in validators
const statusOk = () => createRule(
'status_ok',
(resp) => resp.status >= 200 && resp.status < 300,
'Expected 2xx status code'
);
const statusEquals = (code) => createRule(
`status_${code}`,
(resp) => resp.status === code,
`Expected status code ${code}`
);
const jsonHasKey = (key) => createRule(
`has_key_${key}`,
(resp) => resp.data && typeof resp.data === 'object' && key in resp.data,
`Expected JSON key '${key}' in response`
);
const contentTypeIs = (expected) => createRule(
'content_type',
(resp) => (resp.headers['content-type'] || '').includes(expected),
`Expected Content-Type containing '${expected}'`
);
const maxResponseTime = (ms) => createRule(
'max_response_time',
(resp) => resp.duration <= ms,
`Response took longer than ${ms}ms`
);
// ---------------------------------------------------------------------------
// Response Record
// ---------------------------------------------------------------------------
class ResponseRecord {
constructor({ label, method, url, status, elapsedMs, headers, body, ok }) {
this.label = label || '';
this.method = method;
this.url = url;
this.status = status;
this.elapsedMs = elapsedMs;
this.headers = headers;
this.body = body;
this.ok = ok;
this.validationErrors = [];
}
get isOk() {
return this.ok && this.validationErrors.length === 0;
}
}
// ---------------------------------------------------------------------------
// REST Client
// ---------------------------------------------------------------------------
class RestClient {
/**
* @param {object} options
* @param {string} options.baseURL - Base URL for all requests
* @param {object} options.defaultHeaders - Default headers
* @param {number} options.maxRetries - Max retry attempts (default 3)
* @param {number} options.retryDelay - Initial retry delay in ms (default 1000)
* @param {number[]} options.retryStatusCodes - Status codes to retry on
* @param {number} options.timeout - Request timeout in ms (default 30000)
*/
constructor(options = {}) {
this.baseURL = (options.baseURL || '').replace(/\/+$/, '');
this.defaultHeaders = options.defaultHeaders || {};
this.maxRetries = options.maxRetries || 3;
this.retryDelay = options.retryDelay || 1000;
this.retryStatusCodes = options.retryStatusCodes || [429, 500, 502, 503, 504];
this.timeout = options.timeout || 30000;
this.history = [];
// Create axios instance
this.client = axios.create({
baseURL: this.baseURL,
headers: this.defaultHeaders,
timeout: this.timeout,
validateStatus: () => true, // Don't throw on non-2xx
});
// Add timing interceptor
this.client.interceptors.request.use((config) => {
config.metadata = { startTime: Date.now() };
return config;
});
this.client.interceptors.response.use((response) => {
const start = response.config.metadata?.startTime || Date.now();
response.duration = Date.now() - start;
return response;
});
// Configure retry
axiosRetry(this.client, {
retries: this.maxRetries,
retryDelay: (retryCount) => {
const delay = this.retryDelay * Math.pow(2, retryCount - 1);
Logger.warn(`Retry attempt ${retryCount}, waiting ${delay}ms...`);
return delay;
},
retryCondition: (error) => {
if (axiosRetry.isNetworkOrIdempotentRequestError(error)) return true;
if (error.response && this.retryStatusCodes.includes(error.response.status)) return true;
return false;
},
});
}
// ---- core request ----------------------------------------------------
/**
* Execute a single HTTP request with validation.
* @param {object} config
* @param {string} config.method
* @param {string} config.url
* @param {object} config.headers
* @param {object} config.params
* @param {*} config.data - Request body
* @param {string} config.label
* @param {Array} config.validators - Array of validation rules
*/
async request(config) {
const {
method = 'GET',
url = '/',
headers = {},
params = {},
data = undefined,
label = '',
validators = [],
} = config;
Logger.info(`>>> ${method.toUpperCase()} ${url} | label=${label || '(none)'}`);
try {
const response = await this.client.request({
method: method.toUpperCase(),
url,
headers,
params,
data,
});
const record = new ResponseRecord({
label,
method: method.toUpperCase(),
url: `${this.baseURL}${url}`,
status: response.status,
elapsedMs: response.duration || 0,
headers: response.headers,
body: response.data,
ok: response.status >= 200 && response.status < 300,
});
// Run validation
const errors = this._validate(response, validators);
record.validationErrors = errors;
this._logResponse(record);
this.history.push(record);
if (errors.length > 0) {
throw new ValidationError(errors);
}
return record;
} catch (error) {
if (error instanceof ValidationError) throw error;
Logger.error(`Request failed: ${error.message}`);
throw error;
}
}
_validate(response, rules) {
const errors = [];
for (const rule of rules) {
try {
if (!rule.check(response)) {
errors.push(rule.message);
Logger.warn(`Validation failed [${rule.name}]: ${rule.message}`);
}
} catch (e) {
errors.push(`${rule.name}: ${e.message}`);
}
}
return errors;
}
_logResponse(record) {
const logFn = record.isOk ? Logger.success : Logger.warn;
logFn(
`<<< ${record.status} ${record.url} (${record.elapsedMs}ms) | ` +
`label=${record.label || '(none)'} | ` +
`errors=${record.validationErrors.length === 0 ? 'none' : record.validationErrors.join(', ')}`
);
}
// ---- convenience methods ---------------------------------------------
async get(url, options = {}) {
return this.request({ method: 'GET', url, ...options });
}
async post(url, data, options = {}) {
return this.request({ method: 'POST', url, data, ...options });
}
async put(url, data, options = {}) {
return this.request({ method: 'PUT', url, data, ...options });
}
async patch(url, data, options = {}) {
return this.request({ method: 'PATCH', url, data, ...options });
}
async delete(url, options = {}) {
return this.request({ method: 'DELETE', url, ...options });
}
// ---- request chaining ------------------------------------------------
/**
* Execute a sequence of requests, passing context between them.
* @param {Array} configs - Array of request configs
* @param {object} context - Initial context for interpolation
* @param {function} extractor - (record, ctx) => updatedCtx
*/
async chain(configs, context = {}, extractor = null) {
const ctx = { ...context };
const results = [];
for (let i = 0; i < configs.length; i++) {
const cfg = { ...configs[i] };
// Interpolate context into URL
if (cfg.url) {
for (const [key, val] of Object.entries(ctx)) {
cfg.url = cfg.url.replace(`{${key}}`, val);
}
}
// Interpolate context into headers
if (cfg.headers) {
const interpolated = {};
for (const [k, v] of Object.entries(cfg.headers)) {
let val = v;
for (const [ck, cv] of Object.entries(ctx)) {
val = val.replace(`{${ck}}`, cv);
}
interpolated[k] = val;
}
cfg.headers = interpolated;
}
Logger.info(`Chain step ${i + 1}/${configs.length}: ${cfg.label || cfg.url}`);
const record = await this.request(cfg);
results.push(record);
if (extractor) {
const updated = extractor(record, ctx);
Object.assign(ctx, updated);
}
}
return results;
}
// ---- history ---------------------------------------------------------
printHistory() {
console.log('\n' + '='.repeat(70));
console.log(` Request History (${this.history.length} requests)`);
console.log('='.repeat(70));
this.history.forEach((rec, i) => {
const status = rec.isOk ? chalk.green('OK') : chalk.red('FAIL');
console.log(
` [${i + 1}] ${status} | ${rec.method} ${rec.url} ` +
`-> ${rec.status} (${rec.elapsedMs}ms) | ${rec.label}`
);
rec.validationErrors.forEach((err) => {
console.log(chalk.red(` ! ${err}`));
});
});
console.log('='.repeat(70) + '\n');
}
}
// ---------------------------------------------------------------------------
// Demo / main
// ---------------------------------------------------------------------------
async function main() {
const client = new RestClient({
baseURL: 'https://jsonplaceholder.typicode.com',
defaultHeaders: { 'Accept': 'application/json' },
maxRetries: 3,
retryDelay: 500,
});
// 1. Simple GET with validation
console.log('\n--- Simple GET with validation ---');
try {
const record = await client.get('/posts/1', {
label: 'get_post_1',
validators: [statusOk(), jsonHasKey('title'), contentTypeIs('json')],
});
console.log(`Post title: ${record.body.title}`);
} catch (e) {
console.log(`Request failed: ${e.message}`);
}
// 2. POST with body
console.log('\n--- POST with body ---');
try {
const record = await client.post('/posts', {
title: 'Hello',
body: 'World',
userId: 1,
}, {
label: 'create_post',
validators: [statusEquals(201)],
});
console.log(`Created post ID: ${record.body.id}`);
} catch (e) {
console.log(`Request failed: ${e.message}`);
}
// 3. Request chaining
console.log('\n--- Request chaining ---');
try {
const chainConfigs = [
{
method: 'GET',
url: '/users/1',
label: 'get_user',
validators: [statusOk()],
},
{
method: 'GET',
url: '/users/{user_id}/posts',
label: 'get_user_posts',
validators: [statusOk()],
},
];
const results = await client.chain(chainConfigs, {}, (record, ctx) => {
if (record.body && record.body.id) {
ctx.user_id = record.body.id;
}
return ctx;
});
if (results.length >= 2 && Array.isArray(results[1].body)) {
console.log(`User has ${results[1].body.length} posts`);
}
} catch (e) {
console.log(`Chain failed: ${e.message}`);
}
// 4. PUT request
console.log('\n--- PUT request ---');
try {
const record = await client.put('/posts/1', {
id: 1, title: 'Updated', body: 'Content', userId: 1,
}, {
label: 'update_post',
validators: [statusOk()],
});
console.log(`Updated title: ${record.body.title}`);
} catch (e) {
console.log(`Request failed: ${e.message}`);
}
// 5. DELETE request
console.log('\n--- DELETE request ---');
try {
const record = await client.delete('/posts/1', {
label: 'delete_post',
validators: [statusOk()],
});
console.log(`Delete status: ${record.status}`);
} catch (e) {
console.log(`Request failed: ${e.message}`);
}
client.printHistory();
}
// Export for module use
module.exports = {
RestClient,
ResponseRecord,
ValidationError,
createRule,
statusOk,
statusEquals,
jsonHasKey,
contentTypeIs,
maxResponseTime,
};
// Run if executed directly
if (require.main === module) {
main().catch(console.error);
}
package.json
{
"name": "rest-client",
"version": "1.0.0",
"description": "HTTP REST Client with retry/backoff, request chaining, response validation, and structured logging",
"main": "client.js",
"scripts": {
"start": "node client.js"
},
"keywords": [
"http",
"rest",
"client",
"retry",
"axios"
],
"license": "MIT",
"dependencies": {
"axios": "1.6.7",
"axios-retry": "4.0.0",
"chalk": "4.1.2"
}
}