{"record":{"id":"2a09ece1f5c73fb1","repo":"we-promise/sure","slug":"missing-credentials","errorCode":"missing_credentials","errorMessage":"Refresh token is required","messagePattern":"Refresh token is required","errorType":"exception","errorClass":"Provider::Questrade::ConfigurationError","httpStatus":null,"severity":"error","filePath":"app/models/provider/questrade.rb","lineNumber":116,"sourceCode":"      activities.concat(Array(page[:activities]))\n      window_start = window_end + 1\n    end\n\n    { activities: activities }\n  end\n\n  private\n\n    RETRYABLE_ERRORS = [\n      SocketError, Net::OpenTimeout, Net::ReadTimeout,\n      Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ETIMEDOUT, EOFError\n    ].freeze\n\n    MAX_RETRIES = 3\n    INITIAL_RETRY_DELAY = 2 # seconds\n\n    def validate_configuration!\n      raise ConfigurationError.new(\"Refresh token is required\", :missing_credentials) if @refresh_token.blank?\n    end\n\n    def get_json(path, query: {})\n      ensure_authenticated!\n      with_retries(path) do\n        response = self.class.get(\"#{api_base}#{path}\", headers: auth_headers, query: query)\n        # Access token can expire mid-sync; refresh once and retry on 401.\n        if response.code == 401\n          authenticate!(force: true)\n          response = self.class.get(\"#{api_base}#{path}\", headers: auth_headers, query: query)\n        end\n        handle_response(response)\n      end\n    end\n\n    # Exchange the refresh token unless we already hold a valid access token.\n    def ensure_authenticated!\n      authenticate! if @access_token.nil? || @access_expires_at.nil? || Time.current >= @access_expires_at","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/questrade.rb#L98-L134","documentation":"Provider::Questrade's constructor calls validate_configuration!, which raises ConfigurationError(:missing_credentials) when the refresh_token keyword is nil or blank. It fires immediately at Provider::Questrade.new, before any network call — the client refuses to operate without a refresh token because Questrade has no other auth path.","triggerScenarios":"Provider::Questrade.new(refresh_token: nil) or refresh_token: \"\" — e.g. the persisted Questrade item has no token yet (user never finished authorization), the DB column was cleared, or a hash key typo ('refreshToken') yields nil via the missing default.","commonSituations":"Sync job firing before the OAuth return flow stored the first refresh token; seed/fixture data without a token; a migration wiping encrypted credentials; reading the wrong attribute off the item model (e.g. api_key instead of the questrade refresh token).","solutions":["Guard before constructing: check the item's stored questrade refresh token is present, and skip/queue the sync if blank.","Verify you pass refresh_token: (snake_case keyword) and that the persisted field actually maps to it.","If the token is genuinely missing, route the user through Questrade authorization to obtain the first refresh token.","Search for code paths that instantiate the provider from partial or stale item state (e.g. after failed onboarding) and add presence checks there."],"exampleFix":"# before\nprovider = Provider::Questrade.new(\n  refresh_token: item.settings[\"refresh_token\"], # nil -> raises in initialize\n  api_server: item.settings[\"api_server\"]\n)\n\n# after\ntoken = item.settings[\"refresh_token\"]\nif token.blank?\n  Rails.logger.info \"Questrade item #{item.id} has no refresh token; re-auth required\"\n  next\nend\nprovider = Provider::Questrade.new(refresh_token: token, api_server: item.settings[\"api_server\"])","handlingStrategy":"validation","validationCode":"token = item.settings[\"refresh_token\"].to_s\nraise ArgumentError, \"Questrade refresh token missing\" if token.blank?\nprovider = Provider::Questrade.new(refresh_token: token, api_server: item.settings[\"api_server\"])","typeGuard":null,"tryCatchPattern":"begin\n  provider = Provider::Questrade.new(refresh_token: token, on_token_refresh: persister)\nrescue Provider::Questrade::ConfigurationError => e\n  raise unless e.error_type == :missing_credentials\n  item.flag_reauthorization_required!(e)\nend","preventionTips":["Presence-check the stored refresh token in the job's guard clause so the sync never constructs a half-configured client.","Persist the very first refresh token as part of completing the OAuth return flow, before any sync is enqueued."],"tags":["questrade","configuration","refresh-token","constructor-validation","brokerage-api"],"backgroundTag":"missing-credentials","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}