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
- Treat as re-auth territory: if the user revoked access, only re-authorization fixes it — flag the item for the user to reconnect.
- Verify api_server and access token came from the SAME exchange response (persist them together in on_token_refresh) — mixing sessions yields persistent 401s.
- Ensure the exchange is serialized per item (synchronize_exchange) so a racing refresh cannot consume the token under you.
- Check for NTP/clock drift on the host if 401s appear only near the 30-minute access-token boundary.
- 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
- Persist access_token, refresh_token and api_server atomically from one exchange response.
- Serialize exchanges per item so a concurrent refresh cannot invalidate the token in flight.
- When users disconnect the app in Questrade, expect this error and route to re-auth, not retry.
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.