HTTP REST Client with Retry Logic (javascript, written by Codex)
envgap__codex__javascript-t1-27
Written by a coding agent; not on GitHubWritten 2026-03-03
01 / FAILURE SIGNATURE
As the study recorded it
Expected string/Buffer/undefined but received null - got rejects null body
Not a benchmark task.
- Its repair changed source code, so it is not an environment task.
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
codex/javascript-t1 #27 · read the task the agent was given
Codex wrote this javascript 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 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
4 files, exactly as written, before any repair.
package-lock.json
{
"name": "http-rest-client-retry",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "http-rest-client-retry",
"version": "1.0.0",
"dependencies": {
"got": "14.4.3",
"jsonpath": "1.1.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/got": {
"version": "14.4.3"
},
"node_modules/jsonpath": {
"version": "1.1.1"
}
}
}
package.json
{
"name": "http-rest-client-retry",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js"
},
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"got": "14.4.3",
"jsonpath": "1.1.1"
}
}
README.md
# HTTP REST Client with Retry Logic (JavaScript)
Sends REST requests with configurable retry logic, backoff strategy, timeout, status-based retry filters, JSONPath validation, request chaining, and full request/response logging.
## Requirements
- Ubuntu 22.04
- Node.js 20+ (LTS)
## Dependencies (Pinned)
- `got@14.4.3`
- `jsonpath@1.1.1`
## Setup
```bash
npm install
```
## Run
```bash
node src/index.js https://httpbin.org/get
node src/index.js https://httpbin.org/post --method POST --data '{"hello":"world"}' --header "Content-Type: application/json"
node src/index.js https://httpbin.org/status/503 --retries 3 --backoff exponential --retry-delay 1 --retry-on 503
node src/index.js https://httpbin.org/get --expect-status 200 --expect-json '$.url'
node src/index.js --chain chain.json
node src/index.js
```
## Notes
- `--header` is repeatable.
- `--data` accepts inline JSON text; `--file` reads body from file.
- `--chain` expects a JSON array of request objects.
- No URL launches a built-in demo against `httpbin.org` including chain and retry behavior.
src/index.js
import fs from "fs";
import path from "path";
import got from "got";
import jp from "jsonpath";
const DEMO_CHAIN = [
{ method: "GET", url: "https://httpbin.org/get", expect_status: 200, expect_json: "$.url" },
{ method: "POST", url: "https://httpbin.org/post", data: { message: "hello" }, expect_status: 200, expect_json: "$.json.message" },
{ method: "GET", url: "https://httpbin.org/status/503", retries: 2, retry_on: [503], expect_status: 503 },
{ method: "GET", url: "https://httpbin.org/anything/{{step1.$.json.message}}", expect_status: 200 },
];
function parseArgs(argv) {
const options = {};
const positional = [];
for (let i = 0; i < argv.length; i += 1) {
const t = argv[i];
if (t.startsWith("--")) {
const key = t.slice(2);
const next = argv[i + 1];
const value = next && !next.startsWith("--") ? next : true;
if (value !== true) i += 1;
if (key === "header") {
if (!options.header) options.header = [];
options.header.push(String(value));
} else options[key] = value;
} else positional.push(t);
}
return { options, positional };
}
function parseHeaders(raw = []) {
const out = {};
for (const h of raw) {
const idx = h.indexOf(":");
if (idx <= 0) continue;
out[h.slice(0, idx).trim()] = h.slice(idx + 1).trim();
}
return out;
}
function parseRetryOn(raw) {
const text = String(raw || "429,500,502,503,504");
return new Set(text.split(",").map((x) => Number.parseInt(x.trim(), 10)).filter(Number.isFinite));
}
function delay(ms) {
return new Promise((r) => setTimeout(r, ms));
}
function safeJsonParse(s) {
try {
return { ok: true, value: JSON.parse(s) };
} catch {
return { ok: false, value: null };
}
}
function replaceRefs(text, context) {
return String(text).replace(/\{\{step(\d+)\.(\$[^}]+)\}\}/g, (_, idxRaw, expr) => {
const idx = Number.parseInt(idxRaw, 10);
const body = context[idx]?.response_json;
if (!body) return "";
const vals = jp.query(body, expr);
return vals.length ? String(vals[0]) : "";
});
}
async function requestWithRetry(spec, defaults, context, log) {
const method = String(spec.method || defaults.method || "GET").toUpperCase();
const retryOn = new Set(spec.retry_on || [...defaults.retryOn]);
const retries = Number(spec.retries ?? defaults.retries);
const timeoutMs = Number(spec.timeout ?? defaults.timeout) * 1000;
const backoff = String(spec.backoff || defaults.backoff);
const retryDelayMs = Number(spec.retryDelay ?? defaults.retryDelay) * 1000;
const headers = { ...defaults.headers, ...(spec.headers || {}) };
const url = replaceRefs(spec.url, context);
const dataObj = spec.data != null ? spec.data : null;
const bodyRaw = dataObj == null ? null : (typeof dataObj === "string" ? replaceRefs(dataObj, context) : JSON.stringify(dataObj));
let attempt = 0;
while (true) {
const started = Date.now();
try {
const res = await got(url, {
method,
headers,
body: bodyRaw,
throwHttpErrors: false,
retry: { limit: 0 },
timeout: { request: timeoutMs },
followRedirect: true,
maxRedirects: 10,
});
const elapsed = Date.now() - started;
const parsed = safeJsonParse(res.body);
const entry = {
attempt,
method,
url,
request_headers: headers,
request_body: bodyRaw,
status_code: res.statusCode,
response_headers: res.headers,
response_body: res.body,
response_json: parsed.ok ? parsed.value : null,
timing_ms: elapsed,
};
log.push(entry);
if (retryOn.has(res.statusCode) && attempt < retries) {
attempt += 1;
const d = backoff === "linear" ? retryDelayMs * attempt : retryDelayMs * (2 ** (attempt - 1));
await delay(d);
continue;
}
if (spec.expect_status != null && Number(spec.expect_status) !== res.statusCode) {
throw new Error(`Expected status ${spec.expect_status}, got ${res.statusCode}`);
}
if (spec.expect_json && parsed.ok) {
const vals = jp.query(parsed.value, String(spec.expect_json));
if (!vals.length) throw new Error(`JSONPath not found: ${spec.expect_json}`);
} else if (spec.expect_json && !parsed.ok) {
throw new Error("Expected JSON response but body is not JSON");
}
return entry;
} catch (e) {
const elapsed = Date.now() - started;
log.push({
attempt,
method,
url,
request_headers: headers,
request_body: bodyRaw,
status_code: 0,
response_headers: {},
response_body: "",
response_json: null,
timing_ms: elapsed,
error: e instanceof Error ? e.message : String(e),
});
if (attempt >= retries) throw e;
attempt += 1;
const d = backoff === "linear" ? retryDelayMs * attempt : retryDelayMs * (2 ** (attempt - 1));
await delay(d);
}
}
}
function printResponse(entry) {
console.log(`Status: ${entry.status_code}`);
console.log("Headers:");
for (const [k, v] of Object.entries(entry.response_headers || {})) console.log(` ${k}: ${v}`);
console.log("Body:");
if (entry.response_json) console.log(JSON.stringify(entry.response_json, null, 2));
else console.log(entry.response_body || "");
}
async function runChain(chain, defaults, outputPath) {
const logs = [];
const context = [];
for (const spec of chain) {
const log = [];
const result = await requestWithRetry(spec, defaults, context, log);
logs.push({ request: spec, attempts: log });
context.push(result);
printResponse(result);
}
fs.writeFileSync(outputPath, `${JSON.stringify({ generated_at: new Date().toISOString(), chain, logs }, null, 2)}\n`, "utf8");
console.log(`Saved log: ${outputPath}`);
}
async function main() {
const { options, positional } = parseArgs(process.argv.slice(2));
const url = positional[0] || "";
const defaults = {
method: String(options.method || "GET"),
headers: parseHeaders(options.header || []),
retries: Number(options.retries ?? 3),
backoff: String(options.backoff || "exponential"),
retryDelay: Number(options["retry-delay"] ?? 1),
retryOn: parseRetryOn(options["retry-on"]),
timeout: Number(options.timeout ?? 30),
};
const output = path.resolve(String(options.output || "request_log.json"));
if (options.chain) {
const chain = JSON.parse(fs.readFileSync(path.resolve(String(options.chain)), "utf8"));
await runChain(Array.isArray(chain) ? chain : chain.requests, defaults, output);
return;
}
if (!url) {
await runChain(DEMO_CHAIN, defaults, output);
return;
}
let data = null;
if (options.file) data = fs.readFileSync(path.resolve(String(options.file)), "utf8");
else if (options.data) data = String(options.data);
const spec = {
method: defaults.method,
url,
headers: defaults.headers,
data,
expect_status: options["expect-status"] ? Number(options["expect-status"]) : undefined,
expect_json: options["expect-json"] || undefined,
};
const log = [];
const result = await requestWithRetry(spec, defaults, [], log);
printResponse(result);
fs.writeFileSync(output, `${JSON.stringify({ generated_at: new Date().toISOString(), request: spec, attempts: log }, null, 2)}\n`, "utf8");
console.log(`Saved log: ${output}`);
}
main().catch((e) => {
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
process.exit(1);
});