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

bad_request

bad_request

Error message

Questrade bad request (#{response.code})

What it means

Raised by Provider::Questrade#handle_response on HTTP 400. Before raising, capture_response_error records status plus the first 1000 chars of the body to the DebugLogEntry channel (visible in /settings/debug), because Questrade wraps specifics — like err 1003 for activity ranges over 31 days — in the body. The raised message itself deliberately omits the body.

Source

Thrown at app/models/provider/questrade.rb:252

    # application logs or exception messages, where the importer re-logs it.
    def capture_response_error(reason, response)
      DebugLogEntry.capture(
        category: "provider_sync",
        level: "error",
        message: "Questrade API #{reason} (#{response.code})",
        source: self.class.name,
        provider_key: "questrade",
        metadata: { status: response.code, body: response.body.to_s.first(1000) }
      )
    end

    def handle_response(response)
      case response.code
      when 200, 201
        JSON.parse(response.body, symbolize_names: true)
      when 400
        capture_response_error("bad_request", response)
        raise Error.new("Questrade bad request (#{response.code})", :bad_request)
      when 401
        raise AuthenticationError.new("Invalid or expired Questrade credentials", :unauthorized)
      when 403
        raise AuthenticationError.new("Access forbidden - check your permissions", :access_forbidden)
      when 404
        raise Error.new("Resource not found", :not_found)
      when 429
        raise RetryableResponseError.new("Questrade rate limit exceeded. Please try again later.", :rate_limited)
      when 500..599
        raise RetryableResponseError.new("Questrade server error (#{response.code}). Please try again later.", :server_error)
      else
        capture_response_error("unexpected_response", response)
        raise Error.new("Questrade unexpected response (#{response.code})", :unknown)
      end
    end
end

View on GitHub (pinned to e69894adb9)

Solutions

  1. Open /settings/debug (or query DebugLogEntry for provider_key 'questrade') and read the captured body — it contains Questrade's numeric error code and message.
  2. For activities, always use get_activities, which chunks ranges into <=30-day windows; never call the endpoint directly with a wider range.
  3. Validate date inputs: pass Date objects and let iso() convert to UTC ISO8601.
  4. For symbols, pass only IDs previously returned by Questrade (e.g. from positions), deduplicated as get_symbols already does.

Example fix

# before
# direct call with a 3-month window -> 400 err 1003
provider.send(:get_json, "v1/accounts/#{id}/activities",
  query: { startTime: 90.days.ago.iso8601, endTime: Time.current.iso8601 })

# after
# get_activities chunks into <=30-day windows internally
provider.get_activities(account_id: id, start_date: 90.days.ago.to_date)
Defensive patterns

Strategy: validation

Validate before calling

# keep activity windows inside Questrade's hard 31-day ceiling
start_date = start_date.to_date
end_date = [ end_date.to_date, start_date + 29.days ].min
raise ArgumentError, "range too wide" if (end_date - start_date).to_i > 30

Try / catch

begin
  provider.get_activities(account_id: id, start_date: start, end_date: stop)
rescue Provider::Questrade::Error => e
  if e.error_type == :bad_request
    detail = DebugLogEntry.where(provider_key: "questrade", category: "bad_request").last
    Rails.logger.warn("Questrade 400 detail: #{detail&.metadata&.dig('body')}")
  end
  raise
end

Prevention

When it happens

Trigger: get_activities with startTime/endTime spanning more than Questrade's strict 31-day ceiling (the SDK chunks into <=30-day windows precisely to avoid this — a caller bypassing get_activities hits it directly); malformed date formats in the query; get_symbols with invalid symbol IDs; passing a non-numeric account_id into the path.

Common situations: Custom code calling get_json('v1/accounts/X/activities') directly with a 60-day range instead of get_activities; timezone math making the UTC window 32 days; hand-building startTime strings without iso8601; symbol IDs from another broker's namespace.

Related errors


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