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.
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: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 hitRemoteDisconnectedand crash the whole run with an uncaught traceback:_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 absoluteTOKEN_URLwith 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.