{"record":{"id":"d03d684b50ee14cb","repo":"we-promise/sure","slug":"unauthorized-d03d68","errorCode":null,"errorMessage":"Unauthorized","messagePattern":"Unauthorized","errorType":"exception","errorClass":"Provider::Binance::AuthenticationError","httpStatus":401,"severity":"error","filePath":"app/models/provider/binance.rb","lineNumber":182,"sourceCode":"    # HMAC-SHA256 of the query string.\n    # Accepts either a Hash of params or a pre-built query string.\n    def sign(params)\n      query_string = params.is_a?(Hash) ? URI.encode_www_form(params.sort) : params\n      OpenSSL::HMAC.hexdigest(\"sha256\", api_secret, query_string)\n    end\n\n    def auth_headers\n      { \"X-MBX-APIKEY\" => api_key }\n    end\n\n    def handle_response(response)\n      parsed = response.parsed_response\n\n      case response.code\n      when 200..299\n        parsed\n      when 401\n        raise AuthenticationError, extract_error_message(parsed) || \"Unauthorized\"\n      when 429\n        raise RateLimitError, \"Rate limit exceeded\"\n      else\n        msg = extract_error_message(parsed) || \"API error: #{response.code}\"\n        raise InvalidSymbolError, msg if parsed.is_a?(Hash) && parsed[\"code\"] == -1121\n        raise ApiError, msg\n      end\n    end\n\n    def extract_error_message(parsed)\n      return parsed if parsed.is_a?(String)\n      return nil unless parsed.is_a?(Hash)\n      parsed[\"msg\"] || parsed[\"message\"] || parsed[\"error\"]\n    end\nend\n","sourceCodeStart":164,"sourceCodeEnd":198,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/binance.rb#L164-L198","documentation":"Raised by Provider::Binance#handle_response when the Binance REST API returns HTTP 401. The client authenticates by sending the api_key in the X-MBX-APIKEY header (auth_headers). Binance returns 401 when the key is malformed, deleted, or lacks permission for the requested endpoint. The message comes from extract_error_message, which reads parsed[\"msg\"], falling back to \"Unauthorized\" when the body is unparseable.","triggerScenarios":"Any signed-off request method on Provider::Binance (account, trade, or user-data endpoints) with a blank, mistyped, revoked, or expired API key; a key created without the required \"Read\" (or trade) permission; a key with an IP access restriction that does not include the server's egress IP; or a key deleted from the Binance account while still stored in the app's provider credentials.","commonSituations":"Rotating or regenerating keys on Binance without updating the stored credential; copying the key with trailing whitespace/newline; environment drift between staging (working key) and production (stale key); IP-restricted keys behind a container/cloud NAT whose egress IP changed; test suites hitting live Binance with placeholder keys.","solutions":["Inspect the stored credential for the Binance provider account and confirm api_key is present and has no whitespace.","In Binance API Management, confirm the key still exists, is active, and has the required permission (at minimum \"Enable Reading\").","If the key has an IP restriction, add the server's current egress IP (or remove the restriction) and retry.","Regenerate the key on Binance and update the stored credential, then retry the request.","If the body is empty (message fell back to \"Unauthorized\"), log response.parsed_response and the raw body to see Binance's exact msg (e.g. \"API-key format invalid\" vs \"API-key format invalid.\")."],"exampleFix":"# before\nprovider = Provider::Binance.new(api_key: key, secret_key: secret)\ndata = provider.get_account # raises AuthenticationError, \"Unauthorized\"\n\n# after\nraise ArgumentError, \"Binance api_key is blank\" if key.to_s.strip.empty?\nprovider = Provider::Binance.new(api_key: key.strip, secret_key: secret)\nbegin\n  data = provider.get_account\nrescue Provider::Binance::AuthenticationError => e\n  # mark the provider connection as needing re-auth instead of failing the job\n  provider_account.update!(status: :reauth_required)\n  Rails.logger.warn(\"Binance auth failed: #{e.message}\")\nend","handlingStrategy":"try-catch","validationCode":"raise ArgumentError, \"Binance api_key is blank\" if api_key.to_s.strip.empty?\nraise ArgumentError, \"Binance api_key has whitespace\" if api_key != api_key.strip","typeGuard":"def binance_auth_error?(err)\n  err.is_a?(Provider::Binance::AuthenticationError)\nend","tryCatchPattern":"begin\n  result = provider.get_account\nrescue Provider::Binance::AuthenticationError => e\n  provider_account.update!(status: :reauth_required)\n  DebugLogEntry.capture(category: \"binance\", level: \"error\", message: e.message, provider_key: \"binance\")\nend","preventionTips":["Validate the key is non-blank and stripped before constructing Provider::Binance.","Store credentials encrypted and re-verify after any key rotation on Binance.","Confirm IP-restriction whitelist covers the app's egress IP before deploying.","Never auto-retry AuthenticationError — flag the connection for user re-auth instead."],"tags":["binance","authentication","http-401","api-key","crypto"],"backgroundTag":"http-401-unauthorized","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}