we-promise/sure · error · Provider::Kraken::ApiError

Malformed Kraken API response: missing error

Error message

Malformed Kraken API response: missing error

What it means

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.

Source

Thrown at app/models/provider/kraken.rb:131

      nonce = params.fetch("nonce").to_s
      digest = OpenSSL::Digest::SHA256.digest(nonce + encoded_payload)
      hmac = OpenSSL::HMAC.digest("sha512", Base64.decode64(api_secret), path + digest)
      Base64.strict_encode64(hmac)
    end

    def handle_response(response)
      parsed = response.parsed_response

      unless response.code.between?(200, 299)
        raise ApiError, "Kraken API request failed: #{response.code}"
      end

      unless parsed.is_a?(Hash)
        raise ApiError, "Malformed Kraken API response"
      end

      unless parsed.key?("error")
        raise ApiError, "Malformed Kraken API response: missing error"
      end

      errors = Array(parsed["error"]).reject(&:blank?)
      raise classified_error(errors) if errors.any?

      unless parsed.key?("result")
        raise ApiError, "Malformed Kraken API response: missing result"
      end

      parsed["result"]
    end

    def classified_error(errors)
      message = errors.join(", ")

      case message
      when /Invalid key|Invalid signature|Temporary lockout/i
        AuthenticationError.new(message)

View on GitHub (pinned to e69894adb9)

Solutions

  1. If seen in tests, fix stubs to include both keys: {"error" => [], "result" => ...}
  2. curl the live endpoint and verify the body actually contains an 'error' key on success (it is [] on success)
  3. Check for proxies/middleboxes between the app and api.kraken.com that could rewrite JSON
  4. Verify BASE_URL has not been overridden to a non-Kraken host in the environment
  5. Check Kraken changelog if live responses genuinely dropped the key; the guard may need updating alongside

Example fix

# before (test stub) — triggers 'missing error'
stub_request(:post, %r{api.kraken.com}).to_return(body: {result: {ZUSD: 1}}.to_json)

# after — stub matches Kraken's real envelope
stub_request(:post, %r{api.kraken.com}).to_return(body: {error: [], result: {ZUSD: 1}}.to_json)
Defensive patterns

Strategy: type-guard

Type guard

# Full envelope guard: both keys required before trusting a Kraken body
def valid_kraken_envelope?(parsed)
  parsed.is_a?(Hash) && parsed.key?("error") && parsed.key?("result") && parsed["error"].is_a?(Array)
end

Try / catch

begin
  result = provider.get_api_key_info
rescue Provider::Kraken::ApiError => e
  raise unless e.message.include?("missing error")
  notify_ops("Kraken envelope drift: response missing 'error' key — check for proxies/upstream changes")
  raise
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/00ad33ffcb46ae69. Report an issue: GitHub.