← All tasks
javascriptclaude-code/javascript-t1 #27Not a task: already works

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