Skip to content

feat(api): add connection retries and request timeout to API client#251

Open
ocervell wants to merge 1 commit into
Syslifters:mainfrom
ocervell:feat/api-client-retries-timeout
Open

feat(api): add connection retries and request timeout to API client#251
ocervell wants to merge 1 commit into
Syslifters:mainfrom
ocervell:feat/api-client-retries-timeout

Conversation

@ocervell

@ocervell ocervell commented Jul 3, 2026

Copy link
Copy Markdown

Problem

APIClient issues every request through the bare module-level requests.get/post/... functions with no session, no retry policy, and no timeout. Two consequences:

  • Transient connection/TLS failures fail immediately. A reset socket mid-handshake (SSLEOFError / UNEXPECTED_EOF_WHILE_READING), a dropped connection, or a brief 502/503 from a reverse proxy propagates straight to the caller on the first attempt, with no retry.
  • No timeout means a hung server blocks forever. requests with no timeout= waits indefinitely, so a stalled connection can hang a script or the CLI with no ceiling.

Change

APIClient now routes all requests through a requests.Session with a urllib3 Retry policy mounted on both http:// and https://, plus a default per-request timeout.

  • Retries cover connection-establishment failures (including TLS handshake resets) for every method, and the retryable status codes [429, 500, 502, 503, 504]. Backoff is exponential (backoff_factor=0.5), and Retry-After is honored.
  • Idempotency is respected: only idempotent methods are retried on a status code (urllib3's default allowed_methods), so POST/PATCH are never replayed against the server — no double-creates.
  • raise_on_status=False preserves existing error semantics: after retries are exhausted, the response is returned and the existing response.raise_for_status() in _do_request raises exactly as before.
  • Default timeout of 30s prevents indefinite hangs.

All values are tunable in reptor/settings.py and overridable per-call via kwargs.

Files

File Change
reptor/api/APIClient.py New _build_session(); __init__ builds self._session; _prepare_kwargs injects default timeout; _do_request uses the session
reptor/settings.py API_TIMEOUT, API_MAX_RETRIES, API_RETRY_BACKOFF_FACTOR, API_RETRY_STATUS_FORCELIST
reptor/api/tests/test_apiclient.py New — 5 tests asserting the retry policy, default timeout/verify, caller overrides, insecureverify=False, and that requests route through the session

Testing

299 passed on the non-integration suite (including the 5 new tests). The only failures observed locally are pre-existing GhostWriter tests that fail identically on unmodified main due to a missing optional gql dependency — unrelated to this change.

Notes

Backward-compatible: no public API changes, all defaults overridable. This does not paper over a deterministic misconfiguration (wrong scheme/port, hard TLS-version mismatch) — those still fail after retries — but it makes the client resilient to genuinely transient failures and bounds request time.

APIClient issued every request through the bare requests.* functions with
no session, no retry policy, and no timeout. Transient connection/TLS
failures (e.g. a reset handshake raising SSLEOFError) and brief 5xx
responses failed on the first attempt, and a hung server could block
forever.

Route all requests through a requests.Session with a urllib3 Retry mounted
on http:// and https://, plus a default per-request timeout. Retries cover
connection establishment (all methods) and the retryable status codes
[429, 500, 502, 503, 504] for idempotent methods only, so POST/PATCH are
never replayed. raise_on_status=False preserves the existing
response.raise_for_status() error semantics. All values are tunable in
settings and overridable per call.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5vSjfkBuGAAHdKxHS3ySm

@aronmolnar aronmolnar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much for your Pull request.
I added some notes. Would you mind looking at them?
Thank you!

Comment thread reptor/api/APIClient.py
return kwargs | {
'headers': kwargs.get('headers', {}) | self._get_headers(json_content=json_content),
'verify': kwargs.get('verify', self.verify),
'timeout': kwargs.get('timeout', settings.API_TIMEOUT),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A default timeout of 30 seconds prevents long-running requests to fail. This would affect, for example rendering processes of large reports or project exports. Maybe the timeout should be increased for those endpoints to max(5m, API_TIMEOUT)

Comment thread reptor/settings.py
API_TIMEOUT = 30 # per-request (connect, read) timeout in seconds; prevents indefinite hangs
API_MAX_RETRIES = 3 # retries for transient connection/TLS failures and retryable status codes
API_RETRY_BACKOFF_FACTOR = 0.5 # exponential backoff: 0s, 0.5s, 1s, 2s, ...
API_RETRY_STATUS_FORCELIST = [429, 500, 502, 503, 504] # only these statuses are retried

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The users should probably have the option to control those values (at least API_TIMEOUT) via CLI options of via their config.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants