{"record":{"id":"df5dfa83b4165822","repo":"we-promise/sure","slug":"network-error-not-retried-non-idempotent-request","errorCode":null,"errorMessage":"Network error (not retried, non-idempotent request): #{e.message}","messagePattern":"Network error \\(not retried, non-idempotent request\\): #(.+?)","errorType":"exception","errorClass":"Provider::Snaptrade::ApiError","httpStatus":null,"severity":"error","filePath":"app/models/provider/snaptrade.rb","lineNumber":186,"sourceCode":"            sleep(delay)\n            retry\n          else\n            Rails.logger.error(\n              \"SnapTrade OAuth: #{operation_name} failed after #{max_retries} retries: \" \\\n              \"#{e.class}: #{e.message}\"\n            )\n            raise ApiError.new(\"Network error after #{max_retries} retries: #{e.message}\")\n          end\n        end\n      end\n\n      # For requests that must not be replayed (single-use codes, token rotation):\n      # translate a network failure into an ApiError without retrying.\n      def without_retry(operation_name)\n        yield\n      rescue Faraday::TimeoutError, Faraday::ConnectionFailed, Errno::ECONNRESET, Errno::ETIMEDOUT => e\n        Rails.logger.error(\"SnapTrade OAuth: #{operation_name} failed (not retried, non-idempotent): #{e.class}: #{e.message}\")\n        raise ApiError.new(\"Network error (not retried, non-idempotent request): #{e.message}\")\n      end\n\n      def calculate_retry_delay(retry_count)\n        base_delay = INITIAL_RETRY_DELAY * (2 ** (retry_count - 1))\n        jitter = base_delay * rand * 0.25\n        [ base_delay + jitter, MAX_RETRY_DELAY ].min\n      end\n  end\n\n  attr_reader :snaptrade_item\n\n  def initialize(snaptrade_item)\n    raise ConfigurationError, \"snaptrade_item is required\" if snaptrade_item.nil?\n    @snaptrade_item = snaptrade_item\n  end\n\n  # --- Data methods. The SnapTrade user is implicit in the Bearer token. ---\n","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/snaptrade.rb#L168-L204","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before - retrying a non-idempotent token exchange\nbegin\n  exchange_code(code)\nrescue Provider::Snaptrade::ApiError\n  retry # WRONG: code may already be consumed\nend\n\n# after - fail, then restart the flow for a fresh code\nbegin\n  exchange_code(code)\nrescue Provider::Snaptrade::ApiError => e\n  redirect_to restart_oauth_flow_path # new authorize code\nend","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  tokens = oauth_client.exchange_code_without_retry(code)\nrescue Provider::Snaptrade::ApiError => e\n  # state unknown: code may be consumed. Restart flow instead of replaying.\n  invalidate_code(code)\n  restart_authorization_flow\nend","preventionTips":["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"],"tags":["snaptrade","oauth","non-idempotent","network","no-retry"],"backgroundTag":"non-idempotent-network-failure","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}