{"record":{"id":"00ad33ffcb46ae69","repo":"we-promise/sure","slug":"malformed-kraken-api-response-missing-error","errorCode":null,"errorMessage":"Malformed Kraken API response: missing error","messagePattern":"Malformed Kraken API response: missing error","errorType":"exception","errorClass":"Provider::Kraken::ApiError","httpStatus":null,"severity":"error","filePath":"app/models/provider/kraken.rb","lineNumber":131,"sourceCode":"      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\"]\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)","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/kraken.rb#L113-L149","documentation":"Raised as Provider::Kraken::ApiError when a 2xx Kraken response parses to a Hash but has no top-level 'error' key. Kraken's contract is strict: every response — success or failure — carries {error: [...], result: ...}. A missing 'error' key means the payload is not a genuine Kraken API response (gateway interference, version change, or truncated body), so the client refuses to trust it even if a 'result' is present.","triggerScenarios":"Any Kraken request whose 2xx JSON body lacks the 'error' field — e.g. a proxy injecting its own JSON object, Kraken shipping a new envelope without the key, or a mock/stub in tests returning {result: ...} without the error array. Fires after the Hash check but before in-band error classification.","commonSituations":"Test suites stubbing Kraken responses incompletely (missing the 'error' key), API gateways rewriting responses, upstream Kraken API contract changes, and responses from a different service reached via a wrong BASE_URL override.","solutions":["If seen in tests, fix stubs to include both keys: {\"error\" => [], \"result\" => ...}","curl the live endpoint and verify the body actually contains an 'error' key on success (it is [] on success)","Check for proxies/middleboxes between the app and api.kraken.com that could rewrite JSON","Verify BASE_URL has not been overridden to a non-Kraken host in the environment","Check Kraken changelog if live responses genuinely dropped the key; the guard may need updating alongside"],"exampleFix":"# before (test stub) — triggers 'missing error'\nstub_request(:post, %r{api.kraken.com}).to_return(body: {result: {ZUSD: 1}}.to_json)\n\n# after — stub matches Kraken's real envelope\nstub_request(:post, %r{api.kraken.com}).to_return(body: {error: [], result: {ZUSD: 1}}.to_json)","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"# Full envelope guard: both keys required before trusting a Kraken body\ndef valid_kraken_envelope?(parsed)\n  parsed.is_a?(Hash) && parsed.key?(\"error\") && parsed.key?(\"result\") && parsed[\"error\"].is_a?(Array)\nend","tryCatchPattern":"begin\n  result = provider.get_api_key_info\nrescue Provider::Kraken::ApiError => e\n  raise unless e.message.include?(\"missing error\")\n  notify_ops(\"Kraken envelope drift: response missing 'error' key — check for proxies/upstream changes\")\n  raise\nend","preventionTips":["Always stub BOTH 'error' and 'result' keys in test fixtures","Keep proxies transparent — any body rewriting breaks Kraken's strict envelope contract","Verify BASE_URL points at api.kraken.com (no mirror) when this fires","Monitor for upstream contract changes via a cheap scheduled ping to a public endpoint"],"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"}