{"record":{"id":"e04c66c8690e3525","repo":"we-promise/sure","slug":"bad-request-e04c66","errorCode":"bad_request","errorMessage":"Questrade bad request (#{response.code})","messagePattern":"Questrade bad request \\(#(.+?)\\)","errorType":"exception","errorClass":"Provider::Questrade::Error","httpStatus":400,"severity":"error","filePath":"app/models/provider/questrade.rb","lineNumber":252,"sourceCode":"    # application logs or exception messages, where the importer re-logs it.\n    def capture_response_error(reason, response)\n      DebugLogEntry.capture(\n        category: \"provider_sync\",\n        level: \"error\",\n        message: \"Questrade API #{reason} (#{response.code})\",\n        source: self.class.name,\n        provider_key: \"questrade\",\n        metadata: { status: response.code, body: response.body.to_s.first(1000) }\n      )\n    end\n\n    def handle_response(response)\n      case response.code\n      when 200, 201\n        JSON.parse(response.body, symbolize_names: true)\n      when 400\n        capture_response_error(\"bad_request\", response)\n        raise Error.new(\"Questrade bad request (#{response.code})\", :bad_request)\n      when 401\n        raise AuthenticationError.new(\"Invalid or expired Questrade credentials\", :unauthorized)\n      when 403\n        raise AuthenticationError.new(\"Access forbidden - check your permissions\", :access_forbidden)\n      when 404\n        raise Error.new(\"Resource not found\", :not_found)\n      when 429\n        raise RetryableResponseError.new(\"Questrade rate limit exceeded. Please try again later.\", :rate_limited)\n      when 500..599\n        raise RetryableResponseError.new(\"Questrade server error (#{response.code}). Please try again later.\", :server_error)\n      else\n        capture_response_error(\"unexpected_response\", response)\n        raise Error.new(\"Questrade unexpected response (#{response.code})\", :unknown)\n      end\n    end\nend\n","sourceCodeStart":234,"sourceCodeEnd":269,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/questrade.rb#L234-L269","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\n# direct call with a 3-month window -> 400 err 1003\nprovider.send(:get_json, \"v1/accounts/#{id}/activities\",\n  query: { startTime: 90.days.ago.iso8601, endTime: Time.current.iso8601 })\n\n# after\n# get_activities chunks into <=30-day windows internally\nprovider.get_activities(account_id: id, start_date: 90.days.ago.to_date)","handlingStrategy":"validation","validationCode":"# keep activity windows inside Questrade's hard 31-day ceiling\nstart_date = start_date.to_date\nend_date = [ end_date.to_date, start_date + 29.days ].min\nraise ArgumentError, \"range too wide\" if (end_date - start_date).to_i > 30","typeGuard":null,"tryCatchPattern":"begin\n  provider.get_activities(account_id: id, start_date: start, end_date: stop)\nrescue Provider::Questrade::Error => e\n  if e.error_type == :bad_request\n    detail = DebugLogEntry.where(provider_key: \"questrade\", category: \"bad_request\").last\n    Rails.logger.warn(\"Questrade 400 detail: #{detail&.metadata&.dig('body')}\")\n  end\n  raise\nend","preventionTips":["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."],"tags":["questrade","http-400","date-range","activity-api","brokerage-api"],"backgroundTag":"http-400-bad-request","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}