Skip to content

_refresh_token bypasses _send's transport retry — RemoteDisconnected aborts the run #1

Description

@hexmode

Problem

HelpScoutClient._send() retries transport-level failures (RemoteDisconnected, read timeouts, reset connections) with backoff, but _refresh_token() issues its OAuth2 token request as a raw, unretried call:

# src/helpscout_mailbox/client.py (v1.0.3, line ~94)
response = self._session.post(
    TOKEN_URL,
    data={"grant_type": "client_credentials", ...},
    timeout=TIMEOUT,
)

When the HelpScout API is flaky — e.g. a read timeout pushes elapsed time past token expiry, so the next _send() triggers a refresh — that unprotected POST can hit RemoteDisconnected and crash the whole run with an uncaught traceback:

http.client.RemoteDisconnected: Remote end closed connection without response

_send()'s transport retry can't be reused as-is because it hardcodes {BASE_URL}{path}, a JSON body, and Bearer-header auth — whereas token refresh is a form-encoded POST to the absolute TOKEN_URL with no Bearer header.

Suggested fix

Extract the transport-retry/backoff loop into a small helper (e.g. _send_with_transport_retry(method, url, **kwargs)) that both _send() and _refresh_token() call, so every request — including the token POST — retries transient transport failures instead of aborting. Status-code handling (401/429/5xx) stays in _send() on top of the helper.

Context

This was found and fixed downstream in Connectify/devops-fabric PR #5788, against the in-tree copy of this client (connectify_devops/provider_invoices/helpscout.py). This issue tracks porting that fix upstream so it isn't lost when devops-fabric adopts/renames to this package.

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