← All tasks
pythoncodex/python-t1 #27Not a task: already works

HTTP REST Client with Retry Logic (python, written by Codex)

envgap__codex__python-t1-27

Written by a coding agent; not on GitHubWritten 2026-03-03

01 / FAILURE SIGNATURE

As the study recorded it

None
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
requirements.txt
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/python-t1 #27 · read the task the agent was given
Codex wrote this python 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 Python project for a clean Ubuntu 22.04 machine with only Python 3.10+ installed. Include:
- Source code
- requirements.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

3 files, exactly as written, before any repair.

README.md
# HTTP REST Client with Retry Logic (Python)

Sends REST requests with retry/backoff policies, status-code retry filters, timeout controls, response validation via status and JSONPath, request chaining from JSON recipes, and full request/response logging.

## Requirements

- Ubuntu 22.04
- Python 3.10+

## Dependencies (Pinned)

- `requests==2.32.3`
- `jsonpath-ng==1.6.1`

## Setup

```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

## Run

```bash
python src/main.py https://httpbin.org/get
python src/main.py https://httpbin.org/post --method POST --data '{"hello":"world"}' --header "Content-Type: application/json"
python src/main.py https://httpbin.org/status/503 --retries 3 --backoff exponential --retry-delay 1 --retry-on 503
python src/main.py https://httpbin.org/get --expect-status 200 --expect-json '$.url'
python src/main.py --chain chain.json
python src/main.py
```

## Notes

- `--header` is repeatable.
- `--data` accepts inline body text, `--file` reads request body from disk.
- `--chain` accepts a JSON array (or object with `requests` array).
- No-URL mode runs a built-in demo chain on `httpbin.org`.
requirements.txt
certifi==2024.8.30
charset-normalizer==3.3.2
idna==3.8
jsonpath-ng==1.6.1
ply==3.11
requests==2.32.3
urllib3==2.2.2
src/main.py
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import time
from pathlib import Path

import requests
from jsonpath_ng.ext import parse as parse_jsonpath

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},
]


def parse_headers(values: list[str]) -> dict[str, str]:
    out = {}
    for h in values:
        if ":" not in h:
            continue
        k, v = h.split(":", 1)
        out[k.strip()] = v.strip()
    return out


def parse_retry_on(raw: str | None) -> set[int]:
    text = raw or "429,500,502,503,504"
    out = set()
    for p in text.split(","):
        p = p.strip()
        if p:
            out.add(int(p))
    return out


def replace_refs(text: str, context: list[dict]) -> str:
    out = text
    while "{{step" in out and "}}" in out:
        start = out.find("{{step")
        end = out.find("}}", start)
        if start < 0 or end < 0:
            break
        token = out[start + 2:end]
        # token format: step<idx>.<jsonpath>
        prefix, expr = token.split(".", 1)
        idx = int(prefix[4:])
        body = context[idx].get("response_json")
        value = ""
        if body is not None:
            vals = [m.value for m in parse_jsonpath(expr).find(body)]
            if vals:
                value = str(vals[0])
        out = out[:start] + value + out[end + 2:]
    return out


def request_with_retry(spec: dict, defaults: dict, context: list[dict], logs: list[dict]) -> dict:
    method = str(spec.get("method", defaults["method"])).upper()
    url = replace_refs(str(spec["url"]), context)
    headers = {**defaults["headers"], **spec.get("headers", {})}
    retries = int(spec.get("retries", defaults["retries"]))
    retry_on = set(spec.get("retry_on", defaults["retry_on"]))
    backoff = str(spec.get("backoff", defaults["backoff"]))
    retry_delay = float(spec.get("retry_delay", defaults["retry_delay"]))
    timeout = float(spec.get("timeout", defaults["timeout"]))

    body = spec.get("data")
    if isinstance(body, str):
        body = replace_refs(body, context)
    elif body is not None:
        body = json.dumps(body)

    attempt = 0
    while True:
        t0 = time.perf_counter()
        try:
            resp = requests.request(method, url, headers=headers, data=body, timeout=timeout, allow_redirects=True)
            elapsed = (time.perf_counter() - t0) * 1000.0
            try:
                parsed_json = resp.json()
            except Exception:
                parsed_json = None
            entry = {
                "attempt": attempt,
                "method": method,
                "url": url,
                "request_headers": headers,
                "request_body": body,
                "status_code": resp.status_code,
                "response_headers": dict(resp.headers),
                "response_body": resp.text,
                "response_json": parsed_json,
                "timing_ms": round(elapsed, 2),
            }
            logs.append(entry)

            if resp.status_code in retry_on and attempt < retries:
                attempt += 1
                delay = retry_delay * (attempt if backoff == "linear" else (2 ** (attempt - 1)))
                time.sleep(delay)
                continue

            expected = spec.get("expect_status")
            if expected is not None and int(expected) != resp.status_code:
                raise RuntimeError(f"Expected status {expected}, got {resp.status_code}")
            expr = spec.get("expect_json")
            if expr:
                if parsed_json is None:
                    raise RuntimeError("Expected JSON response but response is not JSON")
                vals = [m.value for m in parse_jsonpath(str(expr)).find(parsed_json)]
                if not vals:
                    raise RuntimeError(f"JSONPath not found: {expr}")
            return entry
        except Exception as exc:
            elapsed = (time.perf_counter() - t0) * 1000.0
            logs.append({
                "attempt": attempt,
                "method": method,
                "url": url,
                "request_headers": headers,
                "request_body": body,
                "status_code": 0,
                "response_headers": {},
                "response_body": "",
                "response_json": None,
                "timing_ms": round(elapsed, 2),
                "error": str(exc),
            })
            if attempt >= retries:
                raise
            attempt += 1
            delay = retry_delay * (attempt if backoff == "linear" else (2 ** (attempt - 1)))
            time.sleep(delay)


def print_response(entry: dict) -> None:
    print(f"Status: {entry['status_code']}")
    print("Headers:")
    for k, v in (entry.get("response_headers") or {}).items():
        print(f"  {k}: {v}")
    print("Body:")
    if entry.get("response_json") is not None:
        print(json.dumps(entry["response_json"], indent=2))
    else:
        print(entry.get("response_body", ""))


def run_chain(chain: list[dict], defaults: dict, output: Path) -> None:
    logs = []
    context = []
    for step in chain:
        attempt_log = []
        result = request_with_retry(step, defaults, context, attempt_log)
        logs.append({"request": step, "attempts": attempt_log})
        context.append(result)
        print_response(result)
    output.write_text(json.dumps({"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "chain": chain, "logs": logs}, indent=2), encoding="utf-8")
    print(f"Saved log: {output}")


def parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(description="HTTP REST Client with Retry Logic")
    p.add_argument("url", nargs="?")
    p.add_argument("--method", default="GET")
    p.add_argument("--data")
    p.add_argument("--file")
    p.add_argument("--header", action="append", default=[])
    p.add_argument("--retries", type=int, default=3)
    p.add_argument("--backoff", choices=["linear", "exponential"], default="exponential")
    p.add_argument("--retry-delay", type=float, default=1.0)
    p.add_argument("--retry-on", default="429,500,502,503,504")
    p.add_argument("--timeout", type=float, default=30.0)
    p.add_argument("--chain")
    p.add_argument("--expect-status", type=int)
    p.add_argument("--expect-json")
    p.add_argument("--output", default="request_log.json")
    return p


def main() -> int:
    args = parser().parse_args()
    defaults = {
        "method": args.method,
        "headers": parse_headers(args.header),
        "retries": args.retries,
        "backoff": args.backoff,
        "retry_delay": args.retry_delay,
        "retry_on": parse_retry_on(args.retry_on),
        "timeout": args.timeout,
    }
    output = Path(args.output).resolve()

    if args.chain:
        chain_payload = json.loads(Path(args.chain).read_text(encoding="utf-8"))
        chain = chain_payload if isinstance(chain_payload, list) else chain_payload.get("requests", [])
        run_chain(chain, defaults, output)
        return 0

    if not args.url:
        run_chain(DEMO_CHAIN, defaults, output)
        return 0

    body = None
    if args.file:
        body = Path(args.file).read_text(encoding="utf-8")
    elif args.data is not None:
        body = args.data

    spec = {
        "method": args.method,
        "url": args.url,
        "headers": defaults["headers"],
        "data": body,
        "expect_status": args.expect_status,
        "expect_json": args.expect_json,
    }
    attempt_log = []
    result = request_with_retry(spec, defaults, [], attempt_log)
    print_response(result)
    output.write_text(json.dumps({"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "request": spec, "attempts": attempt_log}, indent=2), encoding="utf-8")
    print(f"Saved log: {output}")
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except Exception as exc:
        print(f"Error: {exc}")
        raise SystemExit(1)