{"record":{"id":"f4f507451e40f58d","repo":"instructure/canvas-lms","slug":"unauthorized-request","errorCode":null,"errorMessage":"unauthorized request","messagePattern":"unauthorized request","errorType":"http","errorClass":"Pv4Unauthorized","httpStatus":401,"severity":"error","filePath":"app/models/page_view/pv4_client.rb","lineNumber":60,"sourceCode":"              limit: nil)\n      end_time ||= Time.now.utc\n      start_time ||= Time.at(0).utc\n\n      params = \"start_time=#{start_time.utc.iso8601(PRECISION)}\"\n      params << \"&end_time=#{end_time.utc.iso8601(PRECISION)}\"\n      params << \"&#{cached_root_account_uuids_for(user:)}\"\n      params << \"&last_page_view_id=#{last_page_view_id}\" if last_page_view_id\n      params << \"&limit=#{limit}\" if limit\n      response = CanvasHttp.get(\n        @uri.merge(\"users/#{user.global_id}/page_views?#{params}\").to_s,\n        request_headers\n      )\n\n      case response.code.to_i\n      when 400\n        raise Pv4BadRequest, \"invalid request\"\n      when 401\n        raise Pv4Unauthorized, \"unauthorized request\"\n      when 404\n        raise Pv4NotFound, \"resource not found\"\n      when 429\n        raise Pv4TooManyRequests, \"rate limit exceeded\"\n      end\n\n      json =\n        begin\n          response.body.empty? ? {} : JSON.parse(response.body)\n        rescue JSON::ParserError\n          {}\n        end\n      raise Pv4EmptyResponse, \"the response is empty or does not contain expected keys\" unless json[\"page_views\"]\n\n      json[\"page_views\"].map! do |pv|\n        pv[\"session_id\"] = pv.delete(\"sessionid\")\n        vhost = pv.delete(\"vhost\")\n        http_request = pv.delete(\"http_request\")","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/page_view/pv4_client.rb#L42-L78","documentation":"Pv4Client#fetch raises Pv4Unauthorized with message \"unauthorized request\" when the PV4 page-views API returns HTTP 401. The request's credentials (PV4 auth headers/JWT) were rejected or missing, so the PV4 service refused authentication.","triggerScenarios":"Calling Pv4Client.for_user when request_headers contain an expired, mis-signed, or missing PV4 auth token — e.g. PV4 encryption/secret keys rotated or differing between Canvas and the PV4 service, or clocks drifting so the signed token falls outside the accepted window.","commonSituations":"PV4 auth key mismatch after a config/secret rotation on one side only; missing PV4 secrets in an environment (stage/prod parity issues); clock skew between app and PV4 servers invalidating timestamps in the signed request.","solutions":["Verify PV4 authentication configuration (secret/keys in Canvas and PV4 service) match and were rotated on both sides together.","Check server clock sync (NTP) if the auth scheme includes timestamps.","Confirm the environment has the required PV4 credentials set at all (missing config renders invalid headers).","Rescue Pv4Unauthorized and fall back to an alternate page-view source (e.g. legacy page_views table) while fixing credentials."],"exampleFix":"// before\nbegin\n  Pv4Client.for_user(user, filters)\nrescue Pv4NotFound; end\n// after\nbegin\n  Pv4Client.for_user(user, filters)\nrescue Pv4Unauthorized\n  Rails.logger.error('PV4 auth rejected - check pv4 credentials/clock skew')\n  fallback_to_legacy_page_views(user)\nend","handlingStrategy":"fallback","validationCode":"raise 'PV4 not configured' if Canvas::Plugin.find('pv4')&.settings&.values.any?(&:blank?)","typeGuard":null,"tryCatchPattern":"begin\n  Pv4Client.for_user(user, filters)\nrescue Pv4Unauthorized\n  fallback_to_legacy_page_views(user)\nend","preventionTips":["Keep PV4 secrets in sync between Canvas and the PV4 service across rotations","Run NTP on hosts so signed timestamps stay valid","Add a health check that authenticates against PV4 and alerts on 401s"],"tags":["ruby","http","authentication","page-views"],"backgroundTag":"authentication-required","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}