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

API error: #{response.code}

Error message

API error: #{response.code}

What it means

Raised by Provider::Coinbase#handle_response (coinbase.rb:206) as Provider::Coinbase::ApiError for any non-2xx status that is not 401 or 429. The message is Coinbase's own error text (parsed.dig("errors", 0, "message") || parsed["error"] || parsed["message"]) or the literal "API error: #{response.code}" when the body yields nothing parseable.

Source

Thrown at app/models/provider/coinbase.rb:206

        "Authorization" => "Bearer #{generate_jwt(method, path)}",
        "Content-Type" => "application/json"
      }
    end

    def handle_response(response)
      parsed = response.parsed_response

      case response.code
      when 200..299
        parsed.is_a?(Hash) ? parsed : { "data" => parsed }
      when 401
        error_msg = extract_error_message(parsed) || "Unauthorized - check your API key and secret"
        raise AuthenticationError, error_msg
      when 429
        raise RateLimitError, "Rate limit exceeded"
      else
        error_msg = extract_error_message(parsed) || "API error: #{response.code}"
        raise ApiError, error_msg
      end
    end

    def extract_error_message(parsed)
      return parsed if parsed.is_a?(String)
      return nil unless parsed.is_a?(Hash)

      parsed.dig("errors", 0, "message") || parsed["error"] || parsed["message"]
    end
end

View on GitHub (pinned to e69894adb9)

Solutions

  1. Read the message text: a Coinbase error string points at the exact bad param/resource; "API error: <code>" means the body was unparseable — capture the raw response body to diagnose.
  2. Fix the offending request parameter/resource id for 4xx errors.
  3. Retry 5xx with backoff and check the Coinbase status page during suspected incidents.
  4. Pin/request the correct API version in headers if a behavior change is suspected.
  5. Isolate per-resource failures so one 4xx does not abort a whole import.

Example fix

# before
begin
  txs = provider.get_transactions(account_id: id)
rescue => e
  raise # anything kills the sync
end

# after
begin
  txs = provider.get_transactions(account_id: id)
rescue Provider::Coinbase::ApiError => e
  if /5\d\d/.match?(e.message)
    raise RetryableJobError, e.message
  else
    Rails.logger.error("Coinbase call for #{id} failed: #{e.message}")
  end
end
Defensive patterns

Strategy: try-catch

Type guard

def coinbase_api_error?(err)
  err.is_a?(Provider::Coinbase::ApiError)
end

Try / catch

begin
  txs = provider.get_transactions(account_id: id)
rescue Provider::Coinbase::ApiError => e
  if /API error: 5/.match?(e.message)
    raise RetryableJobError, e.message
  else
    Rails.logger.error("Coinbase failure for #{id}: #{e.message}")
  end
end

Prevention

When it happens

Trigger: HTTP 400 for invalid request params (bad pagination cursor, unknown account id, malformed body); HTTP 404 for deleted resources; HTTP 500/503 during Coinbase incidents; HTML/JSON-parse failures from proxies that leave parsed nil so the generic "API error: <code>" shows; version changes where an endpoint moves or changes shape.

Common situations: Coinbase API version drift after an announcement (fields renamed, endpoints deprecated); passing an account id from a different key's scope; deploys behind a corporate proxy that rewrites error bodies; incident-window syncs returning 503s that a job retries too aggressively.

Related errors


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