Repository navigation
Transport retries replay non-idempotent POSTs (run_many, create, tables.upload) #85
Description
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).
roe-python/src/roe/utils/transport.py
Lines 42 to 82 in 0881424
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) roe-python/src/roe/utils/_dynamic_call.py
Lines 48 to 50 in 0881424
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(noidempotency_keyparameter, unlikerun/run_version)agents.create,agents.versions.create,policies.createtables.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:
- Don't retry POST on 5xx / read timeouts; only on connect errors (request never sent) and 429/503 with
Retry-After. - Generate an
Idempotency-Keyper logical call and reuse it across retries, if the API honours it on these endpoints. Addidempotency_keytorun_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
Don't retry run_many batch submissions
Don't retry run_many batch submissions