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

unauthorized

unauthorized

Error message

Invalid or expired Questrade credentials

What it means

Raised by Provider::Questrade#handle_response on HTTP 401 from a data call (against api_server). Note get_json already handles the common case: on 401 it force-refreshes the token and replays the request once. So this error means the retried request still came back 401 — the access token is genuinely invalid (revoked authorization, wrong api_server/token pairing) rather than merely expired mid-sync.

Source

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

      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. Treat as re-auth territory: if the user revoked access, only re-authorization fixes it — flag the item for the user to reconnect.
  2. Verify api_server and access token came from the SAME exchange response (persist them together in on_token_refresh) — mixing sessions yields persistent 401s.
  3. Ensure the exchange is serialized per item (synchronize_exchange) so a racing refresh cannot consume the token under you.
  4. Check for NTP/clock drift on the host if 401s appear only near the 30-minute access-token boundary.
  5. Reproduce with curl using the access token against the stored api_server to confirm whether Questrade still honors it.

Example fix

# before
balances = provider.get_balances(account_id: id)

# after
begin
  balances = provider.get_balances(account_id: id)
rescue Provider::Questrade::AuthenticationError => e
  raise unless e.error_type == :unauthorized
  # get_json already retried after a forced refresh; still 401 -> dead session
  item.flag_reauthorization_required!(e)
  raise
end
Defensive patterns

Strategy: try-catch

Validate before calling

# ensure token and api_server come from the SAME exchange before data calls
if item.settings["api_server"].blank? || item.settings["access_token"].blank?
  provider.exchange_token!
end

Try / catch

begin
  provider.get_balances(account_id: id)
rescue Provider::Questrade::AuthenticationError => e
  raise unless e.error_type == :unauthorized
  # get_json already force-refreshed once; still 401 means revoked or mixed session
  item.flag_reauthorization_required!(e)
end

Prevention

When it happens

Trigger: User revoked the app's authorization in Questrade, invalidating issued tokens; api_server from a different session paired with a fresh token (Questrade tokens are bound to the api_server returned with them); a forced refresh that raced another process's exchange, leaving an already-consumed token; clock skew beyond ACCESS_TOKEN_SKEW=60s combined with a refresh that fails silently.

Common situations: User disconnects the app in Questrade's security settings while a sync runs; item row restored from an old backup mixing an old api_server with a newer token; two workers exchanging concurrently despite the serialize-exchange design.

Related errors


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