Skip to content

Transport retries replay non-idempotent POSTs (run_many, create, tables.upload) #85

Open

Description

@jadenfix

RoeRetryTransport retries 5xx, 408, 429 and transport errors on every method, including POST. Only the multipart agent-run helpers opt out (x-roe-skip-retry in _dynamic_call.py).

  • def handle_request(self, request: httpx.Request) -> httpx.Response:
    if request.headers.get(_BYPASS_HEADER):
    return super().handle_request(request)
    for attempt in range(self.max_retries + 1):
    try:
    response = super().handle_request(request)
    except httpx.TransportError as exc:
    if attempt >= self.max_retries:
    raise
    wait_time = min(2**attempt, 10)
    logger.warning(
    "Transport error on %s %s (%s), retrying in %ds (attempt %d/%d)",
    request.method,
    request.url,
    exc,
    wait_time,
    attempt + 1,
    self.max_retries,
    )
    time.sleep(wait_time)
    continue
    if (
    not _should_retry_status(response.status_code)
    or attempt >= self.max_retries
    ):
    return response
    wait_time = min(2**attempt, 10)
    logger.warning(
    "Roe API returned %d for %s %s, retrying in %ds (attempt %d/%d)",
    response.status_code,
    request.method,
    request.url,
    wait_time,
    attempt + 1,
    self.max_retries,
    )
    response.close()
    time.sleep(wait_time)
  • request_headers["x-roe-skip-retry"] = (
    "1" # multipart POST — do not replay (aligns with TS)
    )

So these POSTs are sent up to max_retries + 1 (default 4) times when the response is a 5xx or the read times out:

  • agents.run_many (no idempotency_key parameter, unlike run / run_version)
  • agents.create, agents.versions.create, policies.create
  • tables.upload (multipart, but not opted out)

If the server committed the request and a gateway then returns 502, or the response read times out, each replay creates another batch of jobs / agent / policy / table. For run_many that's up to 4x the billed jobs.

Reproduced with a fake transport that always returns 503: run_many, agents.create, policies.create and tables.upload each sent 4 POSTs; agents.run sent 1.

The README documents that JSON POST bodies may be replayed, so this may be intentional, but the comment in client.py (L72-74) says retries are for "502/503/504 (idempotent methods only)", which doesn't match.

Possible fixes, happy to send a PR for whichever you prefer:

  1. Don't retry POST on 5xx / read timeouts; only on connect errors (request never sent) and 429/503 with Retry-After.
  2. Generate an Idempotency-Key per logical call and reuse it across retries, if the API honours it on these endpoints. Add idempotency_key to run_many.

roe-golang has the same behaviour (all POSTs, including single agent runs).

Go counterpart: roe-ai/roe-golang#55

Note: testing this end to end requires a Roe API key, so I checked it against the code and a local mock server rather than the live API.

Activity

  1. added a commit that references this issue on Oct 9, 2026
    338a429
  2. added a commit that references this issue on Oct 19, 2026
    3a43565
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions