{"record":{"id":"7b982ffec4a31f68","repo":"we-promise/sure","slug":"kraken-api-request-failed-response-code","errorCode":null,"errorMessage":"Kraken API request failed: #{response.code}","messagePattern":"Kraken API request failed: #(.+?)","errorType":"exception","errorClass":"Provider::Kraken::ApiError","httpStatus":null,"severity":"error","filePath":"app/models/provider/kraken.rb","lineNumber":123,"sourceCode":"      {\n        \"API-Key\" => api_key,\n        \"API-Sign\" => sign(path, params)\n      }\n    end\n\n    def sign(path, params)\n      encoded_payload = URI.encode_www_form(params)\n      nonce = params.fetch(\"nonce\").to_s\n      digest = OpenSSL::Digest::SHA256.digest(nonce + encoded_payload)\n      hmac = OpenSSL::HMAC.digest(\"sha512\", Base64.decode64(api_secret), path + digest)\n      Base64.strict_encode64(hmac)\n    end\n\n    def handle_response(response)\n      parsed = response.parsed_response\n\n      unless response.code.between?(200, 299)\n        raise ApiError, \"Kraken API request failed: #{response.code}\"\n      end\n\n      unless parsed.is_a?(Hash)\n        raise ApiError, \"Malformed Kraken API response\"\n      end\n\n      unless parsed.key?(\"error\")\n        raise ApiError, \"Malformed Kraken API response: missing error\"\n      end\n\n      errors = Array(parsed[\"error\"]).reject(&:blank?)\n      raise classified_error(errors) if errors.any?\n\n      unless parsed.key?(\"result\")\n        raise ApiError, \"Malformed Kraken API response: missing result\"\n      end\n\n      parsed[\"result\"]","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/kraken.rb#L105-L141","documentation":"Raised as Provider::Kraken::ApiError when the Kraken REST API returns an HTTP status outside 200-299 for a signed private or public request. Kraken normally signals errors in-band with HTTP 200 + an 'error' array, so a non-2xx status indicates a transport/protocol-level problem: 4xx for malformed requests/auth (403 signature, 400 bad payload) or 5xx for Kraken outages. The HTTP code is embedded in the message verbatim.","triggerScenarios":"Any private_post/public_get call (BalanceEx, TradesHistory, Ledgers, Assets...) receiving e.g. 403 (bad API key/signature/IP restriction), 418/429 (rate-limited or banned at HTTP layer), 400 (malformed request), or 5xx (Kraken incident). Distinguishable from Kraken's in-band errors, which are classified separately into AuthenticationError/RateLimitError/NonceError/OTPRequiredError.","commonSituations":"API key without 'Query Funds'/'Query Ledger' permissions enabled, IP whitelist on the Kraken key excluding the server, clock skew breaking signatures, Kraken 5xx during maintenance windows, and Cloudflare-level blocks (403/429) when request volume is too high from one IP.","solutions":["Map the embedded code: 403 → check API key permissions and IP whitelist in Kraken settings; 418/429 → slow down (Kraken rate-limits per key); 5xx → check Kraken status and retry later","Verify the server clock is NTP-synced — signature validation depends on the nonce/timestamp","Confirm the API key has the required permissions (Query Funds, Query Open/Closed Orders, etc.) for the endpoint being called","If IP-whitelisted, add the egress IP of the app server to the key's whitelist","Honor Retry-After on 429 and add exponential backoff around private_post calls"],"exampleFix":"# before: caller retries blindly on any ApiError\nrescue Provider::Kraken::ApiError\n  retry\nend\n\n# after: branch on the embedded HTTP code\nrescue Provider::Kraken::ApiError => e\n  code = e.message[/\\d{3}/]\n  raise if code.in?([\"403\", \"400\"])           # config errors: fix, don't retry\n  sleep(e.message.include?(\"429\") ? 5 : 1)    # rate limit / transient 5xx\n  retry\nend","handlingStrategy":"try-catch","validationCode":"# Verify key permissions/clock before long syncs (cheap public call also works)\n# Kraken private calls need Query Funds + Query Ledger/Orders permissions and NTP-synced clock.\n# Pre-flight clock check:\ndrift = (Process.clock_gettime(Process::CLOCK_REALTIME) - Time.now.to_f).abs\nraise \"Server clock drift too high for Kraken signing\" if drift > 30","typeGuard":null,"tryCatchPattern":"begin\n  result = provider.get_extended_balance\nrescue Provider::Kraken::ApiError => e\n  code = e.message.scan(/\\b(\\d{3})\\b/).last\n  case code\n  when \"403\" then disconnect_kraken(\"Auth/IP restriction: #{e.message}\")\n  when \"418\", \"429\" then reschedule_with_backoff\n  when nil then raise\n  else raise if code.start_with?(\"5\").then { |five| !five } # 5xx → retry, else raise\n  end\nend","preventionTips":["Create Kraken API keys with only the Query permissions the sync needs, and whitelist the server IP","Keep server clocks NTP-synced — signatures are timestamp-sensitive","Respect Kraken's tiered call-rate limits with a local throttle (like RateLimitable) to avoid 418/429","Branch handling on the embedded HTTP code: auth errors are permanent, 429/5xx are transient"],"tags":["kraken","http-status","api-error","authentication","rate-limit","crypto"],"backgroundTag":"http-error-status","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}