{"record":{"id":"198fb85767adc94d","repo":"we-promise/sure","slug":"missing-api-server","errorCode":"missing_api_server","errorMessage":"No api_server; authenticate first","messagePattern":"No api_server; authenticate first","errorType":"exception","errorClass":"Provider::Questrade::ConfigurationError","httpStatus":null,"severity":"error","filePath":"app/models/provider/questrade.rb","lineNumber":185,"sourceCode":"      end\n\n      body = JSON.parse(response.body, symbolize_names: true)\n      @access_token      = body[:access_token]\n      @api_server        = body[:api_server]\n      @refresh_token     = body[:refresh_token] # rotate in-memory immediately\n      @access_expires_at = Time.current + (body[:expires_in].to_i - ACCESS_TOKEN_SKEW).seconds\n\n      # Hand the new credentials to the caller to persist (single-use token!).\n      @on_token_refresh&.call(\n        refresh_token: @refresh_token,\n        api_server:    @api_server,\n        access_token:  @access_token,\n        expires_at:    @access_expires_at\n      )\n    end\n\n    def api_base\n      raise ConfigurationError.new(\"No api_server; authenticate first\", :missing_api_server) if @api_server.blank?\n      @api_server.end_with?(\"/\") ? @api_server : \"#{@api_server}/\"\n    end\n\n    def auth_headers\n      {\n        \"Authorization\" => \"Bearer #{@access_token}\",\n        \"Accept\" => \"application/json\"\n      }\n    end\n\n    def iso(time)\n      time.utc.iso8601\n    end\n\n    def with_retries(operation_name, max_retries: MAX_RETRIES)\n      retries = 0\n\n      begin","sourceCodeStart":167,"sourceCodeEnd":203,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/questrade.rb#L167-L203","documentation":"Raised by Provider::Questrade#api_base: every data request builds its URL from @api_server (the per-session base URL Questrade returns in the token exchange), and if it is blank you get ConfigurationError(:missing_api_server). It means the client tried to call a data endpoint before a successful exchange established api_server, or the caller passed api_server: nil and never authenticated.","triggerScenarios":"Constructing with api_server: nil and calling get_json before ensure_authenticated!/exchange_token! has run; a token exchange body missing :api_server so @api_server stays nil; a new process instance created per request but only used for data calls without re-authenticating.","commonSituations":"Refactor that caches a provider instance across an exchange boundary; persisted item having the refresh token but a blank api_server column; stubbing exchange in tests so @api_server is never set, then hitting a data method.","solutions":["Always run authentication before data calls: the public methods go through get_json -> ensure_authenticated!, so make sure nothing bypasses it (a direct get_json call in a subclass or spec will hit this).","Persist api_server alongside the refresh token via on_token_refresh (the callback hands you api_server), and pass it back on the next construct so no exchange is needed.","If you stub/mock in tests, stub the exchange to return an api_server or stub get_json entirely.","Check the persisted item: blank api_server with a present refresh token means the callback did not save it — fix the persistence path."],"exampleFix":"# before\nprovider = Provider::Questrade.new(refresh_token: token, api_server: nil)\nprovider.get_holdings(account_id: id) # api_base raises missing_api_server\n\n# after\nprovider = Provider::Questrade.new(\n  refresh_token: token,\n  api_server: item.settings[\"api_server\"], # restored from last exchange\n  on_token_refresh: ->(creds) { item.update_credentials!(creds) }\n)\nprovider.list_accounts # triggers exchange which sets api_server","handlingStrategy":"validation","validationCode":"api_server = item.settings[\"api_server\"]\nif api_server.blank? && item.settings[\"refresh_token\"].blank?\n  raise ArgumentError, \"cannot reach Questrade without credentials; authenticate first\"\nend\n# get_json -> ensure_authenticated! sets api_server before any data call","typeGuard":null,"tryCatchPattern":"begin\n  provider.get_holdings(account_id: id)\nrescue Provider::Questrade::ConfigurationError => e\n  raise unless e.error_type == :missing_api_server\n  provider.exchange_token! # establish api_server, then retry\n  retry\nend","preventionTips":["Always persist api_server together with the rotated token in on_token_refresh.","Never call get_json from outside the public methods — they guarantee authentication order."],"tags":["questrade","configuration","api-server","authentication-order","brokerage-api"],"backgroundTag":"missing-base-url","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}