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

SnapTrade item has no refresh token

Error message

SnapTrade item has no refresh token

What it means

Inside refresh_access_token! (called under a DB row lock from ensure_fresh_token! when the access token is within TOKEN_EXPIRY_LEEWAY of expiry, or reactively after a 401), the item must have an oauth_refresh_token to mint a new access token. If the column is blank, AuthenticationError is raised; the method's rescue then marks the item status :requires_update, writes a DebugLogEntry (category provider_sync, provider snaptrade), and re-raises, so the sync aborts but the failure is visible in /settings/debug.

Source

Thrown at app/models/provider/snaptrade.rb:348

    # that specific token (called from request_json). In that case we skip the HTTP refresh
    # only if the DB row's access token has already changed since we made the failed request
    # (i.e. another caller already won the race) -- an expiry-based freshness check would be
    # wrong here since the server rejected a token we believed was still time-valid.
    # When `previous_access_token` is absent, we're refreshing proactively (from
    # ensure_fresh_token!) and skip only if the reloaded row is still time-fresh.
    def refresh_access_token!(previous_access_token: nil)
      snaptrade_item.with_lock do
        snaptrade_item.reload

        if previous_access_token.present?
          next if snaptrade_item.oauth_access_token != previous_access_token
        else
          expires_at = snaptrade_item.oauth_token_expires_at
          next if expires_at.present? && expires_at > TOKEN_EXPIRY_LEEWAY.seconds.from_now
        end

        refresh_token = snaptrade_item.oauth_refresh_token
        raise AuthenticationError, "SnapTrade item has no refresh token" if refresh_token.blank?

        payload = self.class.refresh_tokens(refresh_token: refresh_token)
        snaptrade_item.apply_oauth_tokens!(payload)
      end
    rescue AuthenticationError => e
      mark_requires_update!
      DebugLogEntry.capture(
        category: "provider_sync",
        level: :error,
        message: "SnapTrade token refresh failed: #{e.message}",
        source: "Provider::Snaptrade",
        provider_key: "snaptrade",
        family: snaptrade_item.try(:family),
        metadata: { snaptrade_item_id: snaptrade_item.try(:id) }
      )
      raise
    end

View on GitHub (pinned to e69894adb9)

Solutions

  1. Have the user re-authorize the item (new authorize_url flow) so a complete token set is stored
  2. Check the stored payload: in console inspect the item's oauth_refresh_token / the original exchange response for a refresh_token field before re-auth
  3. If re-auth also yields no refresh token, verify the OAuth app's allowed scopes/grants on dashboard.snaptrade.com

Example fix

# before
def perform(item_id)
  Provider::Snaptrade.new(SnaptradeItem.find(item_id)).get_positions
end

# after
def perform(item_id)
  item = SnaptradeItem.find(item_id)
  if item.oauth_token_expires_at.present? && item.oauth_refresh_token.blank?
    item.update!(status: :requires_update)
    return
  end
  Provider::Snaptrade.new(item).get_positions
rescue Provider::Snaptrade::AuthenticationError
  retry if item.reload.status_previously_changed?
end
Defensive patterns

Strategy: try-catch

Validate before calling

# Skip proactive refresh when there is nothing to refresh with
if item.oauth_token_expires_at.present? && item.oauth_refresh_token.blank?
  item.update!(status: :requires_update)
end

Type guard

def snaptrade_item_refreshable?(item)
  item.oauth_access_token.present? && item.oauth_refresh_token.present?
end

Try / catch

begin
  provider.get_positions
rescue Provider::Snaptrade::AuthenticationError
  item.reload
  raise unless item.status_requires_update? # already flagged by the provider; surface re-auth to user
end

Prevention

When it happens

Trigger: Item has an access token but the stored refresh token is nil -- e.g. the original token payload from SnapTrade contained no refresh_token, the column was cleared, or a previous partial apply_oauth_tokens! persisted only the access token. It fires on the first sync after the access token enters the 60-second leeway window or gets a 401.

Common situations: SnapTrade issued no offline refresh token for the authorization (scope/grant configuration); encrypted-attribute migration or restore losing the refresh column; concurrent flows where an older row version is written back; items connected against a pre-release API that changed its token payload shape.

Related errors


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