we-promise/sure · error · Provider::Questrade::Error
bad_request
bad_request
Error message
Questrade bad request (#{response.code}) What it means
Raised by Provider::Questrade#handle_response on HTTP 400. Before raising, capture_response_error records status plus the first 1000 chars of the body to the DebugLogEntry channel (visible in /settings/debug), because Questrade wraps specifics — like err 1003 for activity ranges over 31 days — in the body. The raised message itself deliberately omits the body.
Source
Thrown at app/models/provider/questrade.rb:252
# application logs or exception messages, where the importer re-logs it.
def capture_response_error(reason, response)
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
- Open /settings/debug (or query DebugLogEntry for provider_key 'questrade') and read the captured body — it contains Questrade's numeric error code and message.
- For activities, always use get_activities, which chunks ranges into <=30-day windows; never call the endpoint directly with a wider range.
- Validate date inputs: pass Date objects and let iso() convert to UTC ISO8601.
- For symbols, pass only IDs previously returned by Questrade (e.g. from positions), deduplicated as get_symbols already does.
Example fix
# before
# direct call with a 3-month window -> 400 err 1003
provider.send(:get_json, "v1/accounts/#{id}/activities",
query: { startTime: 90.days.ago.iso8601, endTime: Time.current.iso8601 })
# after
# get_activities chunks into <=30-day windows internally
provider.get_activities(account_id: id, start_date: 90.days.ago.to_date) Defensive patterns
Strategy: validation
Validate before calling
# keep activity windows inside Questrade's hard 31-day ceiling start_date = start_date.to_date end_date = [ end_date.to_date, start_date + 29.days ].min raise ArgumentError, "range too wide" if (end_date - start_date).to_i > 30
Try / catch
begin
provider.get_activities(account_id: id, start_date: start, end_date: stop)
rescue Provider::Questrade::Error => e
if e.error_type == :bad_request
detail = DebugLogEntry.where(provider_key: "questrade", category: "bad_request").last
Rails.logger.warn("Questrade 400 detail: #{detail&.metadata&.dig('body')}")
end
raise
end Prevention
- Always use get_activities' built-in <=30-day chunking instead of raw activity calls.
- Pass Date objects; let iso() handle UTC ISO8601 formatting.
- Read the captured body in /settings/debug — it names the real Questrade error code.
When it happens
Trigger: get_activities with startTime/endTime spanning more than Questrade's strict 31-day ceiling (the SDK chunks into <=30-day windows precisely to avoid this — a caller bypassing get_activities hits it directly); malformed date formats in the query; get_symbols with invalid symbol IDs; passing a non-numeric account_id into the path.
Common situations: Custom code calling get_json('v1/accounts/X/activities') directly with a 60-day range instead of get_activities; timezone math making the UTC window 32 days; hand-building startTime strings without iso8601; symbol IDs from another broker's namespace.
Related errors
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/e04c66c8690e3525.
Report an issue: GitHub.