{"record":{"id":"6375fd19387e1260","repo":"we-promise/sure","slug":"malformed-kraken-api-response-missing-result","errorCode":null,"errorMessage":"Malformed Kraken API response: missing result","messagePattern":"Malformed Kraken API response: missing result","errorType":"exception","errorClass":"Provider::Kraken::ApiError","httpStatus":null,"severity":"error","filePath":"app/models/provider/kraken.rb","lineNumber":138,"sourceCode":"      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\"]\n    end\n\n    def classified_error(errors)\n      message = errors.join(\", \")\n\n      case message\n      when /Invalid key|Invalid signature|Temporary lockout/i\n        AuthenticationError.new(message)\n      when /Invalid nonce/i\n        NonceError.new(message)\n      when /Permission denied|Invalid permissions/i\n        PermissionError.new(message)\n      when /Rate limit exceeded|Too many requests|limit exceeded|Throttled/i\n        RateLimitError.new(message)\n      when /otp|2fa|two.factor/i","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/kraken.rb#L120-L156","documentation":"Raised as Provider::Kraken::ApiError when a 2xx Kraken response is a valid Hash with an (empty) 'error' array but no 'result' key. Per Kraken's contract, a success is {error: [], result: ...}; reaching this guard means the call was neither an in-band error (those were classified into AuthenticationError/RateLimitError/NonceError/etc.) nor a normal success — the payload is structurally a success envelope missing its payload. This is the last of three shape guards in handle_response.","triggerScenarios":"Any Kraken request returning {error: [], ...} without 'result' — typically partial responses from a gateway, truncated JSON that still parsed, an upstream envelope change, or test stubs returning only {error: []}. Note some Kraken endpoints legitimately return result: {} (present but empty), which does NOT trigger this; the key itself must be absent.","commonSituations":"Truncated responses from proxies under load, Kraken API changes relocating the payload, incomplete test stubs, and HTTParty parsing edge cases where a cut-off body still yields a Hash without the later keys.","solutions":["Reproduce with curl and inspect the full body: does 'result' exist at top level for this endpoint?","Check whether a proxy or response-size limit is truncating large Kraken responses (balance/trade history payloads are big)","Retry transiently — truncation artifacts usually resolve on a second attempt","Fix test stubs to include a 'result' key with the expected payload shape","If Kraken genuinely changed the envelope, update handle_response to read the new location of the payload"],"exampleFix":"# before (test stub) — passes error check, then fails 'missing result'\nstub_request(:post, %r{api.kraken.com}).to_return(body: {error: []}.to_json)\n\n# after — complete success envelope\nstub_request(:post, %r{api.kraken.com}).to_return(body: {error: [], result: {\"ZUSD\": \"100.0\"}}.to_json)","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"# Treat success only when the envelope is complete and result is present\ndef kraken_success?(parsed)\n  parsed.is_a?(Hash) && parsed[\"error\"].is_a?(Array) && parsed[\"error\"].empty? && parsed.key?(\"result\")\nend","tryCatchPattern":"begin\n  result = provider.get_ledgers\nrescue Provider::Kraken::ApiError => e\n  raise unless e.message.include?(\"missing result\")\n  # truncated payloads: retry once, then alert — indicates gateway truncation or contract change\n  sleep 2\n  retry\nend","preventionTips":["Include a 'result' key (even {}) in all Kraken test stubs","Watch response sizes through proxies — large Ledger/TradesHistory payloads are the usual truncation victims","Retry once on 'missing result': truncation is often transient","Alert if persistent: upstream may have moved the payload to a new field"],"tags":["kraken","schema-validation","api-contract","crypto","test-stub"],"backgroundTag":"schema-validation-failed","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}