we-promise/sure · error · Provider::Snaptrade::ApiError
Network error (not retried, non-idempotent request): #{e.mes
Error message
Network error (not retried, non-idempotent request): #{e.message} What it means
Raised by Provider::Snaptrade.without_retry, used for OAuth requests that must not be replayed (single-use authorization codes, token rotation). On a Faraday network error it deliberately does NOT retry, logs 'not retried, non-idempotent', and wraps the failure in an ApiError. The server may or may not have received/processed the request before the connection failed, so the resulting token state is unknown.
Source
Thrown at app/models/provider/snaptrade.rb:186
sleep(delay)
retry
else
Rails.logger.error(
"SnapTrade OAuth: #{operation_name} failed after #{max_retries} retries: " \
"#{e.class}: #{e.message}"
)
raise ApiError.new("Network error after #{max_retries} retries: #{e.message}")
end
end
end
# For requests that must not be replayed (single-use codes, token rotation):
# translate a network failure into an ApiError without retrying.
def without_retry(operation_name)
yield
rescue Faraday::TimeoutError, Faraday::ConnectionFailed, Errno::ECONNRESET, Errno::ETIMEDOUT => e
Rails.logger.error("SnapTrade OAuth: #{operation_name} failed (not retried, non-idempotent): #{e.class}: #{e.message}")
raise ApiError.new("Network error (not retried, non-idempotent request): #{e.message}")
end
def calculate_retry_delay(retry_count)
base_delay = INITIAL_RETRY_DELAY * (2 ** (retry_count - 1))
jitter = base_delay * rand * 0.25
[ base_delay + jitter, MAX_RETRY_DELAY ].min
end
end
attr_reader :snaptrade_item
def initialize(snaptrade_item)
raise ConfigurationError, "snaptrade_item is required" if snaptrade_item.nil?
@snaptrade_item = snaptrade_item
end
# --- Data methods. The SnapTrade user is implicit in the Bearer token. ---
View on GitHub (pinned to e69894adb9)
Solutions
- Do NOT replay the same request - the code/old refresh token may already be consumed server-side
- For an authorization-code exchange failure, restart the OAuth flow from the beginning to obtain a new code
- For refresh-token rotation failure, fall back to the stored previous token; if the server did rotate, catch the resulting auth error and re-authorize
- Fix the underlying network instability (see the logged exception class) so the one allowed attempt succeeds
Example fix
# before - retrying a non-idempotent token exchange begin exchange_code(code) rescue Provider::Snaptrade::ApiError retry # WRONG: code may already be consumed end # after - fail, then restart the flow for a fresh code begin exchange_code(code) rescue Provider::Snaptrade::ApiError => e redirect_to restart_oauth_flow_path # new authorize code end
Defensive patterns
Strategy: try-catch
Try / catch
begin tokens = oauth_client.exchange_code_without_retry(code) rescue Provider::Snaptrade::ApiError => e # state unknown: code may be consumed. Restart flow instead of replaying. invalidate_code(code) restart_authorization_flow end
Prevention
- Route every non-idempotent OAuth request through without_retry and never wrap it in your own retry loop
- Track single-use codes in a 'used' set so a bug can't replay them
- On refresh-rotation failure, keep the previous token and only re-authenticate if it stops working
When it happens
Trigger: A connection reset or timeout during the single POST of an authorization-code exchange or refresh-token rotation; a naive retry could double-spend the code or rotate the token twice, so the helper fails immediately instead.
Common situations: Flaky network at exactly the wrong moment during token exchange; retrying this error by hand and hitting the sibling 'invalid_grant' error because the code was already consumed; load balancer dropping idle keepalive connections on the first byte of the response.
Related errors
- Network error after #{max_retries} retries: #{e.message}
- pagination_error
- request_failed
- SnapTrade OAuth token request failed: #{error}
- SnapTrade OAuth is not configured
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/df5dfa83b4165822.
Report an issue: GitHub.