we-promise/sure · error · Provider::Sophtron::Error

invalid_response

invalid_response

Error message

Invalid Sophtron response format

What it means

Raised by Provider::Sophtron's extract_array_response helper when the parsed response is neither an Array nor a Hash containing any of the expected keys (it normalizes with with_indifferent_access and looks up each candidate key). It is a shape guard: Sophtron returned 2xx JSON, but in a form the caller's key list does not cover - typically a schema change or an error object wearing a success status. The full parsed payload is attached as details.

Source

Thrown at app/models/provider/sophtron.rb:323

        :post,
        "/UserInstitution/GetUserInstitutionAccounts",
        body: { UserInstitutionID: user_institution_id }
      )
      extract_array_response(parsed, :accounts, :Accounts)
    end

    def extract_array_response(parsed, *keys)
      return parsed if parsed.is_a?(Array)
      return [] if parsed.respond_to?(:empty?) && parsed.empty?

      if parsed.respond_to?(:with_indifferent_access)
        parsed = parsed.with_indifferent_access
        keys.each do |key|
          return Array(parsed[key]) if parsed.key?(key)
        end
      end

      raise Error.new("Invalid Sophtron response format", :invalid_response, details: parsed)
    end

    def request(method, api_path, body: nil, parse_json: true)
      options = { headers: auth_headers(method: method, api_path: api_path) }
      options[:body] = body.to_json if body

      response = self.class.public_send(method, "#{base_url}#{api_path}", options)
      handle_response(response, parse_json: parse_json)
    rescue Error
      raise
    rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e
      raise Error.new("Sophtron request failed: #{e.message}", :request_failed)
    rescue StandardError => e
      raise Error.new("Sophtron request failed: #{e.message}", :request_failed)
    end

    def auth_headers(method:, api_path:)
      {

View on GitHub (pinned to e69894adb9)

Solutions

  1. Inspect err details / logs - the parsed payload shows exactly which keys arrived; if one is 'error', chase the upstream message
  2. Reproduce the raw call with curl using the same auth header to see the true envelope
  3. If Sophtron renamed the field, add the new key to the *keys argument of the extract_array_response call site
  4. Keep the previous data snapshot on this failure instead of treating it as an empty result

Example fix

# before
arr = extract_array_response(parsed, :jobs)

# after - accept the renamed envelope from a newer Sophtron API
arr = extract_array_response(parsed, :jobs, :data, :results)
Defensive patterns

Strategy: type-guard

Type guard

def sophtron_array_envelope?(parsed, *keys)
  return true if parsed.is_a?(Array)
  return false unless parsed.is_a?(Hash)
  keys.any? { |k| parsed.key?(k.to_s) || parsed.key?(k.to_sym) }
end

Try / catch

begin
  items = client.get_institutions # passes through extract_array_response
rescue Provider::Sophtron::Error => e
  if e.error_type == :invalid_response
    log_payload_shape(e.message) # details holds the parsed payload
    keep_previous_data # never downgrade to empty
  else
    raise
  end
end

Prevention

When it happens

Trigger: A Sophtron endpoint normally returning {"jobs": [...]} or {"data": [...]} instead returns {"error": ...}, an envelope with a new key name after an API update, or a nested structure the key list was never taught (e.g. institution-specific responses).

Common situations: Sophtron ships an API revision renaming response fields; a middleware returns a JSON error object with HTTP 200; a new institution's data shape differs from existing ones; the key list wasn't extended when a new endpoint was wired through this helper.

Related errors


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